From a7e35c279eb1071b37c137e706120098c823b7e5 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 28 Dec 2025 02:12:03 +0000 Subject: [PATCH] Refactor invoice routes and handlers for improved clarity and functionality - Renamed and updated route handlers for sending and marking invoices to acknowledge and post invoices, respectively. - Changed filter parameters from 'customer' to 'client' and added 'order' and 'orderType' filters for better invoice management. - Enhanced invoice handling logic to include order items and shipments, improving the overall invoicing process. - Updated the population fields in invoice queries to reflect new schema relationships. - Adjusted state checks and logging for better error handling and clarity in invoice processing. --- src/routes/finance/invoices.js | 28 +-- src/services/finance/invoices.js | 298 ++++++++++++++++++++++++------- 2 files changed, 250 insertions(+), 76 deletions(-) diff --git a/src/routes/finance/invoices.js b/src/routes/finance/invoices.js index 9edd3cc..7f353a1 100644 --- a/src/routes/finance/invoices.js +++ b/src/routes/finance/invoices.js @@ -13,9 +13,9 @@ import { listInvoicesByPropertiesRouteHandler, getInvoiceStatsRouteHandler, getInvoiceHistoryRouteHandler, - sendInvoiceRouteHandler, - markInvoicePaidRouteHandler, + acknowledgeInvoiceRouteHandler, cancelInvoiceRouteHandler, + postInvoiceRouteHandler, } from '../../services/finance/invoices.js'; // list of invoices @@ -23,12 +23,13 @@ router.get('/', isAuthenticated, (req, res) => { const { page, limit, property, search, sort, order } = req.query; const allowedFilters = [ 'vendor', - 'customer', - 'invoiceType', + 'client', 'state', - 'value', 'vendor._id', - 'customer._id', + 'client._id', + 'order', + 'order._id', + 'orderType', ]; const filter = getFilter(req.query, allowedFilters); listInvoicesRouteHandler(req, res, page, limit, property, filter, search, sort, order); @@ -38,12 +39,13 @@ router.get('/properties', isAuthenticated, (req, res) => { let properties = convertPropertiesString(req.query.properties); const allowedFilters = [ 'vendor', - 'customer', - 'invoiceType', + 'client', + 'orderType', + 'order', 'state.type', 'value', 'vendor._id', - 'customer._id', + 'client._id', ]; const filter = getFilter(req.query, allowedFilters, false); var masterFilter = {}; @@ -84,12 +86,12 @@ router.delete('/:id', isAuthenticated, async (req, res) => { deleteInvoiceRouteHandler(req, res); }); -router.post('/:id/send', isAuthenticated, async (req, res) => { - sendInvoiceRouteHandler(req, res); +router.post('/:id/post', isAuthenticated, async (req, res) => { + postInvoiceRouteHandler(req, res); }); -router.post('/:id/markpaid', isAuthenticated, async (req, res) => { - markInvoicePaidRouteHandler(req, res); +router.post('/:id/acknowledge', isAuthenticated, async (req, res) => { + acknowledgeInvoiceRouteHandler(req, res); }); router.post('/:id/cancel', isAuthenticated, async (req, res) => { diff --git a/src/services/finance/invoices.js b/src/services/finance/invoices.js index 22e5897..6562cad 100644 --- a/src/services/finance/invoices.js +++ b/src/services/finance/invoices.js @@ -14,6 +14,8 @@ import { getModelHistory, checkStates, } from '../../database/database.js'; +import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js'; +import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js'; const logger = log4js.getLogger('Invoices'); logger.level = config.server.logLevel; @@ -29,7 +31,11 @@ export const listInvoicesRouteHandler = async ( sort = '', order = 'ascend' ) => { - const populateFields = ['vendor', 'customer', 'relatedOrder']; + const populateFields = [ + { path: 'to', strictPopulate: false, ref: 'client' }, + { path: 'from', strictPopulate: false, ref: 'vendor' }, + { path: 'order' }, + ]; const result = await listObjects({ model: invoiceModel, page, @@ -59,7 +65,7 @@ export const listInvoicesByPropertiesRouteHandler = async ( filter = {}, masterFilter = {} ) => { - const populateFields = ['vendor', 'customer', 'relatedOrder']; + const populateFields = ['to', 'from', 'order']; const result = await listObjectsByProperties({ model: invoiceModel, properties, @@ -80,7 +86,15 @@ export const listInvoicesByPropertiesRouteHandler = async ( export const getInvoiceRouteHandler = async (req, res) => { const id = req.params.id; - const populateFields = ['vendor', 'customer', 'relatedOrder']; + const populateFields = [ + { path: 'to', strictPopulate: false }, + { path: 'from', strictPopulate: false }, + { path: 'order' }, + { path: 'invoiceOrderItems.taxRate' }, + { path: 'invoiceShipments.taxRate' }, + { path: 'invoiceOrderItems.orderItem' }, + { path: 'invoiceShipments.shipment' }, + ]; const result = await getObject({ model: invoiceModel, id, @@ -117,12 +131,17 @@ export const editInvoiceRouteHandler = async (req, res) => { const updateData = { updatedAt: new Date(), vendor: req.body.vendor, - customer: req.body.customer, + client: req.body.client, invoiceType: req.body.invoiceType, invoiceDate: req.body.invoiceDate, - dueDate: req.body.dueDate, - relatedOrderType: req.body.relatedOrderType, - relatedOrder: req.body.relatedOrder, + dueAt: req.body.dueDate, + issuedAt: req.body.issuedAt, + orderType: req.body.orderType, + order: req.body.order, + invoiceOrderItems: req.body.invoiceOrderItems, + invoiceShipments: req.body.invoiceShipments, + from: req.body.from, + to: req.body.to, }; // Create audit log before updating const result = await editObject({ @@ -147,7 +166,7 @@ export const editMultipleInvoicesRouteHandler = async (req, res) => { const updates = req.body.map((update) => ({ _id: update._id, vendor: update.vendor, - customer: update.customer, + client: update.client, invoiceType: update.invoiceType, })); @@ -173,21 +192,90 @@ export const editMultipleInvoicesRouteHandler = async (req, res) => { }; export const newInvoiceRouteHandler = async (req, res) => { + const orderItems = await listObjects({ + model: orderItemModel, + filter: { order: req.body.order, orderType: req.body.orderType }, + }); + + const shipments = await listObjects({ + model: shipmentModel, + filter: { order: req.body.order, orderType: req.body.orderType }, + }); + + if (orderItems.error) { + logger.error('Error getting order items:', orderItems.error); + return res.status(orderItems.code).send(orderItems); + } + + if (shipments.error) { + logger.error('Error getting shipments:', shipments.error); + return res.status(shipments.code).send(shipments); + } + + const invoiceOrderItems = orderItems + .map((orderItem) => { + const invoicedAmountWithTax = orderItem.invoicedAmountWithTax || 0; + const totalAmountWithTax = orderItem.totalAmountWithTax || 0; + const invoicedAmount = orderItem.invoicedAmount || 0; + const totalAmount = orderItem.totalAmount || 0; + const quantity = (orderItem.quantity || 0) - (orderItem.invoicedQuantity || 0); + const taxRate = orderItem?.taxRate?._id; + + if (invoicedAmountWithTax >= totalAmountWithTax || invoicedAmount >= totalAmount) { + return null; + } + var finalQuantity = quantity; + if (finalQuantity <= 0) { + finalQuantity = 1; + } + return { + orderItem: orderItem._id, + taxRate: taxRate, + invoiceAmountWithTax: totalAmountWithTax - invoicedAmountWithTax, + invoiceAmount: totalAmount - invoicedAmount, + invoiceQuantity: finalQuantity, + }; + }) + .filter((item) => item !== null); + + const invoiceShipments = shipments + .map((shipment) => { + const invoicedAmount = shipment.invoicedAmount || 0; + const amountWithTax = shipment.amountWithTax || 0; + const invoicedAmountWithTax = shipment.invoicedAmountWithTax || 0; + const amount = shipment.amount || 0; + const taxRate = shipment?.taxRate || null; + + if (invoicedAmountWithTax >= amountWithTax || invoicedAmount >= amount) { + return null; + } + return { + shipment: shipment._id, + taxRate: taxRate, + invoiceAmountWithTax: amountWithTax - invoicedAmountWithTax, + invoiceAmount: amount - invoicedAmount, + }; + }) + .filter((item) => item !== null); + const newData = { updatedAt: new Date(), vendor: req.body.vendor, - customer: req.body.customer, - invoiceType: req.body.invoiceType || 'sales', - invoiceDate: req.body.invoiceDate || new Date(), - dueDate: req.body.dueDate, - relatedOrderType: req.body.relatedOrderType, - relatedOrder: req.body.relatedOrder, + client: req.body.client, + issuedAt: req.body.issuedAt || new Date(), + dueAt: req.body.dueAt || new Date(), + orderType: req.body.orderType, + order: req.body.order, totalAmount: 0, totalAmountWithTax: 0, totalTaxAmount: 0, grandTotalAmount: 0, shippingAmount: 0, shippingAmountWithTax: 0, + invoiceOrderItems: invoiceOrderItems, + invoiceShipments: invoiceShipments, + from: req.body.from, + to: req.body.to, }; const result = await newObject({ model: invoiceModel, @@ -247,7 +335,48 @@ export const getInvoiceHistoryRouteHandler = async (req, res) => { res.send(result); }; -export const sendInvoiceRouteHandler = async (req, res) => { +export const acknowledgeInvoiceRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Invoice with ID: ${id}`); + + const checkStatesResult = await checkStates({ model: invoiceModel, id, states: ['sent'] }); + + if (checkStatesResult.error) { + logger.error('Error checking invoice states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Invoice is not in sent state.'); + res.status(400).send({ error: 'Invoice is not in sent state.', code: 400 }); + return; + } + + const updateData = { + updatedAt: new Date(), + state: { type: 'acknowledged' }, + acknowledgedAt: new Date(), + }; + const result = await editObject({ + model: invoiceModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error acknowledging invoice:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Acknowledged invoice with ID: ${id}`); + res.send(result); +}; + +export const postInvoiceRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Invoice with ID: ${id}`); @@ -266,10 +395,98 @@ export const sendInvoiceRouteHandler = async (req, res) => { return; } + const invoice = await getObject({ + model: invoiceModel, + id, + }); + + const invoiceOrderItems = invoice.invoiceOrderItems; + const invoiceShipments = invoice.invoiceShipments; + + for (const invoiceOrderItem of invoiceOrderItems) { + const orderItem = await getObject({ + model: orderItemModel, + id: invoiceOrderItem.orderItem._id, + }); + if (orderItem.error) { + logger.error('Error getting order item:', orderItem.error); + return res.status(orderItem.code).send(orderItem); + } + const invoiceQuantity = invoiceOrderItem.invoiceQuantity || 0; + const invoiceAmount = invoiceOrderItem.invoiceAmount || 0; + const invoiceAmountWithTax = invoiceOrderItem.invoiceAmountWithTax || 0; + const invoicedQuantity = orderItem.invoicedQuantity || 0; + const invoicedAmount = orderItem.invoicedAmount || 0; + const invoicedAmountWithTax = orderItem.invoicedAmountWithTax || 0; + var quantity = (orderItem.invoiceQuantity || 0) + invoiceQuantity; + if (quantity <= orderItem.quantity) { + quantity = orderItem.quantity; + } + const updateData = { + updatedAt: new Date(), + invoicedQuantity: invoicedQuantity + invoiceQuantity, + invoicedAmount: invoicedAmount + invoiceAmount, + invoicedAmountWithTax: invoicedAmountWithTax + invoiceAmountWithTax, + }; + const result = await editObject({ + model: orderItemModel, + id: orderItem._id, + updateData, + user: req.user, + }); + if (result.error) { + logger.error('Error updating order item:', result.error); + return res.status(result.code).send(result); + } + logger.debug(`Updated order item with ID: ${orderItem._id}`); + } + + for (const invoiceShipment of invoiceShipments) { + const shipmentId = invoiceShipment.shipment._id || invoiceShipment.shipment; + const shipment = await getObject({ + model: shipmentModel, + id: shipmentId, + }); + if (shipment.error) { + logger.error('Error getting shipment:', shipment.error); + return res.status(shipment.code).send(shipment); + } + const invoiceAmount = invoiceShipment.invoiceAmount || 0; + const invoiceAmountWithTax = invoiceShipment.invoiceAmountWithTax || 0; + const invoicedAmount = shipment.invoicedAmount || 0; + const invoicedAmountWithTax = shipment.invoicedAmountWithTax || 0; + const updateData = { + updatedAt: new Date(), + invoicedAmount: invoicedAmount + invoiceAmount, + invoicedAmountWithTax: invoicedAmountWithTax + invoiceAmountWithTax, + }; + const result = await editObject({ + model: shipmentModel, + id: shipment._id, + updateData, + user: req.user, + }); + if (result.error) { + logger.error('Error updating shipment:', result.error); + return res.status(result.code).send(result); + } + logger.debug(`Updated shipment with ID: ${shipment._id}`); + } + + if (invoiceOrderItems.error) { + logger.error('Error getting invoice order items:', invoiceOrderItems.error); + return res.status(invoiceOrderItems.code).send(invoiceOrderItems); + } + + if (invoiceShipments.error) { + logger.error('Error getting invoice shipments:', invoiceShipments.error); + return res.status(invoiceShipments.code).send(invoiceShipments); + } + const updateData = { updatedAt: new Date(), state: { type: 'sent' }, - sentAt: new Date(), + postedAt: new Date(), }; const result = await editObject({ model: invoiceModel, @@ -279,57 +496,12 @@ export const sendInvoiceRouteHandler = async (req, res) => { }); if (result.error) { - logger.error('Error sending invoice:', result.error); + logger.error('Error posting invoice:', result.error); res.status(result.code).send(result); return; } - logger.debug(`Sent invoice with ID: ${id}`); - res.send(result); -}; - -export const markInvoicePaidRouteHandler = async (req, res) => { - const id = new mongoose.Types.ObjectId(req.params.id); - - logger.trace(`Invoice with ID: ${id}`); - - const checkStatesResult = await checkStates({ - model: invoiceModel, - id, - states: ['sent', 'partiallyPaid'], - }); - - if (checkStatesResult.error) { - logger.error('Error checking invoice states:', checkStatesResult.error); - res.status(checkStatesResult.code).send(checkStatesResult); - return; - } - - if (checkStatesResult === false) { - logger.error('Invoice is not in sent or partially paid state.'); - res.status(400).send({ error: 'Invoice is not in sent or partially paid state.', code: 400 }); - return; - } - - const updateData = { - updatedAt: new Date(), - state: { type: 'paid' }, - paidAt: new Date(), - }; - const result = await editObject({ - model: invoiceModel, - id, - updateData, - user: req.user, - }); - - if (result.error) { - logger.error('Error marking invoice as paid:', result.error); - res.status(result.code).send(result); - return; - } - - logger.debug(`Marked invoice as paid with ID: ${id}`); + logger.debug(`Posted invoice with ID: ${id}`); res.send(result); };