|
|
|
@ -311,6 +311,236 @@ exports.respondToRequestedBooking = async (req, reply) => {
|
|
|
|
|
// const Customer = require("../models/customer.model"); // e.g., { customerId, name, phone, address, latitude, longitude }
|
|
|
|
|
// const Supplier = require("../models/supplier.model"); // e.g., { supplierId, name, phone, tankerName, address, latitude, longitude }
|
|
|
|
|
|
|
|
|
|
// const parseNumber = (v, def = 0) => {
|
|
|
|
|
// if (v === null || v === undefined) return def;
|
|
|
|
|
// const n = parseFloat(String(v).replace(/,/g, ""));
|
|
|
|
|
// return Number.isFinite(n) ? n : def;
|
|
|
|
|
// };
|
|
|
|
|
// const mkBookingId = (prefix = "RBK") => {
|
|
|
|
|
// const ts = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
|
|
|
|
// const rnd = Math.floor(Math.random() * 1e6).toString().padStart(6, "0");
|
|
|
|
|
// return `${prefix}-${ts}-${rnd}`;
|
|
|
|
|
// };
|
|
|
|
|
// const isIsoYMD = (s) => /^\d{4}-\d{2}-\d{2}$/.test(s);
|
|
|
|
|
|
|
|
|
|
// // "2025-10-21" -> "21-Oct-2025" (to match your old saved sample)
|
|
|
|
|
// const formatDDMonYYYY = (isoYmd) => {
|
|
|
|
|
// if (!isIsoYMD(isoYmd)) return isoYmd;
|
|
|
|
|
// const [y, m, d] = isoYmd.split("-").map(Number);
|
|
|
|
|
// const mon = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][(m || 1) - 1];
|
|
|
|
|
// return `${String(d).padStart(2,"0")}-${mon}-${y}`;
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
// exports.respondToRecurringRequestedBooking = async (req, reply) => {
|
|
|
|
|
// const { _id } = req.params;
|
|
|
|
|
// const { action, supplierId } = req.body;
|
|
|
|
|
|
|
|
|
|
// if (!mongoose.Types.ObjectId.isValid(_id)) {
|
|
|
|
|
// return reply.code(400).send({ message: "Invalid recurring requested booking ID" });
|
|
|
|
|
// }
|
|
|
|
|
// if (!["accept", "reject"].includes(action)) {
|
|
|
|
|
// return reply.code(400).send({ message: "Action must be 'accept' or 'reject'" });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// try {
|
|
|
|
|
// const booking = await RecurringRequestedBooking.findById(_id);
|
|
|
|
|
// if (!booking) {
|
|
|
|
|
// return reply.code(404).send({ message: "Recurring requested booking not found" });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// const supplierEntry = booking.requested_suppliers.find((s) => s.supplierId === supplierId);
|
|
|
|
|
// if (!supplierEntry) {
|
|
|
|
|
// return reply.code(404).send({ message: "Supplier not found in this booking" });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Update supplier response on the recurring request
|
|
|
|
|
// supplierEntry.status = action === "accept" ? "accepted" : "rejected";
|
|
|
|
|
// await booking.save();
|
|
|
|
|
|
|
|
|
|
// if (action === "reject") {
|
|
|
|
|
// return reply.code(200).send({
|
|
|
|
|
// status_code: 200,
|
|
|
|
|
// message: "Recurring booking rejected by supplier successfully",
|
|
|
|
|
// data: booking,
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // ACCEPT: build per-date TankerBooking docs with rich fields
|
|
|
|
|
// const allDates = Array.isArray(booking.dates) ? booking.dates.filter(isIsoYMD) : [];
|
|
|
|
|
// if (!allDates.length) {
|
|
|
|
|
// return reply.code(400).send({ message: "No valid ISO dates found in booking.dates" });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Preload related party info to fill address/phones/names
|
|
|
|
|
// const [customerDoc, supplierDoc] = await Promise.all([
|
|
|
|
|
// User.findOne({ customerId: booking.customerId }).lean(),
|
|
|
|
|
// Supplier.findOne({ supplierId: supplierId }).lean(),
|
|
|
|
|
// ]);
|
|
|
|
|
|
|
|
|
|
// // Pull commonly needed values (tolerant defaults to match legacy)
|
|
|
|
|
// const customerPhone = customerDoc?.phone ?? null;
|
|
|
|
|
// const supplierPhone = supplierDoc?.phone ?? null;
|
|
|
|
|
|
|
|
|
|
// const customerName = customerDoc?.username ?? customerDoc?.displayName ?? "";
|
|
|
|
|
// const supplierName = supplierDoc?.suppliername ?? supplierDoc?.companyName ?? "";
|
|
|
|
|
|
|
|
|
|
// const tankerName = supplierDoc?.tankerName ?? null;
|
|
|
|
|
// const tankName = null; // if you associate a tank per-customer, populate from your Tank model here
|
|
|
|
|
// const tankLocation = null; // same as above
|
|
|
|
|
|
|
|
|
|
// // prefer customer address (your legacy sample stores a geocoded customer address)
|
|
|
|
|
// const address = customerDoc?.profile.address1 ?? null;
|
|
|
|
|
// const latitude = customerDoc?.latitude ?? null;
|
|
|
|
|
// const longitude = customerDoc?.longitude ?? null;
|
|
|
|
|
|
|
|
|
|
// // price: from supplier’s quoted_amount in this request (fallback null)
|
|
|
|
|
// const price = (supplierEntry?.quoted_amount ?? null) !== null
|
|
|
|
|
// ? String(supplierEntry.quoted_amount)
|
|
|
|
|
// : null;
|
|
|
|
|
|
|
|
|
|
// // numeric fields
|
|
|
|
|
// const numericCapacity = parseNumber(booking.capacity);
|
|
|
|
|
// const numericQuantity = parseNumber(booking.quantity);
|
|
|
|
|
// const totalRequired = Number.isFinite(booking.total_required_capacity)
|
|
|
|
|
// ? booking.total_required_capacity
|
|
|
|
|
// : numericCapacity * numericQuantity;
|
|
|
|
|
|
|
|
|
|
// // dedupe check
|
|
|
|
|
// const existing = await Tankerbooking.find(
|
|
|
|
|
// {
|
|
|
|
|
// customerId: booking.customerId,
|
|
|
|
|
// supplierId: supplierId,
|
|
|
|
|
// date: { $in: allDates },
|
|
|
|
|
// time: booking.time, // keep your stored time format intact
|
|
|
|
|
// },
|
|
|
|
|
// { date: 1 }
|
|
|
|
|
// ).lean();
|
|
|
|
|
|
|
|
|
|
// const existingSet = new Set((existing || []).map(e => e.date));
|
|
|
|
|
// const newDates = allDates.filter(d => !existingSet.has(d));
|
|
|
|
|
// if (!newDates.length) {
|
|
|
|
|
// return reply.code(200).send({
|
|
|
|
|
// status_code: 200,
|
|
|
|
|
// message: "All dates already have bookings; nothing to create.",
|
|
|
|
|
// data: { created: 0, skippedExistingDates: allDates },
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // ---------- BUILD ENRICHED DOCS (matches your legacy example fields) ----------
|
|
|
|
|
// const todayIso = new Date().toISOString().slice(0, 10);
|
|
|
|
|
|
|
|
|
|
// const docs = newDates.map((d) => ({
|
|
|
|
|
// // Required/IDs
|
|
|
|
|
// customerId: booking.customerId,
|
|
|
|
|
// supplierId: supplierId,
|
|
|
|
|
// bookingid: mkBookingId("RBK"),
|
|
|
|
|
|
|
|
|
|
// // Legacy display & logistics
|
|
|
|
|
// tankName, // null (fill if you link tank per-customer)
|
|
|
|
|
// tankLocation, // null (fill if available)
|
|
|
|
|
// tankerName, // from Supplier if present
|
|
|
|
|
|
|
|
|
|
// // Dates/times (kept both ISO & legacy formats as you showed)
|
|
|
|
|
// dateOfOrder: todayIso, // "2025-09-10"
|
|
|
|
|
// expectedDateOfDelivery: formatDDMonYYYY(d), // "21-Oct-2025" style (legacy sample)
|
|
|
|
|
// date: d, // keep ISO in `date` too
|
|
|
|
|
// time: booking.time, // keep your request time as-is
|
|
|
|
|
|
|
|
|
|
// // Water & capacity
|
|
|
|
|
// type_of_water: booking.type_of_water,
|
|
|
|
|
// typeofwater: booking.type_of_water, // legacy field name kept too
|
|
|
|
|
// capacity: booking.capacity, // e.g., "100" or "10,000 L"
|
|
|
|
|
// quantity: booking.quantity, // string
|
|
|
|
|
// total_required_capacity: totalRequired,
|
|
|
|
|
|
|
|
|
|
// // Money / status
|
|
|
|
|
// price: price, // from quoted_amount (string) or null
|
|
|
|
|
// payment_status: "due",
|
|
|
|
|
// orderStatus: "accepted",
|
|
|
|
|
|
|
|
|
|
// // Contacts & names
|
|
|
|
|
// address: address, // from customer
|
|
|
|
|
// customerPhone: customerPhone,
|
|
|
|
|
// supplierPhone: supplierPhone,
|
|
|
|
|
// customerName: customerName,
|
|
|
|
|
// supplierName: supplierName,
|
|
|
|
|
|
|
|
|
|
// // Delivery defaults (match your legacy doc)
|
|
|
|
|
// delivery_agent: "null",
|
|
|
|
|
// delivery_agent_mobile: "null",
|
|
|
|
|
// delivery_agent_alternative_mobile: "null",
|
|
|
|
|
|
|
|
|
|
// // Metering defaults
|
|
|
|
|
// initial_water_level: "null",
|
|
|
|
|
// final_water_level: "null",
|
|
|
|
|
// start_time: "null",
|
|
|
|
|
// stop_time: "null",
|
|
|
|
|
// quantityDelivered: null,
|
|
|
|
|
|
|
|
|
|
// // Accounting defaults
|
|
|
|
|
// amount_paid: null,
|
|
|
|
|
// amount_due: null,
|
|
|
|
|
// distrubance_price: "none",
|
|
|
|
|
// amount_difference: "none",
|
|
|
|
|
// payment_mode: null,
|
|
|
|
|
// remarks: null,
|
|
|
|
|
|
|
|
|
|
// // Device/geo defaults
|
|
|
|
|
// tankerRunningStatus: "0",
|
|
|
|
|
// latitude: latitude ?? undefined, // keep same field names as your legacy doc
|
|
|
|
|
// longitude: longitude ?? undefined, // if not available, omit field
|
|
|
|
|
|
|
|
|
|
// // Misc you already store
|
|
|
|
|
// frequency: booking.frequency,
|
|
|
|
|
// weekly_count: booking.weekly_count ?? 1,
|
|
|
|
|
// deliveredDate: null,
|
|
|
|
|
// distrubance_status: "0",
|
|
|
|
|
// }));
|
|
|
|
|
// // ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// // Insert without transactions, tolerate duplicates if unique index exists
|
|
|
|
|
// let insertedCount = 0;
|
|
|
|
|
// let duplicateErrors = 0;
|
|
|
|
|
// try {
|
|
|
|
|
// const res = await Tankerbooking.collection.insertMany(docs, { ordered: false });
|
|
|
|
|
// insertedCount = res.insertedCount || 0;
|
|
|
|
|
// } catch (e) {
|
|
|
|
|
// if (e && e.writeErrors && Array.isArray(e.writeErrors)) {
|
|
|
|
|
// insertedCount = e.result?.nInserted ?? 0;
|
|
|
|
|
// duplicateErrors = e.writeErrors.length;
|
|
|
|
|
// } else {
|
|
|
|
|
// throw e;
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// return reply.code(200).send({
|
|
|
|
|
// status_code: 200,
|
|
|
|
|
// message: `Recurring booking accepted. Created ${insertedCount} tanker booking(s).`,
|
|
|
|
|
// data: {
|
|
|
|
|
// createdDates: newDates.slice(0, insertedCount),
|
|
|
|
|
// skippedExistingDates: allDates.filter(d => existingSet.has(d)),
|
|
|
|
|
// duplicateConflicts: duplicateErrors,
|
|
|
|
|
// },
|
|
|
|
|
// });
|
|
|
|
|
// } catch (err) {
|
|
|
|
|
// console.error(err);
|
|
|
|
|
// throw boom.internal("Failed to update recurring supplier response", err);
|
|
|
|
|
// }
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// controllers/supplier.controller.js (only the changed parts shown for brevity)
|
|
|
|
|
// const boom = require("@hapi/boom");
|
|
|
|
|
// const mongoose = require("mongoose");
|
|
|
|
|
|
|
|
|
|
// const RecurringRequestedBooking = require("../models/recurringRequestedBooking.model");
|
|
|
|
|
// const TankerBooking = require("../models/tankerBooking.model");
|
|
|
|
|
// const Customer = require("../models/customer.model");
|
|
|
|
|
// const Supplier = require("../models/supplier.model");
|
|
|
|
|
|
|
|
|
|
// ---------- helpers (IST + formatting) ----------
|
|
|
|
|
const parseNumber = (v, def = 0) => {
|
|
|
|
|
if (v === null || v === undefined) return def;
|
|
|
|
|
const n = parseFloat(String(v).replace(/,/g, ""));
|
|
|
|
@ -322,19 +552,135 @@ const mkBookingId = (prefix = "RBK") => {
|
|
|
|
|
return `${prefix}-${ts}-${rnd}`;
|
|
|
|
|
};
|
|
|
|
|
const isIsoYMD = (s) => /^\d{4}-\d{2}-\d{2}$/.test(s);
|
|
|
|
|
const MON = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
|
|
|
|
|
|
|
|
// Return a Date adjusted to IST (UTC+5:30) without changing the original instant
|
|
|
|
|
const toIST = (d = new Date()) => {
|
|
|
|
|
const utc = d.getTime() + (d.getTimezoneOffset() * 60000);
|
|
|
|
|
// IST = UTC + 5:30
|
|
|
|
|
return new Date(utc + (5 * 60 + 30) * 60000);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// "2025-10-21" -> "21-Oct-2025" (to match your old saved sample)
|
|
|
|
|
const formatDDMonYYYY = (isoYmd) => {
|
|
|
|
|
if (!isIsoYMD(isoYmd)) return isoYmd;
|
|
|
|
|
// Format Date -> "DD-Mon-YYYY - HH:MM" in IST
|
|
|
|
|
const fmtDDMonYYYY_HHMM = (dateObj) => {
|
|
|
|
|
const dd = String(dateObj.getDate()).padStart(2, "0");
|
|
|
|
|
const mon = MON[dateObj.getMonth()];
|
|
|
|
|
const yyyy = dateObj.getFullYear();
|
|
|
|
|
const hh = String(dateObj.getHours()).padStart(2, "0");
|
|
|
|
|
const mm = String(dateObj.getMinutes()).padStart(2, "0");
|
|
|
|
|
return `${dd}-${mon}-${yyyy} - ${hh}:${mm}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Extract "HH:MM" (24h) from arbitrary string like "21-Nov-2025 - 14:37" or "4:00 PM to 6:00 PM"
|
|
|
|
|
// If not found, default "00:00"
|
|
|
|
|
const extractHHMM = (timeStr) => {
|
|
|
|
|
if (!timeStr) return "00:00";
|
|
|
|
|
// First try 24h "14:37"
|
|
|
|
|
const m24 = timeStr.match(/\b(\d{1,2}):(\d{2})\b/);
|
|
|
|
|
if (m24) {
|
|
|
|
|
const h = String(Math.min(23, parseInt(m24[1],10))).padStart(2,"0");
|
|
|
|
|
const m = m24[2];
|
|
|
|
|
return `${h}:${m}`;
|
|
|
|
|
}
|
|
|
|
|
// crude parse for "4:00 PM" → 16:00
|
|
|
|
|
const ampm = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i);
|
|
|
|
|
if (ampm) {
|
|
|
|
|
let h = parseInt(ampm[1],10);
|
|
|
|
|
const m = ampm[2];
|
|
|
|
|
const p = ampm[3].toUpperCase();
|
|
|
|
|
if (p === "PM" && h !== 12) h += 12;
|
|
|
|
|
if (p === "AM" && h === 12) h = 0;
|
|
|
|
|
return `${String(h).padStart(2,"0")}:${m}`;
|
|
|
|
|
}
|
|
|
|
|
return "00:00";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Combine ISO date "YYYY-MM-DD" + a time string → IST "DD-Mon-YYYY - HH:MM"
|
|
|
|
|
const fmtFromISOAndTime = (isoYmd, timeStr) => {
|
|
|
|
|
if (!isIsoYMD(isoYmd)) return isoYmd; // fallback
|
|
|
|
|
const [y, m, d] = isoYmd.split("-").map(Number);
|
|
|
|
|
const mon = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][(m || 1) - 1];
|
|
|
|
|
return `${String(d).padStart(2,"0")}-${mon}-${y}`;
|
|
|
|
|
const [hh, mm] = extractHHMM(timeStr).split(":").map(Number);
|
|
|
|
|
// Construct a Date in UTC for that local time, then convert to IST display
|
|
|
|
|
const dt = new Date(Date.UTC(y, (m - 1), d, hh, mm, 0));
|
|
|
|
|
// We only care about display in IST:
|
|
|
|
|
return fmtDDMonYYYY_HHMM(toIST(dt));
|
|
|
|
|
};
|
|
|
|
|
// ----
|
|
|
|
|
// --------------------------------------------
|
|
|
|
|
|
|
|
|
|
// ---------- add these helpers near your other helpers ----------
|
|
|
|
|
const to12h = (h24, m) => {
|
|
|
|
|
let h = h24 % 12 || 12;
|
|
|
|
|
const ampm = h24 >= 12 ? "PM" : "AM";
|
|
|
|
|
return `${h}:${String(m).padStart(2,"0")} ${ampm}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Extract first time from any string: supports "14:37", "4:00 PM", "21-Nov-2025 - 14:37", "4:00 PM to 6:00 PM"
|
|
|
|
|
const parseFirstTimeToHM = (timeStr) => {
|
|
|
|
|
if (!timeStr) return { h: 0, m: 0 };
|
|
|
|
|
// Try explicit 12h with AM/PM
|
|
|
|
|
const ampm = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i);
|
|
|
|
|
if (ampm) {
|
|
|
|
|
let h = parseInt(ampm[1], 10);
|
|
|
|
|
const m = parseInt(ampm[2], 10);
|
|
|
|
|
const p = ampm[3].toUpperCase();
|
|
|
|
|
if (p === "PM" && h !== 12) h += 12;
|
|
|
|
|
if (p === "AM" && h === 12) h = 0;
|
|
|
|
|
return { h, m };
|
|
|
|
|
}
|
|
|
|
|
// Try any 24h HH:MM in the string
|
|
|
|
|
const m24 = timeStr.match(/\b(\d{1,2}):(\d{2})\b/);
|
|
|
|
|
if (m24) {
|
|
|
|
|
const h = Math.max(0, Math.min(23, parseInt(m24[1], 10)));
|
|
|
|
|
const m = Math.max(0, Math.min(59, parseInt(m24[2], 10)));
|
|
|
|
|
return { h, m };
|
|
|
|
|
}
|
|
|
|
|
return { h: 0, m: 0 };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// If already "X to Y" (case-insensitive), keep it. Otherwise, make a slot of `windowHours` starting at first parsed time.
|
|
|
|
|
const normalizeTimeForLegacy = (timeStr, windowHours = 2) => {
|
|
|
|
|
if (typeof timeStr === "string" && / to /i.test(timeStr)) {
|
|
|
|
|
return timeStr; // already in "4:00 PM to 6:00 PM"
|
|
|
|
|
}
|
|
|
|
|
const { h, m } = parseFirstTimeToHM(timeStr);
|
|
|
|
|
const start = to12h(h, m);
|
|
|
|
|
const endDate = new Date(Date.UTC(2000, 0, 1, h, m)); // dummy date, add hours
|
|
|
|
|
endDate.setUTCHours(endDate.getUTCHours() + windowHours);
|
|
|
|
|
const end = to12h(endDate.getUTCHours(), endDate.getUTCMinutes());
|
|
|
|
|
return `${start} to ${end}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---- Booking ID helpers (IST-based) ----
|
|
|
|
|
// Get YYYYMMDD in IST (UTC+05:30)
|
|
|
|
|
const getISTDatePart = (d = new Date()) => {
|
|
|
|
|
const utcMs = d.getTime() + d.getTimezoneOffset() * 60000;
|
|
|
|
|
const ist = new Date(utcMs + (5 * 60 + 30) * 60000);
|
|
|
|
|
const y = ist.getFullYear();
|
|
|
|
|
const m = String(ist.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
const day = String(ist.getDate()).padStart(2, "0");
|
|
|
|
|
return `${y}${m}${day}`; // YYYYMMDD
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Make ARM id with one random digit (0-9)
|
|
|
|
|
const mkArmBookingIdIST = () => `ARM${getISTDatePart()}${Math.floor(Math.random() * 10)}`;
|
|
|
|
|
|
|
|
|
|
// Ensure uniqueness in DB (recommended since you may create many in one day)
|
|
|
|
|
const generateUniqueArmId = async () => {
|
|
|
|
|
// up to 20 tries with single digit; then fall back to two digits
|
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
|
|
|
const id = mkArmBookingIdIST();
|
|
|
|
|
const exists = await Tankerbooking.exists({ bookingid: id });
|
|
|
|
|
if (!exists) return id;
|
|
|
|
|
}
|
|
|
|
|
// fallback with two random digits to avoid collisions
|
|
|
|
|
const fallback = `ARM${getISTDatePart()}${Math.floor(Math.random()*10)}${Math.floor(Math.random()*10)}`;
|
|
|
|
|
return fallback;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
exports.respondToRecurringRequestedBooking = async (req, reply) => {
|
|
|
|
|
const { _id } = req.params;
|
|
|
|
|
const { action, supplierId } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!mongoose.Types.ObjectId.isValid(_id)) {
|
|
|
|
|
return reply.code(400).send({ message: "Invalid recurring requested booking ID" });
|
|
|
|
|
}
|
|
|
|
@ -344,16 +690,13 @@ exports.respondToRecurringRequestedBooking = async (req, reply) => {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const booking = await RecurringRequestedBooking.findById(_id);
|
|
|
|
|
if (!booking) {
|
|
|
|
|
return reply.code(404).send({ message: "Recurring requested booking not found" });
|
|
|
|
|
}
|
|
|
|
|
if (!booking) return reply.code(404).send({ message: "Recurring requested booking not found" });
|
|
|
|
|
|
|
|
|
|
const supplierEntry = booking.requested_suppliers.find((s) => s.supplierId === supplierId);
|
|
|
|
|
const supplierEntry = booking.requested_suppliers.find(s => s.supplierId === supplierId);
|
|
|
|
|
if (!supplierEntry) {
|
|
|
|
|
return reply.code(404).send({ message: "Supplier not found in this booking" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update supplier response on the recurring request
|
|
|
|
|
supplierEntry.status = action === "accept" ? "accepted" : "rejected";
|
|
|
|
|
await booking.save();
|
|
|
|
|
|
|
|
|
@ -365,57 +708,40 @@ exports.respondToRecurringRequestedBooking = async (req, reply) => {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ACCEPT: build per-date TankerBooking docs with rich fields
|
|
|
|
|
const allDates = Array.isArray(booking.dates) ? booking.dates.filter(isIsoYMD) : [];
|
|
|
|
|
if (!allDates.length) {
|
|
|
|
|
return reply.code(400).send({ message: "No valid ISO dates found in booking.dates" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Preload related party info to fill address/phones/names
|
|
|
|
|
const [customerDoc, supplierDoc] = await Promise.all([
|
|
|
|
|
User.findOne({ customerId: booking.customerId }).lean(),
|
|
|
|
|
Supplier.findOne({ supplierId: supplierId }).lean(),
|
|
|
|
|
Supplier.findOne({ supplierId }).lean(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Pull commonly needed values (tolerant defaults to match legacy)
|
|
|
|
|
const customerPhone = customerDoc?.phone ?? null;
|
|
|
|
|
const supplierPhone = supplierDoc?.phone ?? null;
|
|
|
|
|
|
|
|
|
|
const customerName = customerDoc?.username ?? customerDoc?.displayName ?? "";
|
|
|
|
|
const supplierName = supplierDoc?.suppliername ?? supplierDoc?.companyName ?? "";
|
|
|
|
|
|
|
|
|
|
const tankerName = supplierDoc?.tankerName ?? null;
|
|
|
|
|
const tankName = null; // if you associate a tank per-customer, populate from your Tank model here
|
|
|
|
|
const tankLocation = null; // same as above
|
|
|
|
|
|
|
|
|
|
// prefer customer address (your legacy sample stores a geocoded customer address)
|
|
|
|
|
const address = customerDoc?.profile.address1 ?? null;
|
|
|
|
|
const latitude = customerDoc?.latitude ?? null;
|
|
|
|
|
const longitude = customerDoc?.longitude ?? null;
|
|
|
|
|
const latitude = customerDoc?.latitude ?? undefined;
|
|
|
|
|
const longitude = customerDoc?.longitude ?? undefined;
|
|
|
|
|
|
|
|
|
|
// price: from supplier’s quoted_amount in this request (fallback null)
|
|
|
|
|
const price = (supplierEntry?.quoted_amount ?? null) !== null
|
|
|
|
|
const price = (supplierEntry?.quoted_amount ?? null) !== null
|
|
|
|
|
? String(supplierEntry.quoted_amount)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
// numeric fields
|
|
|
|
|
const numericCapacity = parseNumber(booking.capacity);
|
|
|
|
|
const numericQuantity = parseNumber(booking.quantity);
|
|
|
|
|
const totalRequired = Number.isFinite(booking.total_required_capacity)
|
|
|
|
|
const totalRequired = Number.isFinite(booking.total_required_capacity)
|
|
|
|
|
? booking.total_required_capacity
|
|
|
|
|
: numericCapacity * numericQuantity;
|
|
|
|
|
|
|
|
|
|
// dedupe check
|
|
|
|
|
// dedupe by (customerId, supplierId, date, time)
|
|
|
|
|
const existing = await Tankerbooking.find(
|
|
|
|
|
{
|
|
|
|
|
customerId: booking.customerId,
|
|
|
|
|
supplierId: supplierId,
|
|
|
|
|
date: { $in: allDates },
|
|
|
|
|
time: booking.time, // keep your stored time format intact
|
|
|
|
|
},
|
|
|
|
|
{ customerId: booking.customerId, supplierId, date: { $in: allDates }, time: booking.time },
|
|
|
|
|
{ date: 1 }
|
|
|
|
|
).lean();
|
|
|
|
|
|
|
|
|
|
const existingSet = new Set((existing || []).map(e => e.date));
|
|
|
|
|
const newDates = allDates.filter(d => !existingSet.has(d));
|
|
|
|
|
if (!newDates.length) {
|
|
|
|
@ -425,59 +751,61 @@ exports.respondToRecurringRequestedBooking = async (req, reply) => {
|
|
|
|
|
data: { created: 0, skippedExistingDates: allDates },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- BUILD ENRICHED DOCS (matches your legacy example fields) ----------
|
|
|
|
|
const todayIso = new Date().toISOString().slice(0, 10);
|
|
|
|
|
|
|
|
|
|
const docs = newDates.map((d) => ({
|
|
|
|
|
// Required/IDs
|
|
|
|
|
const legacyTime = normalizeTimeForLegacy(booking.time);
|
|
|
|
|
// --- FORMAT THESE THREE FIELDS EXACTLY AS LEGACY EXPECTS ---
|
|
|
|
|
const nowIST = toIST(new Date());
|
|
|
|
|
const dateOfOrderFmt = fmtDDMonYYYY_HHMM(nowIST); // "DD-Mon-YYYY - HH:MM"
|
|
|
|
|
const today = new Date();
|
|
|
|
|
const datePart = today.toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
|
|
|
|
|
const randomDigit = Math.floor(Math.random() * 10); // 0–9
|
|
|
|
|
const bookingId = `ARM${datePart}${randomDigit}`;
|
|
|
|
|
// ADD: pre-generate unique booking IDs (one per date)
|
|
|
|
|
const bookingIds = await Promise.all(newDates.map(() => generateUniqueArmId()));
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const docs = newDates.map((d,i) => ({
|
|
|
|
|
// IDs
|
|
|
|
|
customerId: booking.customerId,
|
|
|
|
|
supplierId: supplierId,
|
|
|
|
|
bookingid: mkBookingId("RBK"),
|
|
|
|
|
supplierId,
|
|
|
|
|
bookingid: bookingIds[i],
|
|
|
|
|
|
|
|
|
|
// Legacy display & logistics
|
|
|
|
|
tankName, // null (fill if you link tank per-customer)
|
|
|
|
|
tankLocation, // null (fill if available)
|
|
|
|
|
tankerName, // from Supplier if present
|
|
|
|
|
// Legacy fields (match your old document)
|
|
|
|
|
tankName: null,
|
|
|
|
|
tankLocation: null,
|
|
|
|
|
tankerName,
|
|
|
|
|
|
|
|
|
|
// Dates/times (kept both ISO & legacy formats as you showed)
|
|
|
|
|
dateOfOrder: todayIso, // "2025-09-10"
|
|
|
|
|
expectedDateOfDelivery: formatDDMonYYYY(d), // "21-Oct-2025" style (legacy sample)
|
|
|
|
|
date: d, // keep ISO in `date` too
|
|
|
|
|
time: booking.time, // keep your request time as-is
|
|
|
|
|
dateOfOrder: dateOfOrderFmt, // e.g., "03-Sep-2025 - 13:25"
|
|
|
|
|
expectedDateOfDelivery: fmtFromISOAndTime(d, booking.time), // "DD-Mon-YYYY - HH:MM"
|
|
|
|
|
date: d, // keep ISO for backend logic
|
|
|
|
|
time: legacyTime, // keep whatever UI sent
|
|
|
|
|
|
|
|
|
|
// Water & capacity
|
|
|
|
|
type_of_water: booking.type_of_water,
|
|
|
|
|
typeofwater: booking.type_of_water, // legacy field name kept too
|
|
|
|
|
capacity: booking.capacity, // e.g., "100" or "10,000 L"
|
|
|
|
|
quantity: booking.quantity, // string
|
|
|
|
|
typeofwater: booking.type_of_water,
|
|
|
|
|
capacity: booking.capacity,
|
|
|
|
|
quantity: booking.quantity,
|
|
|
|
|
total_required_capacity: totalRequired,
|
|
|
|
|
|
|
|
|
|
// Money / status
|
|
|
|
|
price: price, // from quoted_amount (string) or null
|
|
|
|
|
price,
|
|
|
|
|
payment_status: "due",
|
|
|
|
|
orderStatus: "accepted",
|
|
|
|
|
|
|
|
|
|
// Contacts & names
|
|
|
|
|
address: address, // from customer
|
|
|
|
|
customerPhone: customerPhone,
|
|
|
|
|
supplierPhone: supplierPhone,
|
|
|
|
|
customerName: customerName,
|
|
|
|
|
supplierName: supplierName,
|
|
|
|
|
address,
|
|
|
|
|
customerPhone,
|
|
|
|
|
supplierPhone,
|
|
|
|
|
customerName,
|
|
|
|
|
supplierName,
|
|
|
|
|
|
|
|
|
|
// Delivery defaults (match your legacy doc)
|
|
|
|
|
delivery_agent: "null",
|
|
|
|
|
delivery_agent_mobile: "null",
|
|
|
|
|
delivery_agent_alternative_mobile: "null",
|
|
|
|
|
|
|
|
|
|
// Metering defaults
|
|
|
|
|
initial_water_level: "null",
|
|
|
|
|
final_water_level: "null",
|
|
|
|
|
start_time: "null",
|
|
|
|
|
stop_time: "null",
|
|
|
|
|
quantityDelivered: null,
|
|
|
|
|
|
|
|
|
|
// Accounting defaults
|
|
|
|
|
amount_paid: null,
|
|
|
|
|
amount_due: null,
|
|
|
|
|
distrubance_price: "none",
|
|
|
|
@ -485,20 +813,17 @@ exports.respondToRecurringRequestedBooking = async (req, reply) => {
|
|
|
|
|
payment_mode: null,
|
|
|
|
|
remarks: null,
|
|
|
|
|
|
|
|
|
|
// Device/geo defaults
|
|
|
|
|
tankerRunningStatus: "0",
|
|
|
|
|
latitude: latitude ?? undefined, // keep same field names as your legacy doc
|
|
|
|
|
longitude: longitude ?? undefined, // if not available, omit field
|
|
|
|
|
latitude,
|
|
|
|
|
longitude,
|
|
|
|
|
|
|
|
|
|
// Misc you already store
|
|
|
|
|
frequency: booking.frequency,
|
|
|
|
|
weekly_count: booking.weekly_count ?? 1,
|
|
|
|
|
deliveredDate: null,
|
|
|
|
|
deliveredDate: null, // new bookings: not delivered yet
|
|
|
|
|
distrubance_status: "0",
|
|
|
|
|
}));
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Insert without transactions, tolerate duplicates if unique index exists
|
|
|
|
|
// insert (no transactions, ordered:false)
|
|
|
|
|
let insertedCount = 0;
|
|
|
|
|
let duplicateErrors = 0;
|
|
|
|
|
try {
|
|
|
|
@ -527,6 +852,3 @@ exports.respondToRecurringRequestedBooking = async (req, reply) => {
|
|
|
|
|
throw boom.internal("Failed to update recurring supplier response", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|