From d2540c55ee6da286a823c260c984c2d228f5f724 Mon Sep 17 00:00:00 2001 From: Varun Date: Fri, 29 Aug 2025 13:10:31 +0530 Subject: [PATCH] changes in plans --- src/handlers/supplierHandler.js | 360 ++++++++++++++++++++++++++------ src/models/supplier.js | 64 ++++-- 2 files changed, 341 insertions(+), 83 deletions(-) diff --git a/src/handlers/supplierHandler.js b/src/handlers/supplierHandler.js index 18c240ec..647d32f6 100644 --- a/src/handlers/supplierHandler.js +++ b/src/handlers/supplierHandler.js @@ -2135,88 +2135,313 @@ exports.getSuppliersForPlanSearch = async (req, reply) => { // controllers/validationHandler.js (add below the previous handler) -exports.createRequestedPlanBooking = async (req, reply) => { - const { - customerId, - type_of_water, - capacity, - quantity, - start_date, - end_date, - time, - frequency, // "daily" | "weekly_once" | "weekly_twice" | "weekly_thrice" | "weekly" - weekly_count, // used only if frequency === "weekly" - requested_suppliers - } = req.body; +// exports.createRequestedPlanBooking = async (req, reply) => { +// const { +// customerId, +// type_of_water, +// capacity, +// quantity, +// start_date, +// end_date, +// time, +// frequency, // "daily" | "weekly_once" | "weekly_twice" | "weekly_thrice" | "weekly" +// weekly_count, // used only if frequency === "weekly" +// requested_suppliers +// } = req.body; + +// // helpers inside function (as you prefer) +// const parseCapacity = (v) => parseFloat((v || "0").toString().replace(/,/g, "")) || 0; +// const parseIntSafe = (v) => parseInt((v || "0").toString().replace(/,/g, ""), 10) || 0; +// const toISODate = (d) => d.toISOString().slice(0, 10); +// const mkUTCDate = (yyyy_mm_dd) => { +// const [y, m, d] = (yyyy_mm_dd || "").split("-").map(Number); +// return new Date(Date.UTC(y, (m || 1) - 1, d || 1)); +// }; +// const normalizeWeeklyCount = (freq, wc) => { +// if (freq === "weekly_once") return 1; +// if (freq === "weekly_twice") return 2; +// if (freq === "weekly_thrice") return 3; +// if (freq === "weekly") return wc || 1; +// return 1; +// }; +// const computeWeeklyDOWs = ({ anchorDow, weeklyCount }) => { +// if (weeklyCount === 1) return [anchorDow]; +// if (weeklyCount === 2) return [anchorDow, (anchorDow + 3) % 7]; +// if (weeklyCount === 3) return [anchorDow, (anchorDow + 2) % 7, (anchorDow + 4) % 7]; +// return [anchorDow]; +// }; +// const generateDates = ({ frequency, start_date, end_date, weekly_count }) => { +// const start = mkUTCDate(start_date); +// const end = mkUTCDate(end_date); +// if (isNaN(start) || isNaN(end)) throw new Error("Invalid start_date or end_date"); +// if (end < start) throw new Error("end_date must be after or equal to start_date"); + +// // ~3 months cap +// const maxMs = 92 * 24 * 60 * 60 * 1000; +// if ((end - start) > maxMs) throw new Error("Range exceeds 3 months"); + +// const out = []; +// if (frequency === "daily") { +// for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { +// out.push(toISODate(d)); +// } +// return out; +// } - // helpers inside function (as you prefer) - const parseCapacity = (v) => parseFloat((v || "0").toString().replace(/,/g, "")) || 0; - const parseIntSafe = (v) => parseInt((v || "0").toString().replace(/,/g, ""), 10) || 0; - const toISODate = (d) => d.toISOString().slice(0, 10); - const mkUTCDate = (yyyy_mm_dd) => { - const [y, m, d] = (yyyy_mm_dd || "").split("-").map(Number); - return new Date(Date.UTC(y, (m || 1) - 1, d || 1)); - }; - const normalizeWeeklyCount = (freq, wc) => { - if (freq === "weekly_once") return 1; - if (freq === "weekly_twice") return 2; - if (freq === "weekly_thrice") return 3; - if (freq === "weekly") return wc || 1; - return 1; - }; - const computeWeeklyDOWs = ({ anchorDow, weeklyCount }) => { - if (weeklyCount === 1) return [anchorDow]; - if (weeklyCount === 2) return [anchorDow, (anchorDow + 3) % 7]; - if (weeklyCount === 3) return [anchorDow, (anchorDow + 2) % 7, (anchorDow + 4) % 7]; - return [anchorDow]; - }; - const generateDates = ({ frequency, start_date, end_date, weekly_count }) => { - const start = mkUTCDate(start_date); - const end = mkUTCDate(end_date); - if (isNaN(start) || isNaN(end)) throw new Error("Invalid start_date or end_date"); - if (end < start) throw new Error("end_date must be after or equal to start_date"); - - // ~3 months cap - const maxMs = 92 * 24 * 60 * 60 * 1000; - if ((end - start) > maxMs) throw new Error("Range exceeds 3 months"); - - const out = []; - if (frequency === "daily") { - for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { - out.push(toISODate(d)); - } - return out; +// if (frequency.startsWith("weekly") || frequency === "weekly") { +// const wc = normalizeWeeklyCount(frequency, weekly_count); +// const dows = computeWeeklyDOWs({ anchorDow: start.getUTCDay(), weeklyCount: wc }); +// const set = new Set(dows); +// for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { +// if (set.has(d.getUTCDay())) out.push(toISODate(d)); +// } +// return out; +// } + +// throw new Error("Unsupported frequency"); +// }; + +// try { +// if (!customerId || !type_of_water || !capacity || !quantity || +// !start_date || !end_date || !time || !frequency || !requested_suppliers) { +// return reply.code(400).send({ +// status_code: 400, +// message: "Missing required fields" +// }); +// } + +// const cap = parseCapacity(capacity); +// const qty = parseIntSafe(quantity); +// const total_required_capacity = cap * qty; + +// const dates = generateDates({ frequency, start_date, end_date, weekly_count }); +// if (dates.length === 0) { +// return reply.code(400).send({ status_code: 400, message: "No dates generated for given inputs" }); +// } + +// const doc = new RecurringRequestedBooking({ +// customerId, +// type_of_water, +// capacity, +// quantity, +// total_required_capacity, +// frequency, +// weekly_count: normalizeWeeklyCount(frequency, weekly_count), +// start_date, +// end_date, +// time, +// dates, +// requested_suppliers, +// status: "pending" +// }); + +// await doc.save(); + +// return reply.send({ +// status_code: 200, +// message: "Plan requested booking created successfully", +// count: dates.length, +// dates, +// data: doc +// }); +// } catch (err) { +// console.error(err); +// return reply.code(500).send({ +// status_code: 500, +// message: "Something went wrong while saving", +// error: err.message +// }); +// } +// }; +// controllers/plan.controller.js +//const RecurringRequestedBooking = require("../models/RecurringRequestedBooking"); + +// ---------- Helpers ---------- +const MONTHS = { + jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, + jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11, +}; + +const parseCapacity = (v) => parseFloat((v ?? "0").toString().replace(/,/g, "")) || 0; +const parseIntSafe = (v) => parseInt((v ?? "0").toString().replace(/,/g, ""), 10) || 0; +const toISODate = (d) => d.toISOString().slice(0, 10); + +/** + * Accepts: + * - "YYYY-MM-DD" + * - "DD-MMM-YYYY" + * - "DD-MMM-YYYY - HH:mm" (time portion ignored for date gen) + * Returns a Date in UTC midnight of that calendar day. + */ +const mkUTCDate = (input) => { + if (!input || typeof input !== "string") throw new Error("Invalid date string"); + + const s = input.trim(); + + // ISO: YYYY-MM-DD (optionally with time, but we only take the first three parts) + const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/); + if (iso) { + const y = Number(iso[1]); + const m = Number(iso[2]) - 1; + const d = Number(iso[3]); + return new Date(Date.UTC(y, m, d)); + } + + // D-MMM-YYYY (optional " - HH:mm") + const mmm = s.match(/^(\d{1,2})-([A-Za-z]{3})-(\d{4})(?:\s*-\s*(\d{1,2}):(\d{2}))?$/); + if (mmm) { + const d = Number(mmm[1]); + const mon = MONTHS[mmm[2].toLowerCase()]; + const y = Number(mmm[3]); + if (mon == null) throw new Error("Invalid month abbreviation in date"); + return new Date(Date.UTC(y, mon, d)); + } + + throw new Error("Unsupported date format. Use YYYY-MM-DD or DD-MMM-YYYY (- HH:mm)."); +}; + +const normalizeWeeklyCount = (freq, wc) => { + if (freq === "weekly_once") return 1; + if (freq === "weekly_twice") return 2; + if (freq === "weekly_thrice") return 3; + if (freq === "weekly") return wc || 1; + return 1; +}; + +const computeWeeklyDOWs = ({ anchorDow, weeklyCount }) => { + if (weeklyCount === 1) return [anchorDow]; + if (weeklyCount === 2) return [anchorDow, (anchorDow + 3) % 7]; + if (weeklyCount === 3) return [anchorDow, (anchorDow + 2) % 7, (anchorDow + 4) % 7]; + return [anchorDow]; +}; + +const generateDates = ({ frequency, start_date, end_date, weekly_count }) => { + const start = mkUTCDate(start_date); + const end = mkUTCDate(end_date); + + if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { + throw new Error("Invalid start_date or end_date"); + } + if (end < start) throw new Error("end_date must be after or equal to start_date"); + + // ~3 months cap + const maxMs = 92 * 24 * 60 * 60 * 1000; + if ((end - start) > maxMs) throw new Error("Range exceeds 3 months"); + + const out = []; + + if (frequency === "daily") { + for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { + out.push(toISODate(d)); } + return out; + } - if (frequency.startsWith("weekly") || frequency === "weekly") { - const wc = normalizeWeeklyCount(frequency, weekly_count); - const dows = computeWeeklyDOWs({ anchorDow: start.getUTCDay(), weeklyCount: wc }); - const set = new Set(dows); - for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { - if (set.has(d.getUTCDay())) out.push(toISODate(d)); - } - return out; + if (frequency.startsWith("weekly") || frequency === "weekly") { + const wc = normalizeWeeklyCount(frequency, weekly_count); + const dows = computeWeeklyDOWs({ anchorDow: start.getUTCDay(), weeklyCount: wc }); + const set = new Set(dows); + for (let d = new Date(start); d <= end; d.setUTCDate(d.getUTCDate() + 1)) { + if (set.has(d.getUTCDay())) out.push(toISODate(d)); } + return out; + } - throw new Error("Unsupported frequency"); - }; + throw new Error("Unsupported frequency"); +}; +const ensureRequestedSuppliers = (arr) => { + const inArr = Array.isArray(arr) ? arr : []; + return inArr.map((x) => ({ + supplierId: x?.supplierId ?? "", + quoted_amount: typeof x?.quoted_amount === "number" ? x.quoted_amount : 0, + time: x?.time ?? null, + status: x?.status ?? "pending", + })); +}; + +// ---------- Controller ---------- +exports.createRequestedPlanBooking = async (req, reply) => { try { - if (!customerId || !type_of_water || !capacity || !quantity || - !start_date || !end_date || !time || !frequency || !requested_suppliers) { + const { + customerId, + type_of_water, + capacity, + quantity, + start_date, + end_date, + time, + frequency, // "daily" | "weekly_once" | "weekly_twice" | "weekly_thrice" | "weekly" + weekly_count, // used only if frequency === "weekly" + requested_suppliers + } = req.body || {}; + + // Basic presence check + const missing = [ + ["customerId", customerId], + ["type_of_water", type_of_water], + ["capacity", capacity], + ["quantity", quantity], + ["start_date", start_date], + ["end_date", end_date], + ["time", time], + ["frequency", frequency], + ["requested_suppliers", requested_suppliers], + ].filter(([k, v]) => v == null || (typeof v === "string" && v.trim() === "")); + + if (missing.length) { + return reply.code(400).send({ + status_code: 400, + message: `Missing required fields: ${missing.map(([k]) => k).join(", ")}` + }); + } + + // Validate frequency early + const ALLOWED_FREQ = new Set(["daily", "weekly_once", "weekly_twice", "weekly_thrice", "weekly"]); + if (!ALLOWED_FREQ.has(frequency)) { return reply.code(400).send({ status_code: 400, - message: "Missing required fields" + message: "Invalid frequency. Allowed: daily, weekly_once, weekly_twice, weekly_thrice, weekly" }); } + // Parse numbers const cap = parseCapacity(capacity); const qty = parseIntSafe(quantity); const total_required_capacity = cap * qty; - const dates = generateDates({ frequency, start_date, end_date, weekly_count }); - if (dates.length === 0) { - return reply.code(400).send({ status_code: 400, message: "No dates generated for given inputs" }); + if (cap <= 0 || qty <= 0) { + return reply.code(400).send({ + status_code: 400, + message: "capacity and quantity must be positive numbers" + }); + } + + // Build dates + let dates; + try { + dates = generateDates({ frequency, start_date, end_date, weekly_count }); + } catch (e) { + return reply.code(400).send({ + status_code: 400, + message: e.message || "Invalid dates" + }); + } + + if (!Array.isArray(dates) || dates.length === 0) { + return reply.code(400).send({ + status_code: 400, + message: "No dates generated for the given inputs" + }); + } + + // Suppliers normalization + const suppliers = ensureRequestedSuppliers(requested_suppliers); + if (suppliers.length === 0) { + return reply.code(400).send({ + status_code: 400, + message: "requested_suppliers must contain at least one supplier" + }); } const doc = new RecurringRequestedBooking({ @@ -2231,7 +2456,7 @@ exports.createRequestedPlanBooking = async (req, reply) => { end_date, time, dates, - requested_suppliers, + requested_suppliers: suppliers, status: "pending" }); @@ -2254,3 +2479,4 @@ exports.createRequestedPlanBooking = async (req, reply) => { } }; + diff --git a/src/models/supplier.js b/src/models/supplier.js index f28208a5..38a26bd4 100644 --- a/src/models/supplier.js +++ b/src/models/supplier.js @@ -181,30 +181,62 @@ const requestedBookingSchema = new mongoose.Schema({ // models/RecurringRequestedBooking.js +// const requestedSupplier1Schema = new mongoose.Schema({ +// supplierId: String, +// quoted_amount: Number, +// time: { type: String, default: null }, +// status: { type: String, default: "pending" }, +// }, { _id: false }); + +// const recurringRequestedBookingSchema = new mongoose.Schema({ +// customerId: { type: String, required: true }, +// type_of_water: String, +// capacity: String, +// quantity: String, +// total_required_capacity: Number, +// frequency: { type: String, enum: ["daily","weekly_once","weekly_twice","weekly_thrice","weekly"], required: true }, +// weekly_count: { type: Number, enum: [1,2,3] }, +// start_date: { type: String, required: true }, +// end_date: { type: String, required: true }, +// time: String, +// dates: [String], +// requested_suppliers: [requestedSupplier1Schema], +// status: { type: String, default: "pending" }, +// }, { timestamps: true }); + + const requestedSupplier1Schema = new mongoose.Schema({ - supplierId: String, - quoted_amount: Number, - time: { type: String, default: null }, - status: { type: String, default: "pending" }, + supplierId: { type: String, required: true }, + quoted_amount: { type: Number, default: 0 }, + time: { type: String, default: null }, // keep as string to match current payloads + status: { type: String, enum: ["pending", "accepted", "rejected"], default: "pending" }, }, { _id: false }); const recurringRequestedBookingSchema = new mongoose.Schema({ customerId: { type: String, required: true }, - type_of_water: String, - capacity: String, - quantity: String, - total_required_capacity: Number, - frequency: { type: String, enum: ["daily","weekly_once","weekly_twice","weekly_thrice","weekly"], required: true }, - weekly_count: { type: Number, enum: [1,2,3] }, - start_date: { type: String, required: true }, + type_of_water: { type: String, required: true }, + + capacity: { type: String, required: true }, // keep as sent by UI, we also store parsed number below if you want + quantity: { type: String, required: true }, + total_required_capacity: { type: Number, required: true }, // capacity * quantity (numeric) + + frequency: { + type: String, + enum: ["daily", "weekly_once", "weekly_twice", "weekly_thrice", "weekly"], + required: true + }, + weekly_count: { type: Number, default: 1 }, + + start_date: { type: String, required: true }, // storing original string for audit end_date: { type: String, required: true }, - time: String, - dates: [String], - requested_suppliers: [requestedSupplier1Schema], - status: { type: String, default: "pending" }, -}, { timestamps: true }); + time: { type: String, required: true }, + + dates: { type: [String], default: [] }, // ISO "YYYY-MM-DD" strings + requested_suppliers: { type: [requestedSupplier1Schema], default: [] }, + status: { type: String, default: "pending" }, +}, { timestamps: true }); const RequestedBooking = mongoose.model('RequestedBooking', requestedBookingSchema);