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 = ` + S.No. + Item Details + Net Amount + CGST % + CGST + SGST % + SGST + Total Amount + `; + colspan = "7"; + } else { + tableHeader = ` + S.No. + Item Details + Net Amount + IGST % + IGST + Total Amount + `; + colspan = "5"; + } + + let totalNetAmount = 0; + let totalTaxAmount = 0; + let totalGrossAmount = 0; items.forEach((item) => { const gross = Number(item.amount) || 0; const net = gross / 1.18; const tax = gross - net; - if (!isDelhiPincode) { + totalNetAmount += net; + totalTaxAmount += tax; + totalGrossAmount += gross; + + if (isDelhiCustomer) { const half = tax / 2; tableRows += ` - ${runningSerial} - ${item.name || "Item"} - ${item.type || "-"} - Rs ${net.toFixed(2)} + ${runningSerial} + ${item.name || "Item"} + Rs ${net.toFixed(2)} 9% - CGST Rs ${half.toFixed(2)} - Rs ${gross.toFixed(2)} - - 9% - SGST Rs ${half.toFixed(2)} + Rs ${gross.toFixed(2)} `; } else { @@ -515,10 +560,8 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet ${runningSerial} ${item.name || "Item"} - ${item.type || "-"} Rs ${net.toFixed(2)} 18% - IGST Rs ${tax.toFixed(2)} Rs ${gross.toFixed(2)} @@ -527,36 +570,74 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet runningSerial++; }); + // Subtotal row for customer invoices + if (isDelhiCustomer) { + tableRows += ` + + + Subtotal: + Rs ${totalNetAmount.toFixed(2)} + + Rs ${(totalTaxAmount / 2).toFixed(2)} + + Rs ${(totalTaxAmount / 2).toFixed(2)} + Rs ${totalGrossAmount.toFixed(2)} + + `; + } else { + tableRows += ` + + + Subtotal: + Rs ${totalNetAmount.toFixed(2)} + + Rs ${totalTaxAmount.toFixed(2)} + Rs ${totalGrossAmount.toFixed(2)} + + `; + } + const discountTotalsRow = discountAmount > 0 ? ` - Discount${couponCode ? ` (${couponCode})` : ""}: - - Rs ${discountAmount.toFixed(2)} + Discount${couponCode ? ` (${couponCode})` : ""}: + - Rs ${discountAmount.toFixed(2)} ` : ""; - const totalRow = ` - - Convenience Fee: - Rs ${convinienceFee.toFixed(2)} - - ${discountTotalsRow} - - Total to be paid: - Rs ${finalAmount.toFixed(2)} - - - Paid: - Rs ${paidAmountNum.toFixed(2)} - + const alreadyPaidTotal = Number(paymentDetails.alreadyPaidAmount || paidAmountNum); + const balanceDue = Math.max(0, finalAmount - alreadyPaidTotal); + const balanceRow = balanceDue > 0 + ? ` + + Balance Due: + Rs ${balanceDue.toFixed(2)} + ` + : ""; + + let totalRow = ` + + Convenience Fee: + Rs ${convinienceFee.toFixed(2)} + + ${discountTotalsRow} + + Total to be paid: + Rs ${finalAmount.toFixed(2)} + + + Paid (${(paymentType || 'Full').replace(/([a-z])(\d)/g, '$1 $2').toUpperCase()}): + Rs ${paidAmountNum.toFixed(2)} + + ${balanceRow} `; const amountInWordsRow = ` - + Amount Paid: ${formatAmountInWords(paidAmountNum)} @@ -566,6 +647,7 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet

${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:

+ Signature +

Authorized Signatory

+ + + `; + + const vendorBlock = ` +
+

Issued to:

+
+ ${vendorDetails} +
+
+ `; + + const customerBlock = ` +
+

Issued to:

+
+ ${userDetails} +
+
+ `; + html = html .replace("{{invoiceCount}}", invoiceNumber) .replace("{{paymentId}}", paymentId) @@ -612,12 +720,13 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet .replace("{{invoiceDate}}", invoiceDate) .replace("{{amount}}", `Rs ${paidAmountNum.toFixed(2)}`) .replace("{{userId}}", id) - .replace("{{userDetails}}", userDetails) - .replace("{{vendorDetails}}", vendorDetails) - .replace("{{advanceDetails}}", advanceDetails) + .replace("{{vendorBlock}}", "") + .replace("{{customerBlock}}", customerBlock) + .replace("{{tableHeader}}", tableHeader) .replace("{{tableRows}}", tableRows) .replace("{{totalRow}}", totalRow) - .replace("{{amountInWordsRow}}", amountInWordsRow); + .replace("{{amountInWordsRow}}", amountInWordsRow) + .replace("{{signatureRow}}", signatureRow); browser = await chromium.launch({ headless: true, @@ -633,17 +742,14 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet const custInvoiceUrl = await uploadToS3( pdfBuffer, - `invoices/bookings/${paymentDetails.event_id}/customers/${customer.id}/customer-booking-invoice-${paymentDetails.invoiceNumber}.pdf` + `bookings/${paymentDetails.event_id}/customers/${customer.id}/customer-${paymentLabel}.pdf` ); // Decide invoice type for this payment - const invoiceType = - paymentType === "advance" - ? "advance_booking" - : paymentType === "remaining" - ? "payment" - : "booking"; + const ptLower = (paymentType || "").toLowerCase(); + const isFullOrFinal = ptLower === "full" || ptLower === "remaining" || ptLower.includes("final") || ptLower.includes("last"); + const invoiceType = isFullOrFinal ? "booking" : "advance_booking"; const customerId = customer.id; // from customerPayload const vendorId = vendor.id; // from vendorPayload @@ -658,61 +764,53 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet await axios.post(`${process.env.URL}/api/invoices`, { invoice_url: custInvoiceUrl, type: invoiceType, // 'advance_booking' | 'booking' | 'payment' + invoice_for: 'customer', + payment_label: paymentLabel, vendor_id: vendorId, service_id: serviceId, customer_id: customerId, event_id: eventId, }); - if (customer.mobile) { - await sendCustomerEventBookingMessage( - custInvoiceUrl, - customer.mobile, - paymentDetails.date - ); - } + // if (customer.mobile) { + // await sendCustomerEventBookingMessage( + // custInvoiceUrl, + // customer.mobile, + // paymentDetails.date + // ); + // } // ---------- VENDOR INVOICE PDF ---------- tableRows = ""; runningSerial = 1; + + const hasVendorGst = !!vendorGst; + + // Always show full columns with GST breakdown for vendor invoices + colspan = "5"; + tableHeader = ` + S.No. + Item Details + Net Amount + GST % + GST + Total Amount + `; + items.forEach((item) => { const gross = Number(item.amount) || 0; const net = gross / 1.18; const tax = gross - net; - - if (!isDelhiPincode) { - const half = tax / 2; - tableRows += ` - - ${runningSerial} - ${item.name || "Item"} - ${item.type || "-"} - Rs ${net.toFixed(2)} - 9% - CGST - Rs ${half.toFixed(2)} - Rs ${gross.toFixed(2)} - - - 9% - SGST - Rs ${half.toFixed(2)} - - `; - } else { - tableRows += ` + tableRows += ` ${runningSerial} ${item.name || "Item"} - ${item.type || "-"} Rs ${net.toFixed(2)} 18% - IGST Rs ${tax.toFixed(2)} Rs ${gross.toFixed(2)} `; - } runningSerial++; }); @@ -721,27 +819,38 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet

${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 + ? ` + + To be received: + Rs ${balanceReceivable.toFixed(2)} + ` + : ""; const vendorTotalRow = ` - Vendor Commission: - Rs ${commissionFee.toFixed(2)} + Vendor Commission: + - Rs ${commissionFee.toFixed(2)} - Total Receivable: - Rs ${vendorTotalReceivableNum.toFixed(2)} + Total Receivable: + Rs ${vendorTotalReceivableNum.toFixed(2)} - Received: - Rs ${vendorReceivedNum.toFixed(2)} + Received: + Rs ${vendorReceivedNum.toFixed(2)} + ${balanceRowVendor} `; const vendorAmountInWordsRow = ` - + Amount Received: ${formatAmountInWords(vendorReceivedNum)} @@ -755,11 +864,13 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet .replace("{{invoiceDate}}", invoiceDate) .replace("{{amount}}", `Rs ${vendorReceivedNum.toFixed(2)}`) .replace("{{userId}}", id) - .replace("{{userDetails}}", userDetails) - .replace("{{vendorDetails}}", vendorDetails) + .replace("{{vendorBlock}}", vendorBlock) + .replace("{{customerBlock}}", "") + .replace("{{tableHeader}}", tableHeader) .replace("{{tableRows}}", tableRows) .replace("{{totalRow}}", vendorTotalRow) - .replace("{{amountInWordsRow}}", vendorAmountInWordsRow); + .replace("{{amountInWordsRow}}", vendorAmountInWordsRow) + .replace("{{signatureRow}}", signatureRow); browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"] }); page = await browser.newPage(); @@ -772,7 +883,7 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet const venInvoiceUrl = await uploadToS3( pdfBuffer, - `invoices/bookings/${paymentDetails.event_id}/vendors/${vendor.id}/vendor-booking-invoice-${paymentDetails.invoiceNumber}.pdf` + `bookings/${paymentDetails.event_id}/vendors/${vendor.id}/vendor-${paymentLabel}.pdf` ); @@ -788,19 +899,22 @@ export async function generateBookingPaymentInvoice(customer, vendor, paymentDet await axios.post(`${process.env.URL}/api/invoices`, { invoice_url: venInvoiceUrl, type: vendorInvoiceType, // 'advance_booking' | 'booking' | 'payment' + invoice_for: 'vendor', + payment_label: paymentLabel, vendor_id: vendorId, service_id: serviceId, customer_id: customerId, event_id: eventId, }); - if (vendor.mobile) { - await sendVendorEventBookingMessage( - venInvoiceUrl, - vendor.mobile, - paymentDetails.date, - ); - } + // if (vendor.mobile) { + // await sendVendorEventBookingMessage( + // venInvoiceUrl, + // `vendor-${invoiceNumber}.pdf`, + // vendor.mobile, + // paymentDetails.date, + // ); + // } } catch (err) { try { if (page && !page.isClosed()) await page.close(); diff --git a/invoicing-service/getInvoiceCount.js b/invoicing-service/getInvoiceCount.js index 4cb91528..39451a8e 100644 --- a/invoicing-service/getInvoiceCount.js +++ b/invoicing-service/getInvoiceCount.js @@ -1,30 +1,18 @@ -import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3"; +import axios from "axios"; import dotenv from "dotenv"; dotenv.config(); - -const s3 = new S3Client({ - region: "ap-south-1", - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY - } -}); - const getInvoiceCount = async () => { try { - const params = { - Bucket: process.env.AWS_S3_BUCKET_NAME, - Prefix: "invoices/vendors/", - }; + const url = process.env.URL || (process.env.IS_DEV === "true" ? "http://localhost:5000" : "https://eventory.in"); + const response = await axios.get(`${url}/api/invoices?limit=1`); - const command = new ListObjectsV2Command(params); - const response = await s3.send(command); - - - return response.Contents ? response.Contents.length : 0; + if (response.data && response.data.pagination) { + return response.data.pagination.total_records || 0; + } + return 0; } catch (error) { - console.error("Error getting invoice count from S3:", error); + console.error("Error getting invoice count from API:", error?.response?.data || error.message); return 0; } }; diff --git a/invoicing-service/sendtoWA.js b/invoicing-service/sendtoWA.js index 02addf4d..0c1e9ede 100644 --- a/invoicing-service/sendtoWA.js +++ b/invoicing-service/sendtoWA.js @@ -74,7 +74,7 @@ export async function sendInvoiceToWhatsApp(link, mobile, vendorName = "Vendor") } } -export async function sendVendorEventBookingMessage(invoice_link, vendor_mobile, date) { +export async function sendVendorEventBookingMessage(invoice_link, filename, vendor_mobile, date) { const WHATSAPP_API_URL = `https://graph.facebook.com/v23.0/${process.env.WA_PHONE_NUMBER_ID}/messages`; @@ -100,7 +100,7 @@ export async function sendVendorEventBookingMessage(invoice_link, vendor_mobile, { type: "header", parameters: [ - { type: "document", document: { link: invoice_link, filename: "booking.pdf" } }, + { type: "document", document: { link: invoice_link, filename: filename } }, ], }, { diff --git a/invoicing-service/templates/bookingPaymentInvoice.html b/invoicing-service/templates/bookingPaymentInvoice.html index 4dd0ad9d..5fe38588 100644 --- a/invoicing-service/templates/bookingPaymentInvoice.html +++ b/invoicing-service/templates/bookingPaymentInvoice.html @@ -30,18 +30,8 @@

INVOICE - {{invoiceCount}}


-
-

Vendor Details:

-
- {{vendorDetails}} -
-
-
-

Issued to:

-
- {{userDetails}} -
-
+ {{vendorBlock}} + {{customerBlock}}
@@ -72,31 +62,14 @@

Issued to:

- - - - - - - - + {{tableHeader}} {{tableRows}} {{totalRow}} {{amountInWordsRow}} - - - - + {{signatureRow}}
S.No.Item DetailsTypeNet AmountTax RateTax TypeTax AmountTotal Amount
- - -

For EVENTORY TECH SOLUTIONS PVT LIMITED:

- Signature -

Authorized Signatory

-
diff --git a/invoicing-service/templates/style.css b/invoicing-service/templates/style.css index 232ccac3..39494d08 100644 --- a/invoicing-service/templates/style.css +++ b/invoicing-service/templates/style.css @@ -6,13 +6,11 @@ body { } .invoice-container { - max-width: 800px; - margin: 20px auto; + width: 100%; + margin: 0; background: #fff; padding: 30px; - border: 1px solid #ddd; - border-radius: 5px; - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + box-sizing: border-box; } header { diff --git a/models/anonCustomerOrder.js b/models/anonCustomerOrder.js index 660831c5..3469c461 100644 --- a/models/anonCustomerOrder.js +++ b/models/anonCustomerOrder.js @@ -10,7 +10,7 @@ const anonOrderItemSchema = new mongoose.Schema({ required: true }, service_asset: [{ - type: String + type: String }], quantity: { type: Number, @@ -27,7 +27,7 @@ const anonOrderItemSchema = new mongoose.Schema({ }, tax_rate: { type: Number, - default: 0 + default: 0 }, tax_type: { type: String, @@ -43,6 +43,31 @@ const anonOrderItemSchema = new mongoose.Schema({ } }, { _id: false }); +const anonPaymentBreakdownsSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + amount: { + type: Number, + required: true + }, + date: { + type: Date, + required: true + }, + status: { + type: String, + enum: ["Unpaid", "Paid", "Failed"], + default: "Unpaid" + }, + custom_items: [{ + name_of_service: String, + price: Number, + description: String + }] +}, { _id: false }); + const anonPaymentDetailsSchema = new mongoose.Schema({ paymentStatus: { type: String, @@ -74,139 +99,144 @@ const anonCustomerOrderSchema = new mongoose.Schema({ unique: true, default: () => `ANON_ODR_${Date.now()}` }, - + chat_id: { type: String, required: true, }, - + anon_user_id: { type: String, required: false, index: true }, - + em_id: { type: String, required: true, default: 'EM_SYSTEM' }, - + service_id: { type: String, default: null }, - + vendor_id: { type: String, default: null }, - + vendor_name: { type: String, default: null }, - + customer_name: { type: String, default: "Anonymous Customer" }, - + customer_contact_number: { type: String, default: null }, - + customer_contact_email: { type: String, default: null }, - + event_type: { type: String, - default: null + default: null }, - + event_start: { type: Date, default: null }, - + event_end: { type: Date, default: null }, - + event_time: { type: String, default: null }, - + event_location: { type: String, default: null }, - + location_type: { type: String, enum: ['indoor', 'outdoor', null], default: null }, - + guest_count: { type: Number, default: null }, - + budget: { type: Number, default: null }, - + final_order_items: [anonOrderItemSchema], - + final_amount: { type: Number, default: 0 }, - + advance_amount_requested: { type: Number, default: 0 }, - + paymentDetails: { type: anonPaymentDetailsSchema, default: () => ({}) }, - + + paymentBreakdowns: { + type: [anonPaymentBreakdownsSchema], + default: [] + }, + specificTerms: { type: [String], default: [] }, - + order_status: { type: String, enum: ['draft', 'pending', 'sent_to_customer', 'approved', 'rejected', 'converted', 'cancelled'], default: 'draft' }, - + priority: { type: String, enum: ['low', 'medium', 'high', 'urgent'], default: 'medium' }, - + em_notes: { type: String, default: '' }, - + customer_requirements: { type: String, default: '' }, - + source_info: { utm_source: String, utm_medium: String, @@ -215,22 +245,22 @@ const anonCustomerOrderSchema = new mongoose.Schema({ landing_page: String, device_info: String }, - + converted_to_order_id: { type: String, default: null }, - + converted_at: { type: Date, default: null }, - + created_at: { type: Date, default: Date.now }, - + updated_at: { type: Date, default: Date.now diff --git a/models/events.js b/models/events.js index 2f31d915..7e632612 100644 --- a/models/events.js +++ b/models/events.js @@ -42,6 +42,32 @@ const cartItemSchema = new mongoose.Schema({ } }, { _id: false }); +// Payment Breakdowns Schema (embedded in Events) +const paymentBreakdownsSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + amount: { + type: Number, + required: true + }, + date: { + type: Date, + required: true + }, + status: { + type: String, + enum: ["Unpaid", "Paid", "Failed"], + default: "Unpaid" + }, + custom_items: [{ + name_of_service: String, + price: Number, + description: String + }] +}, { _id: false }); + // Events Schema const eventsSchema = new mongoose.Schema({ event_id: { @@ -209,12 +235,6 @@ const eventsSchema = new mongoose.Schema({ default: 0, min: 0 }, - advance_amount_paid: { - type: Number, - required: true, - default: 0, - min: 0 - }, payment_status: { type: String, required: true, @@ -242,6 +262,10 @@ const eventsSchema = new mongoose.Schema({ taxOnCommission: { type: Number, default: 0 } } }, + payment_breakdowns: { + type: [paymentBreakdownsSchema], + default: [] + }, payment_method_details: [ { payment_method: { @@ -345,10 +369,6 @@ eventsSchema.pre('save', function (next) { return next(new Error('Already paid amount cannot exceed final amount')); } - if (this.advance_amount_paid > this.final_amount) { - return next(new Error('Advance amount cannot exceed final amount')); - } - next(); }); diff --git a/models/invoices.js b/models/invoices.js index 57d72b3b..c227be04 100644 --- a/models/invoices.js +++ b/models/invoices.js @@ -42,6 +42,14 @@ const invoicesSchema = new mongoose.Schema({ required: true, enum: ['registration', 'advance_booking', 'booking', 'payment'] }, + invoice_for: { + type: String, + required: true, + enum: ['customer', 'vendor'] + }, + payment_label: { + type: String // e.g. "token", "advance1", "finalpay", "lastpay" + }, vendor_id: { type: String, required: true diff --git a/models/orders.js b/models/orders.js index 320d9310..856f5c62 100644 --- a/models/orders.js +++ b/models/orders.js @@ -56,6 +56,32 @@ const lastApprovalSchema = new Schema({ } }, { _id: false }); +// Payment Breakdowns Schema (embedded in Orders) +const paymentBreakdownsSchema = new Schema({ + name: { + type: String, + required: true + }, + amount: { + type: Number, + required: true + }, + date: { + type: Date, + required: true + }, + status: { + type: String, + enum: ["Unpaid", "Paid", "Failed"], + default: "Unpaid" + }, + custom_items: [{ + name_of_service: String, + price: Number, + description: String + }] +}, { _id: false }); + // Payment Details Schema (embedded in Orders) const paymentDetailsSchema = new Schema({ paymentStatus: { @@ -252,6 +278,10 @@ const ordersSchema = new Schema({ paymentDetails: { type: paymentDetailsSchema }, + paymentBreakdowns: { + type: [paymentBreakdownsSchema], + default: [] + }, specificTerms: { type: [String], default: [] diff --git a/routes/bookingRoutes.js b/routes/bookingRoutes.js index 6a1ddc0a..9f040e18 100644 --- a/routes/bookingRoutes.js +++ b/routes/bookingRoutes.js @@ -15,7 +15,8 @@ import { getAllVendorServiceSchedules, addBookingInvoice, updateEventPaymentDetails, - cancelBooking + cancelBooking, + getTransactionsByEventId } from "../controllers/bookingController.js"; const router = express.Router(); @@ -344,4 +345,6 @@ router.put("/:event_id/payment-details", updateEventPaymentDetails); */ router.put("/:event_id/cancel", cancelBooking); +router.get("/transactions/:event_id", getTransactionsByEventId); + export default router;