diff --git a/controllers/anonOrderController.js b/controllers/anonOrderController.js index c7fb1fa1..cfd81eff 100644 --- a/controllers/anonOrderController.js +++ b/controllers/anonOrderController.js @@ -10,7 +10,7 @@ import CustomerNotification from "../models/customerNotifications.js"; export const createOrUpdateAnonOrder = async (req, res) => { try { const { - anon_order_id, + anon_order_id, chat_id, anon_user_id, em_id, @@ -31,6 +31,7 @@ export const createOrUpdateAnonOrder = async (req, res) => { final_order_items, final_amount, advance_amount_requested, + paymentBreakdowns, paymentDetails, specificTerms, order_status, @@ -49,18 +50,18 @@ export const createOrUpdateAnonOrder = async (req, res) => { message: "chat_id is required" }); } - + // Ensure chat_id is trimmed to avoid lookup failures const trimmedChatId = chat_id.trim(); // Fetch chat to get correct chat_type console.log(`[ORDER] Fetching chat details for chat_id: '${trimmedChatId}'`); const chat = await Chat.findOne({ chat_id: trimmedChatId }); - + if (!chat) { - console.log(`[ORDER] WARNING: Chat with id '${chat_id}' NOT FOUND in DB during order creation/update.`); + console.log(`[ORDER] WARNING: Chat with id '${chat_id}' NOT FOUND in DB during order creation/update.`); } else { - console.log(`[ORDER] Chat found. Database chat_type: '${chat.chat_type}'`); + console.log(`[ORDER] Chat found. Database chat_type: '${chat.chat_type}'`); } const chatType = chat ? chat.chat_type : "anon_customer-admin"; @@ -152,6 +153,7 @@ export const createOrUpdateAnonOrder = async (req, res) => { event_location: event_location !== undefined ? event_location : (existingOrder.event_location || 'Location Not Provided'), final_amount: finalAmountToStore !== undefined ? finalAmountToStore : 0, advance_amount_requested: advance_amount_requested !== undefined ? advance_amount_requested : (existingOrder.advance_amount_requested || 0), + paymentBreakdowns: paymentBreakdowns !== undefined ? paymentBreakdowns : (existingOrder.paymentBreakdowns || []), vendor_approval: true, customer_approval: true, original_ask_by_customer: customer_requirements !== undefined ? customer_requirements : (existingOrder.customer_requirements || ''), @@ -197,6 +199,7 @@ export const createOrUpdateAnonOrder = async (req, res) => { final_order_items: final_order_items !== undefined ? final_order_items : existingOrder.final_order_items, final_amount: finalAmountToStore, advance_amount_requested: advance_amount_requested !== undefined ? advance_amount_requested : existingOrder.advance_amount_requested, + paymentBreakdowns: paymentBreakdowns !== undefined ? paymentBreakdowns : existingOrder.paymentBreakdowns, paymentDetails: paymentDetailsToUse, specificTerms: specificTerms !== undefined ? specificTerms : existingOrder.specificTerms, order_status: order_status !== undefined ? order_status : existingOrder.order_status, @@ -224,7 +227,7 @@ export const createOrUpdateAnonOrder = async (req, res) => { try { let updateMessage = `Order updated! Order ID: ${order.anon_order_id}`; - + if (order_status === 'sent_to_customer') { updateMessage = 'Your order quotation has been sent! Please review the details.'; } else if (order_status === 'approved') { @@ -272,21 +275,22 @@ export const createOrUpdateAnonOrder = async (req, res) => { // Check if we should send a new order card or update an existing one? // For now, consistent with previous behavior, we send a new card on update to show latest state. - + const cardData = { - anon_order_id: order.anon_order_id, - order_status: order.order_status, - manager_name: "Event Manager", - created_at: order.updated_at || order.created_at, - event_type: order.event_type || "", - event_date: order.event_start, - event_time: order.event_time || "", - location: order.event_location || "", - guests: `${order.guest_count || 0} guests`, - services: Array.from(servicesMap.values()), - payment_breakdown: order.paymentDetails?.customerPayable || {}, - advance_amount_requested: order.advance_amount_requested || 0, - checkout_url: order.checkout_url || checkout_url || null, + anon_order_id: order.anon_order_id, + order_status: order.order_status, + manager_name: "Event Manager", + created_at: order.updated_at || order.created_at, + event_type: order.event_type || "", + event_date: order.event_start, + event_time: order.event_time || "", + location: order.event_location || "", + guests: `${order.guest_count || 0} guests`, + services: Array.from(servicesMap.values()), + payment_breakdown: order.paymentDetails?.customerPayable || {}, + paymentBreakdowns: order.paymentBreakdowns || [], + advance_amount_requested: order.advance_amount_requested || 0, + checkout_url: order.checkout_url || checkout_url || null, }; const orderCardMessage = await Message.create({ @@ -303,11 +307,11 @@ export const createOrUpdateAnonOrder = async (req, res) => { await Chat.updateOne( { chat_id, chat_type: chatType }, - { - $set: { + { + $set: { last_message_updated_at: new Date(), chat_updated_at: new Date() - } + } } ); @@ -315,38 +319,38 @@ export const createOrUpdateAnonOrder = async (req, res) => { // Re-fetch chat logic to ensures we have the absolute latest state // But 'Dual-Cast' is safer: emit to all potential rooms for this chat ID const rooms = [ - `${chat_id}-${chatType}`, // The database's current type - `${chat_id}-anon_customer-admin`, // Where the legacy/initial frontend might be - `${chat_id}-customer-admin` // Where the upgraded/known frontend might be + `${chat_id}-${chatType}`, // The database's current type + `${chat_id}-anon_customer-admin`, // Where the legacy/initial frontend might be + `${chat_id}-customer-admin` // Where the upgraded/known frontend might be ]; // Deduplicate rooms const uniqueRooms = [...new Set(rooms)]; - + console.log(`[ORDER] Emitting socket events to rooms: ${uniqueRooms.join(', ')}`); uniqueRooms.forEach(roomId => { - req.io.to(roomId).emit("new_message", { - _id: systemMessage._id, - chat_id: systemMessage.chat_id, - chat_type: systemMessage.chat_type, - sender: systemMessage.sender, - sender_id: systemMessage.sender_id, - message_content: systemMessage.message_content, - message_type: systemMessage.message_type, - message_sent_at: systemMessage.message_sent_at, - }); - - req.io.to(roomId).emit("new_message", { - _id: orderCardMessage._id, - chat_id: orderCardMessage.chat_id, - chat_type: orderCardMessage.chat_type, - sender: orderCardMessage.sender, - sender_id: orderCardMessage.sender_id, - message_content: orderCardMessage.message_content, - message_type: orderCardMessage.message_type, - card_data: orderCardMessage.card_data, - message_sent_at: orderCardMessage.message_sent_at, - }); + req.io.to(roomId).emit("new_message", { + _id: systemMessage._id, + chat_id: systemMessage.chat_id, + chat_type: systemMessage.chat_type, + sender: systemMessage.sender, + sender_id: systemMessage.sender_id, + message_content: systemMessage.message_content, + message_type: systemMessage.message_type, + message_sent_at: systemMessage.message_sent_at, + }); + + req.io.to(roomId).emit("new_message", { + _id: orderCardMessage._id, + chat_id: orderCardMessage.chat_id, + chat_type: orderCardMessage.chat_type, + sender: orderCardMessage.sender, + sender_id: orderCardMessage.sender_id, + message_content: orderCardMessage.message_content, + message_type: orderCardMessage.message_type, + card_data: orderCardMessage.card_data, + message_sent_at: orderCardMessage.message_sent_at, + }); }); } @@ -432,6 +436,7 @@ export const createOrUpdateAnonOrder = async (req, res) => { final_order_items: final_order_items || [], final_amount: final_amount || 0, advance_amount_requested: advance_amount_requested || 0, + paymentBreakdowns: paymentBreakdowns || [], paymentDetails: paymentDetails || {}, specificTerms: specificTerms || [], order_status: order_status || 'draft', @@ -456,7 +461,7 @@ export const createOrUpdateAnonOrder = async (req, res) => { const servicesMap = new Map(); if (order.final_order_items && Array.isArray(order.final_order_items)) { order.final_order_items.forEach((item) => { - if (!item) return; + if (!item) return; const serviceName = item.name_of_service?.split(" - ")[0] || item.name_of_service || "Service"; if (!servicesMap.has(serviceName)) { servicesMap.set(serviceName, { @@ -478,18 +483,19 @@ export const createOrUpdateAnonOrder = async (req, res) => { } const cardData = { - anon_order_id: order.anon_order_id, - order_status: order.order_status, - manager_name: "Event Manager", - created_at: order.created_at, - event_type: order.event_type || "", - event_date: order.event_start, - event_time: order.event_time || "", - location: order.event_location || "", - guests: `${order.guest_count || 0} guests`, - services: Array.from(servicesMap.values()), - payment_breakdown: order.paymentDetails?.customerPayable || {}, - advance_amount_requested: order.advance_amount_requested || 0, + anon_order_id: order.anon_order_id, + order_status: order.order_status, + manager_name: "Event Manager", + created_at: order.created_at, + event_type: order.event_type || "", + event_date: order.event_start, + event_time: order.event_time || "", + location: order.event_location || "", + guests: `${order.guest_count || 0} guests`, + services: Array.from(servicesMap.values()), + payment_breakdown: order.paymentDetails?.customerPayable || {}, + paymentBreakdowns: order.paymentBreakdowns || [], + advance_amount_requested: order.advance_amount_requested || 0, }; const orderCardMessage = await Message.create({ @@ -506,47 +512,47 @@ export const createOrUpdateAnonOrder = async (req, res) => { await Chat.updateOne( { chat_id, chat_type: chatType }, - { - $set: { + { + $set: { last_message_updated_at: new Date(), chat_updated_at: new Date() - } + } } ); if (req.io) { console.log(`[ORDER] Emitting socket events for chat ${chat_id}`); - + const rooms = [ - `${chat_id}-${chatType}`, - `${chat_id}-anon_customer-admin`, - `${chat_id}-customer-admin` + `${chat_id}-${chatType}`, + `${chat_id}-anon_customer-admin`, + `${chat_id}-customer-admin` ]; const uniqueRooms = [...new Set(rooms)]; - + uniqueRooms.forEach(roomId => { - req.io.to(roomId).emit("new_message", { - _id: systemMessage._id, - chat_id: systemMessage.chat_id, - chat_type: systemMessage.chat_type, - sender: systemMessage.sender, - sender_id: systemMessage.sender_id, - message_content: systemMessage.message_content, - message_type: systemMessage.message_type, - message_sent_at: systemMessage.message_sent_at, - }); - - req.io.to(roomId).emit("new_message", { - _id: orderCardMessage._id, - chat_id: orderCardMessage.chat_id, - chat_type: orderCardMessage.chat_type, - sender: orderCardMessage.sender, - sender_id: orderCardMessage.sender_id, - message_content: orderCardMessage.message_content, - message_type: orderCardMessage.message_type, - card_data: orderCardMessage.card_data, - message_sent_at: orderCardMessage.message_sent_at, - }); + req.io.to(roomId).emit("new_message", { + _id: systemMessage._id, + chat_id: systemMessage.chat_id, + chat_type: systemMessage.chat_type, + sender: systemMessage.sender, + sender_id: systemMessage.sender_id, + message_content: systemMessage.message_content, + message_type: systemMessage.message_type, + message_sent_at: systemMessage.message_sent_at, + }); + + req.io.to(roomId).emit("new_message", { + _id: orderCardMessage._id, + chat_id: orderCardMessage.chat_id, + chat_type: orderCardMessage.chat_type, + sender: orderCardMessage.sender, + sender_id: orderCardMessage.sender_id, + message_content: orderCardMessage.message_content, + message_type: orderCardMessage.message_type, + card_data: orderCardMessage.card_data, + message_sent_at: orderCardMessage.message_sent_at, + }); }); } diff --git a/controllers/bookingController.js b/controllers/bookingController.js index 44e145fb..bade999f 100644 --- a/controllers/bookingController.js +++ b/controllers/bookingController.js @@ -57,14 +57,13 @@ export const createBooking = async (req, res) => { customer_contact_email, already_paid_amount, - advance_amount_paid, payment_status, // advance_paid|fully_paid|refunded (any case) payment_method, - // Payment blocks from order paymentDetails, // { customerPayable, vendorReceivable } payment_method_details, // array of method entries + paymentBreakdowns, // array of payment break points passed from checkout // For compatibility with some callers final_order_items, // cart items array @@ -112,14 +111,10 @@ export const createBooking = async (req, res) => { const finalAmountNum = asNumber(final_amount); const alreadyPaid = asNumber(already_paid_amount, 0); - const advancePaid = asNumber(advance_amount_paid, 0); if (alreadyPaid > finalAmountNum) { return res.status(400).json({ message: "already_paid_amount cannot exceed final_amount" }); } - if (advancePaid > finalAmountNum) { - return res.status(400).json({ message: "advance_amount_paid cannot exceed final_amount" }); - } // Map paymentDetails to schema payment_details const payment_details = paymentDetails @@ -197,10 +192,10 @@ export const createBooking = async (req, res) => { customer_contact_number, customer_contact_email, already_paid_amount: alreadyPaid, - advance_amount_paid: advancePaid, payment_status: payStatusNorm || "advance_paid", payment_method, payment_details, + payment_breakdowns: paymentBreakdowns || [], // Save into Event payment_method_details: normalizedMethodDetails, final_order_items: items, }; @@ -256,9 +251,74 @@ export const createBooking = async (req, res) => { ); } - // Handle anonymous order conversion if applicable + // Resolve Quotation ID let effectiveQuotationId = quotation_id || body.quotationId; + if (!effectiveQuotationId && event_id && event_id.startsWith("ODR")) { + try { + const sourceOrder = await Order.findOne({ order_id: event_id }); + if (sourceOrder && sourceOrder.quotation_id) { + effectiveQuotationId = sourceOrder.quotation_id; + } + } catch (err) { } + } + + // Process Payment Breakdowns Status Update + try { + if (normalizedMethodDetails && normalizedMethodDetails.length > 0) { + // Last one pushed usually represents the new valid payment just made + const recentPayment = normalizedMethodDetails[normalizedMethodDetails.length - 1]; + if (recentPayment && recentPayment.payment_status === "SUCCESS") { + const breakdownNamePassed = recentPayment.payment_group; + + if (breakdownNamePassed) { + console.log(`[BOOKING] Attempting to mark payment breakdown "${breakdownNamePassed}" as Paid for ${effectiveQuotationId}`); + + const updateQuery = breakdownNamePassed === "FULL" + ? { $set: { "paymentBreakdowns.$[elem].status": "Paid" } } + : { $set: { "paymentBreakdowns.$[elem].status": "Paid" } }; + + const arrayFilters = breakdownNamePassed === "FULL" + ? [{ "elem.status": { $ne: "Paid" } }] + : [{ "elem.name": breakdownNamePassed }]; + + const updateOptions = { arrayFilters, new: true }; + + // Apply to Orders Model + let orderRes = await Order.updateMany( + { quotation_id: effectiveQuotationId }, + updateQuery, + updateOptions + ); + + // Apply to AnonCustomerOrder Model + let anonRes = await AnonCustomerOrder.updateMany( + { anon_order_id: effectiveQuotationId }, + updateQuery, + updateOptions + ); + + // Apply to Events Model + const eventUpdateQuery = breakdownNamePassed === "FULL" + ? { $set: { "payment_breakdowns.$[elem].status": "Paid" } } + : { $set: { "payment_breakdowns.$[elem].status": "Paid" } }; + + let eventRes = await Events.updateMany( + { event_id: saved.event_id || saved._id }, + eventUpdateQuery, + updateOptions + ); + + console.log(`[BOOKING] Mark Paid Result -> Orders: ${orderRes.modifiedCount}, Anon: ${anonRes.modifiedCount}, Events: ${eventRes.modifiedCount}`); + } + } + } + } catch (paymentBreakdownErr) { + console.error("[BOOKING] Failed to map paymentBreakdowns paid status:", paymentBreakdownErr); + } + + // Handle anonymous order conversion if applicable + // Fallback: If quotation_id is missing, try to find it via Order if event_id is an Order ID (ODR...) if (!effectiveQuotationId && event_id && event_id.startsWith("ODR")) { console.log(`[BOOKING] quotation_id missing. Attempting lookup via Order ID: ${event_id}`); @@ -892,4 +952,25 @@ export const cancelBooking = async (req, res) => { error: error.message }); } -}; \ No newline at end of file +}; + +// GET /api/bookings/transactions/:event_id +export const getTransactionsByEventId = async (req, res) => { + try { + const { event_id } = req.params; + if (!event_id) return res.status(400).json({ message: "event_id is required" }); + + const event = await Events.findOne({ event_id }); + if (!event) return res.status(404).json({ message: "Event not found" }); + + const { Transaction } = await import("../models/transactions.js"); + const transactions = await Transaction.find({ quotation_id: event.quotation_id }) + .sort({ createdAt: -1 }) + .lean(); + + return res.status(200).json({ transactions, event_id, quotation_id: event.quotation_id }); + } catch (error) { + console.error("Error fetching transactions:", error); + return res.status(500).json({ error: "Internal Server Error" }); + } +}; diff --git a/controllers/cashfree.js b/controllers/cashfree.js index e83ca9cf..7076eb65 100644 --- a/controllers/cashfree.js +++ b/controllers/cashfree.js @@ -7,7 +7,7 @@ import Order from "../models/orders.js"; import { Transaction } from "../models/transactions.js"; import Quotation from "../models/quotations.js"; import { sendEmailInvoice } from "./sesController.js"; -import { sendFCMNotificationToVendor,sendFCMNotificationToEm } from "../utils/firebaseNotificationUtils.js"; +import { sendFCMNotificationToVendor, sendFCMNotificationToEm } from "../utils/firebaseNotificationUtils.js"; import generateUniqueId, { generatePaymentId, generateSignature } from "../utils/generateId.js"; import { sqs } from "../config/awsConfig.js"; import { SendMessageCommand } from "@aws-sdk/client-sqs"; @@ -120,6 +120,9 @@ const createOrder = async (req, res) => { customer_id: customer_details.id, customer_phone: customer_details.phone, }, + order_tags: { + internal_order_id: internal_order_id || "MISSING", + }, }; const response = await cashfree.PGCreateOrder(request); @@ -166,6 +169,27 @@ export const verifyPayment = async (req, res) => { const vendor = await Vendor.findOne({ vendor_id: ven_id }); + try { + const pg_transfer_id = generateUniqueId("TRN_PG"); + await Transaction.create({ + quotation_id: "VENDOR_ONBOARDING", + internalOrderId: order_id, + vendor_id: ven_id, + customer_id: ven_id, + service_id: serviceData?.service_id || "ONBOARDING", + pgOrderId: order_id, + pgStatus: payment.order_status, + transfer_id: pg_transfer_id, + status: "SUCCESS", + transfer_amount: payment.order_amount, + transfer_mode: "PG_IN", + payment_type: "vendor_onboarding", + }); + console.log(`[VerifyVendorPayment] ✅ Successfully recorded PG_IN transaction for ${order_id}`); + } catch (txnErr) { + console.error(`[VerifyVendorPayment] ❌ Failed to record PG_IN transaction:`, txnErr); + } + const sqsMessage = { type: "vendorOnboarded", customer: vendor, @@ -366,6 +390,7 @@ const getServiceModelById = async (service_id) => { }; const verifyCustomerPayment = async (req, res) => { + console.log("DEBUG: verifyCustomerPayment CALLED [Version: ID_FIX_V1]"); try { const { order_id, @@ -435,17 +460,26 @@ const verifyCustomerPayment = async (req, res) => { } // Fetch the final order to get required IDs - console.log(`[VerifyPayment] Searching for internal_order_id: ${internal_order_id}`); - var finalOrder = await Order.findOne({ order_id: internal_order_id }).lean(); + let effective_internal_order_id = internal_order_id; + if (!effective_internal_order_id || effective_internal_order_id === order_id) { + if (payment.order_tags && payment.order_tags.internal_order_id && payment.order_tags.internal_order_id !== "MISSING") { + effective_internal_order_id = payment.order_tags.internal_order_id; + console.log(`[VerifyPayment] Recovered internal_order_id from order_tags: ${effective_internal_order_id}`); + } + } + + console.log(`[VerifyPayment] Searching for internal_order_id: ${effective_internal_order_id}`); + var finalOrder = await Order.findOne({ order_id: effective_internal_order_id }).lean(); if (!finalOrder) { - console.log(`[VerifyPayment] Order not found by order_id, trying quotation_id: ${internal_order_id}`); - finalOrder = await Order.findOne({ quotation_id: internal_order_id }).lean(); + console.log(`[VerifyPayment] Order not found by order_id, trying quotation_id: ${effective_internal_order_id}`); + finalOrder = await Order.findOne({ quotation_id: effective_internal_order_id }).lean(); if (!finalOrder) { - console.error(`[VerifyPayment] ⚠️ Final order not found for internal_order_id: ${internal_order_id}. BUT payment is PAID. Returning SUCCESS to frontend for recovery.`); + console.error(`[VerifyPayment] ⚠️ Final order not found for internal_order_id: ${effective_internal_order_id}. BUT payment is PAID. Returning SUCCESS to frontend for recovery.`); return res.status(200).json({ message: "Payment verified, but internal order sync pending", payment_status: payment.order_status, - order_id: internal_order_id, + order_id: effective_internal_order_id, + cf_order_id: order_id, // Add this for frontend lookup resilience no_order_record: true }); } @@ -453,7 +487,7 @@ const verifyCustomerPayment = async (req, res) => { console.log(`[VerifyPayment] Found Order: ${finalOrder.order_id}, customer_id: ${finalOrder.customer_id}`); const quotation_id = finalOrder.quotation_id; - const internalOrderId = finalOrder.order_id; + const internalOrderId = finalOrder.order_id || effective_internal_order_id; const vendor_id = finalOrder.vendor_id; const em_id = finalOrder.em_id; @@ -497,6 +531,42 @@ const verifyCustomerPayment = async (req, res) => { } } + // NEW: Record the PG Payment Transaction + try { + const pg_transfer_id = generateUniqueId("TRN_PG"); + await Transaction.create({ + quotation_id: quotation_id, + internalOrderId: internalOrderId, + vendor_id: vendor_id, + customer_id: finalCustomerId, + service_id: service_id, + pgOrderId: order_id, + pgStatus: payment.order_status, + transfer_id: pg_transfer_id, + status: "SUCCESS", + transfer_amount: order_amount, + transfer_mode: "PG_IN", + payment_type: payment_type, + paymentDetails: { + customerPayable: { + total: finalOrder?.paymentDetails?.customerPayable?.total ?? 0, + baseAmount: finalOrder?.paymentDetails?.customerPayable?.baseAmount ?? 0, + convenienceFee: finalOrder?.paymentDetails?.customerPayable?.convenienceFee ?? 0, + taxOnConvenience: finalOrder?.paymentDetails?.customerPayable?.taxOnConvenience ?? 0, + }, + vendorReceivable: { + total: finalOrder?.paymentDetails?.vendorReceivable?.total ?? 0, + baseAmount: finalOrder?.paymentDetails?.vendorReceivable?.baseAmount ?? 0, + commission: finalOrder?.paymentDetails?.vendorReceivable?.commission ?? 0, + taxOnCommission: finalOrder?.paymentDetails?.vendorReceivable?.taxOnCommission ?? 0, + }, + }, + }); + console.log(`[VerifyPayment] ✅ Successfully recorded PG_IN transaction for ${order_id}`); + } catch (txnErr) { + console.error(`[VerifyPayment] ❌ Failed to record PG_IN transaction:`, txnErr); + } + // Success Socket Emission if (req.io) { try { @@ -567,13 +637,23 @@ const verifyCustomerPayment = async (req, res) => { const alreadyPaid = previousTxn?.transfer_amount ?? 0; let payoutAmount; - if (payment_type === "advance") { - payoutAmount = order_amount; - } else if (payment_type === "full") { + + // NEW: Dynamic Category Payout Logic + // Token / Advance / Final Pay / Last Pay will be passed as `payment_type`. + // We cap ANY payout to strictly what is mathematically owed up to that point. + // Ensure we do not pay the vendor more than `receivableFromOrder - alreadyPaid`. + const maxAllowedPayout = Math.max(0, (receivableFromOrder ?? 0) - alreadyPaid); + + if (payment_type === "full") { payoutAmount = receivableFromOrder; - } else if (payment_type === "remaining") { - payoutAmount = Number(((receivableFromOrder ?? 0) - alreadyPaid).toFixed(2)); - if (payoutAmount < 0) payoutAmount = 0; + } else if (payment_type === "remaining" || payment_type === "Last Pay" || payment_type === "Final Pay") { + payoutAmount = Number(maxAllowedPayout.toFixed(2)); + } else { + // For anything else ("Advance", "Advance 1", "Token"), we payout the passed amount + // but cap it against the final allowable vendor receivable so we don't accidentally + // pay out Eventory's commission during heavy advance phases. + payoutAmount = Math.min(order_amount, maxAllowedPayout); + payoutAmount = Number(payoutAmount.toFixed(2)); } console.log(`[VerifyPayment] Payment Type: ${payment_type}, Payout Amount Calculated: ${payoutAmount}, Receivable was: ${receivableFromOrder}, Already Paid was: ${alreadyPaid}`); @@ -806,13 +886,9 @@ const verifyCustomerPayment = async (req, res) => { // Update payment details in the Order model const paymentDetailsUpdate = { paymentStatus: - payment_type === "full" + (payment_type === "full" || payment_type === "remaining" || payment_type.toLowerCase().includes("final") || payment_type.toLowerCase().includes("last")) ? "Fully Paid" - : payment_type === "advance" - ? "Partially Paid" - : payment_type === "remaining" - ? "Fully Paid" - : "Unknown", + : "Partially Paid", customerPayable: { total: finalOrder?.paymentDetails?.customerPayable?.total ?? order_amount, baseAmount: finalOrder?.paymentDetails?.customerPayable?.baseAmount ?? order_amount, @@ -834,16 +910,37 @@ const verifyCustomerPayment = async (req, res) => { { new: true } ); - const paymentTypeMap = { - advance: "Advance Payment", - remaining: "Remaining Payment", - full: "Full Payment", - }; - const paymentMode = paymentTypeMap[payment_type] || "Payment"; + // Update the matching paymentBreakdown status to "Paid" + if (payment_type && payment_type !== "full") { + await Order.findOneAndUpdate( + { + order_id: internalOrderId, + "paymentBreakdowns.name": payment_type, + "paymentBreakdowns.status": "Unpaid", + }, + { + $set: { "paymentBreakdowns.$.status": "Paid" }, + } + ); + console.log(`[VerifyPayment] Marked breakdown "${payment_type}" as Paid for order ${internalOrderId}`); + } else if (payment_type === "full") { + // Full payment: mark ALL breakdowns as Paid + await Order.updateOne( + { order_id: internalOrderId }, + { $set: { "paymentBreakdowns.$[].status": "Paid" } } + ); + console.log(`[VerifyPayment] Marked all breakdowns as Paid (full payment) for order ${internalOrderId}`); + } - const customerMessage = `${paymentMode} of ₹${order_amount} done successfully to ${vendorName} for Order ID: ${internalOrderId}`; - const vendorMessage = `${paymentMode} of ₹${order_amount} received successfully from ${customerName} for Order ID: ${internalOrderId}`; - const adminMessage = `${paymentMode} of ₹${order_amount} is done by ${customerName} to ${vendorName} for Order ID: ${internalOrderId}`; + // Try to sanitize known names for chat logs, else default back + let paymentModeLog = payment_type; + if (payment_type === "full") paymentModeLog = "Full Payment"; + else if (payment_type === "remaining") paymentModeLog = "Remaining Payment"; + else paymentModeLog = `${payment_type} Payment`; // e.g. "Token Payment", "Advance 1 Payment" + + const customerMessage = `${paymentModeLog} of ₹${order_amount} done successfully to ${vendorName} for Order ID: ${internalOrderId}`; + const vendorMessage = `${paymentModeLog} of ₹${order_amount} received successfully from ${customerName} for Order ID: ${internalOrderId}`; + const adminMessage = `${paymentModeLog} of ₹${order_amount} is done by ${customerName} to ${vendorName} for Order ID: ${internalOrderId}`; // Send real-time chat message to Customer try { @@ -944,7 +1041,7 @@ const verifyCustomerPayment = async (req, res) => { priority: "high", notification: { title: "Payment Received", - body: `${paymentMode} of ₹${order_amount} received successfully from ${customerName} for Order ID: ${internalOrderId}` + body: `${paymentModeLog} of ₹${order_amount} received successfully from ${customerName} for Order ID: ${internalOrderId}` }, data: { type: "payment", @@ -953,7 +1050,7 @@ const verifyCustomerPayment = async (req, res) => { chat_id: quotation_id, service_id: service_id, vendor_id: vendor_id, - message: `${paymentMode} of ₹${order_amount} received successfully from ${customerName} for Order ID: ${internalOrderId}` + message: `${paymentModeLog} of ₹${order_amount} received successfully from ${customerName} for Order ID: ${internalOrderId}` } }).then(result => { console.log(`FCM notifications sent to vendor ${vendor_id} for payment ${order_id}`, result); @@ -961,12 +1058,53 @@ const verifyCustomerPayment = async (req, res) => { console.error("Failed to send FCM notification for payment:", error); }); + // ── Check if an event already exists for this quotation ── let event_id; - if (payment_type !== "remaining") { + const existingEvent = await Events.findOne({ quotation_id: quotation_id }); + + if (existingEvent) { + // ── UPDATE existing event (subsequent payment) ── + event_id = existingEvent.event_id; + console.log(`[VerifyPayment] Existing event found: ${event_id}. Updating payment amounts...`); + + const currentPaid = Number(existingEvent.already_paid_amount || 0); + const newTotalPaid = Number((currentPaid + Number(order_amount)).toFixed(2)); + + const updateData = { + $set: { + already_paid_amount: newTotalPaid, + payment_status: newTotalPaid >= (existingEvent.final_amount || 0) ? "fully_paid" : "advance_paid", + }, + $push: { + payment_method_details: { + channel: payment.order_meta.payment_methods || "Online", + cf_payment_id: payment.cf_order_id || order_id, + payment_amount: Number(order_amount), + payment_completion_time: new Date(), + payment_status: "SUCCESS", + payment_group: payment_type, + } + } + }; + + // Mark the specific breakdown as Paid in the array + if (payment_type && payment_type !== "remaining" && payment_type !== "full") { + updateData.$set["payment_breakdowns.$[elem].status"] = "Paid"; + } + + const updateOptions = { + arrayFilters: [{ "elem.name": payment_type }], + new: true + }; + + await Events.findOneAndUpdate({ event_id }, updateData, updateOptions); + console.log(`[VerifyPayment] Event ${event_id} updated. New total paid: ${newTotalPaid}`); + } else { + // ── CREATE new event (first payment for this quotation) ── const now = new Date(); const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000); event_id = generateUniqueId("EVTY"); - console.log(`[VerifyPayment] Creating NEW event ${event_id} for Customer ID: ${finalCustomerId} | Order ID: ${internalOrderId}`); + console.log(`[VerifyPayment] No existing event found. Creating NEW event ${event_id} for Customer ID: ${finalCustomerId} | Order ID: ${internalOrderId}`); const preBooking = new Events({ event_id: event_id, customer_id: finalCustomerId, @@ -977,65 +1115,58 @@ const verifyCustomerPayment = async (req, res) => { // Required event fields (valid defaults) event_type: finalOrder?.event_type || "Pending", - location_type: (finalOrder?.location_type || "outdoor").toUpperCase(), // valid enum + location_type: (finalOrder?.location_type || "outdoor").toUpperCase(), event_location: finalOrder?.event_location || "Pending location", event_start: now, - event_end: oneHourLater, // strictly after start + event_end: oneHourLater, final_guest_count: Math.max(1, Number(finalOrder?.final_guest_count || 1)), final_amount: Math.max(0, Number(finalOrder?.final_amount || 0)), - event_status: "booked", // valid enum + event_status: "booked", vendor_manager_name: "Not Assigned", customer_name: customerName, - // Contacts vendor_manager_contact_number: isValidINMobile(serviceDoc?.basic_details?.service_contact_number) ? serviceDoc?.basic_details?.service_contact_number : "", vendor_manager_contact_email: vendorDoc?.email || "", customer_contact_number: customerPhone, customer_contact_email: customerEmail, - // Payment status for advance flow - already_paid_amount: payment_type === "advance" ? Number(order_amount) : 0, - advance_amount_paid: payment_type === "advance" ? Number(order_amount) : 0, - payment_status: "advance_paid", + already_paid_amount: Number(order_amount), + payment_status: (payment_type === "full" || payment_type === "remaining" || payment_type.toLowerCase().includes("final") || payment_type.toLowerCase().includes("last")) ? "fully_paid" : "advance_paid", payment_method: "online", - // Totals blocks empty for now; real values set later by createBooking payment_details: { customerPayable: { - total: 0, - baseAmount: 0, - convenienceFee: 0, - taxOnConvenience: 0, - convenienceFeeBefore: 0, - taxOnConvenienceBefore: 0, - couponCode: null, - discountAmount: 0 + total: N(finalOrder?.paymentDetails?.customerPayable?.total), + baseAmount: N(finalOrder?.paymentDetails?.customerPayable?.baseAmount), + convenienceFee: N(finalOrder?.paymentDetails?.customerPayable?.convenienceFee), + taxOnConvenience: N(finalOrder?.paymentDetails?.customerPayable?.taxOnConvenience), + convenienceFeeBefore: N(finalOrder?.paymentDetails?.customerPayable?.convenienceFeeBefore), + taxOnConvenienceBefore: N(finalOrder?.paymentDetails?.customerPayable?.taxOnConvenienceBefore), + couponCode: finalOrder?.paymentDetails?.customerPayable?.couponCode ?? null, + discountAmount: N(finalOrder?.paymentDetails?.customerPayable?.discountAmount), }, vendorReceivable: { - total: 0, - baseAmount: 0, - commission: 0, - taxOnCommission: 0 - } + total: N(finalOrder?.paymentDetails?.vendorReceivable?.total), + baseAmount: N(finalOrder?.paymentDetails?.vendorReceivable?.baseAmount), + commission: N(finalOrder?.paymentDetails?.vendorReceivable?.commission), + taxOnCommission: N(finalOrder?.paymentDetails?.vendorReceivable?.taxOnCommission), + }, }, - // Minimal items; you can also leave an empty array final_order_items: [], payment_method_details: [], + payment_breakdowns: (finalOrder?.paymentBreakdowns || []).map(b => { + const isMatching = payment_type === "full" || payment_type === "remaining" || (b.name && b.name === payment_type); + if (isMatching) { + return { ...(b.toObject ? b.toObject() : b), status: "Paid" }; + } + return b; + }), }); await preBooking.save(); - console.log(`[VerifyPayment] Event ${event_id} successfully saved.`); - } else { - console.log(`[VerifyPayment] Remaining payment detected. Finding existing event for quotation: ${quotation_id}`); - const event = await Events.findOne({ quotation_id: quotation_id }); - if (event) { - event_id = event.event_id; - console.log(`[VerifyPayment] Found existing event: ${event_id}`); - } else { - console.error(`[VerifyPayment] ERROR: Event not found for remaining payment. Quotation: ${quotation_id}`); - } + console.log(`[VerifyPayment] Event ${event_id} successfully created.`); } const paymentMethod = payment.order_meta.payment_methods !== null @@ -1050,7 +1181,7 @@ const verifyCustomerPayment = async (req, res) => { const discountAbs = typeof couponDiscount === "number" ? Number(couponDiscount) - : Number(finalOrder?.paymentDetails?.discount || 0); + : Number(finalOrder?.paymentDetails?.customerPayable?.discountAmount || finalOrder?.paymentDetails?.discount || 0); const cp = finalOrder?.paymentDetails?.customerPayable || {}; const vr = finalOrder?.paymentDetails?.vendorReceivable || {}; @@ -1151,7 +1282,15 @@ const verifyCustomerPayment = async (req, res) => { finalAmount, amount: String(Number(totalCustomerPayable.toFixed(2))), // pre-discount total discount: String(Number(discountForInvoice.toFixed(2))), // absolute coupon discount - advanceAmount: String(Number((payment_type === "advance" ? order_amount : 0).toFixed(2))), + advanceAmount: "0", + alreadyPaidAmount: (() => { + // After this payment, what is the total already paid? + if (existingEvent) { + const prev = Number(existingEvent.already_paid_amount || 0); + return String(Number((prev + Number(order_amount)).toFixed(2))); + } + return String(Number(order_amount)); + })(), convinienceFee: String(Number((convenienceFee + taxOnConvenience).toFixed(2))), commissionFee: String(Number(commissionFee.toFixed(2))), couponCode: couponCodeParam, @@ -1193,7 +1332,14 @@ const verifyCustomerPayment = async (req, res) => { })); console.log("Invoice generated successfully"); - return res.status(200).json({ message: "Customer payment verified", payment, event_id }); + const updatedEvent = await Events.findOne({ event_id }); + return res.status(200).json({ + message: "Customer payment verified", + payment, + event_id, + cf_order_id: order_id, + updated_event: updatedEvent + }); } catch (error) { console.error("❌ verifyCustomerPayment UNEXPECTED ERROR:", error?.response?.data || error.message); if (error.response) { diff --git a/controllers/finalOrderController.js b/controllers/finalOrderController.js index 1b94a8ea..d38c5b1e 100644 --- a/controllers/finalOrderController.js +++ b/controllers/finalOrderController.js @@ -5,7 +5,7 @@ import vendorNotification from "../models/vendorNotifications.js"; import adminNotification from "../models/emNotifications.js"; import Chat from "../models/chats.js"; import Message from "../models/message2.js"; -import { sendFCMNotificationToVendor,sendFCMNotificationToEm } from "../utils/firebaseNotificationUtils.js"; +import { sendFCMNotificationToVendor, sendFCMNotificationToEm } from "../utils/firebaseNotificationUtils.js"; import { Customer } from "../models/customer.js"; export const createOrUpdateFinalOrder = async (req, res) => { @@ -23,6 +23,7 @@ export const createOrUpdateFinalOrder = async (req, res) => { customer_contact_number, customer_name, specificTerms, + paymentBreakdowns, ...incomingData } = req.body; @@ -101,6 +102,7 @@ export const createOrUpdateFinalOrder = async (req, res) => { updateFields.quotation_id = quotation_id; if (paymentDetails) updateFields.paymentDetails = paymentDetails; + if (paymentBreakdowns) updateFields.paymentBreakdowns = paymentBreakdowns; if (specificTerms) updateFields.specificTerms = specificTerms; if (em_id) updateFields.em_id = em_id; @@ -122,6 +124,68 @@ export const createOrUpdateFinalOrder = async (req, res) => { console.log("✅ Final order saved:", updatedOrder.order_id); + // ── SYNC ORDER EDITS TO LINKED EVENT ── + if (order_id && updatedOrder.event_id) { + try { + const eventSyncFields = {}; + if (updateFields.final_order_items) eventSyncFields.final_order_items = updateFields.final_order_items; + if (updateFields.final_amount != null) eventSyncFields.final_amount = updateFields.final_amount; + if (updateFields.paymentDetails) eventSyncFields.payment_details = updateFields.paymentDetails; + if (updateFields.specificTerms) eventSyncFields.specific_terms = updateFields.specificTerms; + if (updateFields.event_type) eventSyncFields.event_type = updateFields.event_type; + if (updateFields.event_start) eventSyncFields.event_start = updateFields.event_start; + if (updateFields.event_end) eventSyncFields.event_end = updateFields.event_end; + if (updateFields.event_location) eventSyncFields.event_location = updateFields.event_location; + if (updateFields.location_type) eventSyncFields.location_type = updateFields.location_type; + if (updateFields.final_guest_count != null) eventSyncFields.final_guest_count = updateFields.final_guest_count; + if (updateFields.customer_name) eventSyncFields.customer_name = updateFields.customer_name; + if (updateFields.customer_contact_number) eventSyncFields.customer_contact_number = updateFields.customer_contact_number; + if (updateFields.customer_contact_email) eventSyncFields.customer_contact_email = updateFields.customer_contact_email; + + // ── MERGE payment_breakdowns: preserve "Paid" statuses from event ── + if (updateFields.paymentBreakdowns) { + const existingEvent = await Events.findOne({ event_id: updatedOrder.event_id }).lean(); + const existingBreakdowns = existingEvent?.payment_breakdowns || []; + + // Build a map of existing paid statuses by breakdown name + const paidStatusMap = {}; + for (const eb of existingBreakdowns) { + if (eb.status === "Paid") { + paidStatusMap[eb.name] = true; + } + } + + + + // Merge: keep paid status for existing breakdowns, new ones stay as-is + const mergedBreakdowns = updateFields.paymentBreakdowns.map(b => ({ + ...b, + status: paidStatusMap[b.name] ? "Paid" : (b.status || "Unpaid"), + })); + + eventSyncFields.payment_breakdowns = mergedBreakdowns; + } + + + if (Object.keys(eventSyncFields).length > 0) { + const syncResult = await Events.findOneAndUpdate( + { event_id: updatedOrder.event_id }, + { $set: eventSyncFields }, + { new: true } + ); + if (syncResult) { + console.log(`[OrderSync] Event ${updatedOrder.event_id} synced with order changes.`); + } else { + console.warn(`[OrderSync] Event ${updatedOrder.event_id} not found, skipping sync.`); + } + } + } catch (syncErr) { + console.error("[OrderSync] Failed to sync order to event:", syncErr.message); + } + } + + + // ------------------- SEND REAL-TIME NOTIFICATION ------------------- try { const messageContent = `Final Order Generated: ${updatedOrder.order_id}. Please review and approve.`; @@ -605,13 +669,16 @@ export const approveFinalOrder = async (req, res) => { export const updateFinalOrder = async (req, res) => { try { const { order_id } = req.params; - const { paymentDetails, specificTerms, ...updateData } = req.body; + const { paymentDetails, specificTerms, paymentBreakdowns, ...updateData } = req.body; // Handle paymentDetails and specificTerms separately to ensure proper schema validation const updateFields = { ...updateData }; if (paymentDetails) { updateFields.paymentDetails = paymentDetails; } + if (paymentBreakdowns) { + updateFields.paymentBreakdowns = paymentBreakdowns; + } if (specificTerms) { updateFields.specificTerms = specificTerms; } diff --git a/controllers/invoiceController.js b/controllers/invoiceController.js index 193c0233..d092a594 100644 --- a/controllers/invoiceController.js +++ b/controllers/invoiceController.js @@ -6,7 +6,7 @@ import { Events } from "../models/events.js"; // Create a new invoice export const createInvoice = async (req, res) => { try { - const { invoice_url, type, vendor_id, service_id, customer_id, event_id } = + const { invoice_url, type, invoice_for, payment_label, vendor_id, service_id, customer_id, event_id } = req.body; // Validate required fields @@ -33,6 +33,14 @@ export const createInvoice = async (req, res) => { }); } + // Validate invoice_for + if (invoice_for && !['customer', 'vendor'].includes(invoice_for)) { + return res.status(400).json({ + success: false, + message: "Invalid invoice_for. Must be 'customer' or 'vendor'", + }); + } + // Validate customer_id based on type if (type === "registration" && customer_id) { return res.status(400).json({ @@ -92,6 +100,8 @@ export const createInvoice = async (req, res) => { const invoice = new Invoices({ invoice_url, type, + invoice_for: invoice_for || (type === "registration" ? "vendor" : "customer"), + payment_label: payment_label || null, vendor_id, service_id, customer_id: type === "registration" ? null : customer_id, diff --git a/invoicing-service/generateInvoice.js b/invoicing-service/generateInvoice.js index de67a7be..a55f2274 100644 --- a/invoicing-service/generateInvoice.js +++ b/invoicing-service/generateInvoice.js @@ -385,6 +385,8 @@ async function generateVendorOnboardedInvoice(customer, paymentDetails) { await axios.post(`${process.env.URL}/api/invoices`, { invoice_url: invoiceUrl, type: "registration", + invoice_for: "vendor", + payment_label: "registration", vendor_id: customer.vendor_id || customer.id, service_id: (serviceData && serviceData._id) || @@ -459,55 +461,98 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet ? "Eventory-Coupon-Code" : paymentDetails.method || "Online"; const paymentType = paymentDetails.paymentType || null; + + // Generate a filename-friendly payment label from paymentType + const paymentLabel = (() => { + if (!paymentType) return "payment"; + const t = paymentType.toLowerCase().replace(/\s+/g, ""); + if (t === "token") return "token"; + if (t === "full") return "fullpay"; + if (t === "remaining") return "remaining"; + if (t === "finalpay") return "finalpay"; + if (t === "lastpay") return "lastpay"; + if (t.startsWith("advance")) return t; // "advance1", "advance2", etc. + return t; // fallback: use normalized string + })(); const paidAmountNum = (() => { const explicit = Number(paymentDetails.paidAmount || 0); - if (paymentType === "advance") return explicit; - if (paymentType === "full") return finalAmount; - if (paymentType === "remaining") return explicit; + const ptLower = (paymentType || "").toLowerCase(); + if (ptLower === "full") return finalAmount; + // For any partial payment (Token, Advance, Advance 1, remaining, etc.) use the explicit paidAmount return explicit || finalAmount; })(); const invoiceDate = new Date().toLocaleDateString("en-GB", { timeZone: "Asia/Kolkata", }); - - // NEW: service data from SQS + // Service data from SQS const serviceData = paymentDetails.serviceData || {}; - // Prefer service pincode, then customer pincode - const isDelhiPincode = String( - serviceData?.business_details?.pincode - || serviceData?.basic_details?.service_location_make_up?.service_pincode - || customer?.pincode - || customer?.pinCode - || "" - ).startsWith("1"); + // Prefer GST-based Delhi detection (matching admin logic) + const customerGst = customer.gstin || customer.gst || ""; + const vendorGst = + serviceData?.business_details?.gst + || vendor.businessDetails?.gstin + || ""; let runningSerial = 1; let tableRows = ""; + let tableHeader = ""; + let colspan = "7"; + + // --- CUSTOMER INVOICE --- + const hasCustomerGst = !!customerGst; + const isDelhiCustomer = !hasCustomerGst || String(customerGst).startsWith("07"); + + if (isDelhiCustomer) { + tableHeader = ` +
${capitalizeWords(customer.name || "")}
${customer.address || ""}
${customer.pincode || customer.pinCode || ""}
+ ${customerGst ? `GST: ${customerGst}
` : ""} `; // NEW: prefer service business_details, fallback to vendor.businessDetails @@ -584,9 +666,6 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet const vendorPan = serviceData?.business_details?.pan || vendor.businessDetails?.panNo; - const vendorGst = - serviceData?.business_details?.gst - || vendor.businessDetails?.gstin; const vendorDetails = `${capitalizeWords(vendorBusinessName)}
@@ -596,15 +675,44 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet${vendorGst ? `GST: ${vendorGst}` : ""}
`; - const advanceDetails = - Number(paymentDetails.advanceAmount || 0) > 0 - ? `Advance: Rs ${Number(paymentDetails.advanceAmount).toFixed(2)}
` - : ""; + const advanceDetails = ""; let id = `Customer ID:
${customer.id}
`; + const signatureRow = ` +For EVENTORY TECH SOLUTIONS PVT LIMITED:
+
+ Authorized Signatory
+${vendor.id}
`; - const vendorReceivedNum = paidAmountNum; - const vendorTotalReceivableNum = paymentDetails.vendorReceivable.total; + const vendorTotalReceivableNum = Number(paymentDetails?.vendorReceivable?.total || 0); + const alreadyPaidTotalVal = Number(paymentDetails?.alreadyPaidAmount || paidAmountNum || 0); + const vendorReceivedNum = Math.min(alreadyPaidTotalVal, vendorTotalReceivableNum); + + const balanceReceivable = Math.max(0, vendorTotalReceivableNum - vendorReceivedNum); + const balanceRowVendor = balanceReceivable > 0 + ? ` +| S.No. | -Item Details | -Type | -Net Amount | -Tax Rate | -Tax Type | -Tax Amount | -Total Amount | + {{tableHeader}}
|---|---|---|---|---|---|---|---|
| - - | -
- For EVENTORY TECH SOLUTIONS PVT LIMITED: -
- Authorized Signatory - |
- ||||||