import config from '../../config.js'; import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js'; import log4js from 'log4js'; import mongoose from 'mongoose'; import { deleteObject, listObjects, getObject, editObject, editObjects, newObject, listObjectsByProperties, getModelStats, getModelHistory, checkStates, } from '../../database/database.js'; const logger = log4js.getLogger('Shipments'); logger.level = config.server.logLevel; import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js'; export const listShipmentsRouteHandler = async ( req, res, page = 1, limit = 25, property = '', filter = {}, search = '', sort = '', order = 'ascend' ) => { const result = await listObjects({ model: shipmentModel, page, limit, property, filter, search, sort, order, populate: ['order', 'courierService', 'taxRate'], }); if (result?.error) { logger.error('Error listing shipments.'); res.status(result.code).send(result); return; } logger.debug(`List of shipments (Page ${page}, Limit ${limit}). Count: ${result.length}`); res.send(result); }; export const listShipmentsByPropertiesRouteHandler = async ( req, res, properties = '', filter = {}, masterFilter = {} ) => { const result = await listObjectsByProperties({ model: shipmentModel, properties, filter, populate: ['courierService'], masterFilter, }); if (result?.error) { logger.error('Error listing shipments.'); res.status(result.code).send(result); return; } logger.debug(`List of shipments. Count: ${result.length}`); res.send(result); }; export const getShipmentRouteHandler = async (req, res) => { const id = req.params.id; const result = await getObject({ model: shipmentModel, id, populate: ['order', 'courierService', 'taxRate'], }); if (result?.error) { logger.warn(`Shipment not found with supplied id.`); return res.status(result.code).send(result); } logger.debug(`Retreived shipment with ID: ${id}`); res.send(result); }; export const editShipmentRouteHandler = async (req, res) => { // Get ID from params const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Shipment with ID: ${id}`); const updateData = { updatedAt: new Date(), orderType: req.body.orderType, order: req.body.order, courierService: req.body.courierService, trackingNumber: req.body.trackingNumber, amount: req.body.amount, amountWithTax: req.body.amountWithTax, taxRate: req.body.taxRate, }; // Create audit log before updating const result = await editObject({ model: shipmentModel, id, updateData, user: req.user, }); if (result.error) { logger.error('Error editing shipment:', result.error); res.status(result).send(result); return; } logger.debug(`Edited shipment with ID: ${id}`); res.send(result); }; export const editMultipleShipmentsRouteHandler = async (req, res) => { const updates = req.body.map((update) => ({ _id: update._id, orderType: update.orderType, order: update.order, courierService: update.courierService, trackingNumber: update.trackingNumber, amount: update.amount, amountWithTax: update.amountWithTax, taxRate: update.taxRate, })); if (!Array.isArray(updates)) { return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 }); } const result = await editObjects({ model: shipmentModel, updates, user: req.user, }); if (result.error) { logger.error('Error editing shipments:', result.error); res.status(result.code || 500).send(result); return; } logger.debug(`Edited ${updates.length} shipments`); res.send(result); }; export const newShipmentRouteHandler = async (req, res) => { const newData = { updatedAt: new Date(), orderType: req.body.orderType, order: req.body.order, courierService: req.body.courierService, trackingNumber: req.body.trackingNumber, amount: req.body.amount, amountWithTax: req.body.amountWithTax, taxRate: req.body.taxRate, shippedAt: req.body.shippedAt, expectedAt: req.body.expectedAt, deliveredAt: req.body.deliveredAt, state: { type: 'draft' }, }; const result = await newObject({ model: shipmentModel, newData, user: req.user, }); if (result.error) { logger.error('No shipment created:', result.error); return res.status(result.code).send(result); } logger.debug(`New shipment with ID: ${result._id}`); res.send(result); }; export const deleteShipmentRouteHandler = async (req, res) => { // Get ID from params const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Shipment with ID: ${id}`); const result = await deleteObject({ model: shipmentModel, id, user: req.user, }); if (result.error) { logger.error('No shipment deleted:', result.error); return res.status(result.code).send(result); } logger.debug(`Deleted shipment with ID: ${result._id}`); res.send(result); }; export const getShipmentStatsRouteHandler = async (req, res) => { const result = await getModelStats({ model: shipmentModel }); if (result?.error) { logger.error('Error fetching shipment stats:', result.error); return res.status(result.code).send(result); } logger.trace('Shipment stats:', result); res.send(result); }; export const getShipmentHistoryRouteHandler = async (req, res) => { const from = req.query.from; const to = req.query.to; const result = await getModelHistory({ model: shipmentModel, from, to }); if (result?.error) { logger.error('Error fetching shipment history:', result.error); return res.status(result.code).send(result); } logger.trace('Shipment history:', result); res.send(result); }; export const shipShipmentRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Shipment with ID: ${id}`); const checkStatesResult = await checkStates({ model: shipmentModel, id, states: ['planned'] }); if (checkStatesResult.error) { logger.error('Error checking shipment states:', checkStatesResult.error); res.status(checkStatesResult.code).send(checkStatesResult); return; } if (checkStatesResult === false) { logger.error('Shipment is not in planned state.'); res.status(400).send({ error: 'Shipment is not in planned state.', code: 400 }); return; } const orderItemsResult = await listObjects({ model: orderItemModel, filter: { shipment: id }, pagination: false, }); if (orderItemsResult.error) { logger.error('Error listing order items:', orderItemsResult.error); res.status(orderItemsResult.code).send(orderItemsResult); return; } for (const orderItem of orderItemsResult) { if (orderItem.state.type != 'ordered') { logger.error('Order item is not in ordered state.'); res.status(400).send({ error: 'Order item is not in ordered state.', code: 400 }); return; } } for (const orderItem of orderItemsResult) { await editObject({ model: orderItemModel, id: orderItem._id, user: req.user, updateData: { state: { type: 'shipped' }, receivedAt: new Date(), }, }); } const updateData = { state: { type: 'shipped' }, shippedAt: new Date(), }; const result = await editObject({ model: shipmentModel, id, updateData, user: req.user }); if (result.error) { logger.error('Error shipping shipment:', result.error); res.status(result.code).send(result); return; } logger.debug(`Shipped shipment with ID: ${id}`); res.send(result); return; }; export const receiveShipmentRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Shipment with ID: ${id}`); const checkStatesResult = await checkStates({ model: shipmentModel, id, states: ['shipped'] }); if (checkStatesResult.error) { logger.error('Error checking shipment states:', checkStatesResult.error); res.status(checkStatesResult.code).send(checkStatesResult); return; } if (checkStatesResult === false) { logger.error('Shipment is not in shipped state.'); res.status(400).send({ error: 'Shipment is not in shipped state.', code: 400 }); return; } const orderItemsResult = await listObjects({ model: orderItemModel, filter: { shipment: id }, pagination: false, }); if (orderItemsResult.error) { logger.error('Error listing order items:', orderItemsResult.error); res.status(orderItemsResult.code).send(orderItemsResult); return; } for (const orderItem of orderItemsResult) { if (orderItem.state.type != 'shipped') { logger.error('Order item is not in shipped state.'); res.status(400).send({ error: 'Order item is not in shipped state.', code: 400 }); return; } } for (const orderItem of orderItemsResult) { await editObject({ model: orderItemModel, id: orderItem._id, updateData: { state: { type: 'received' }, receivedAt: new Date(), }, user: req.user, }); } const result = await editObject({ model: shipmentModel, id, updateData: { state: { type: 'delivered' }, deliveredAt: new Date(), }, user: req.user, }); if (result.error) { logger.error('Error receiving shipment:', result.error); res.status(result.code).send(result); return; } logger.debug(`Received shipment with ID: ${id}`); res.send(result); }; export const cancelShipmentRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Shipment with ID: ${id}`); const checkStatesResult = await checkStates({ model: shipmentModel, id, states: ['planned', 'shipped'], }); if (checkStatesResult.error) { logger.error('Error checking shipment states:', checkStatesResult.error); res.status(checkStatesResult.code).send(checkStatesResult); return; } if (checkStatesResult === false) { logger.error('Shipment is not in a cancellable state.'); res.status(400).send({ error: 'Shipment is not in a cancellable state (must be planned or shipped).', code: 400, }); return; } const orderItemsResult = await listObjects({ model: orderItemModel, filter: { shipment: id }, pagination: false, }); if (orderItemsResult.error) { logger.error('Error listing order items:', orderItemsResult.error); res.status(orderItemsResult.code).send(orderItemsResult); return; } // Cancel related order items if they are in cancellable states for (const orderItem of orderItemsResult) { if (orderItem.state.type === 'draft' || orderItem.state.type === 'ordered') { await editObject({ model: orderItemModel, id: orderItem._id, updateData: { state: { type: 'cancelled' }, }, user: req.user, }); } } const updateData = { updatedAt: new Date(), state: { type: 'cancelled' }, cancelledAt: new Date(), }; const result = await editObject({ model: shipmentModel, id, updateData, user: req.user, }); if (result.error) { logger.error('Error cancelling shipment:', result.error); res.status(result.code).send(result); return; } logger.debug(`Cancelled shipment with ID: ${id}`); res.send(result); };