diff --git a/src/routes/inventory/shipments.js b/src/routes/inventory/shipments.js index 2acf609..f457f55 100644 --- a/src/routes/inventory/shipments.js +++ b/src/routes/inventory/shipments.js @@ -13,19 +13,15 @@ import { listShipmentsByPropertiesRouteHandler, getShipmentStatsRouteHandler, getShipmentHistoryRouteHandler, + shipShipmentRouteHandler, + receiveShipmentRouteHandler, + cancelShipmentRouteHandler, } from '../../services/inventory/shipments.js'; // list of shipments router.get('/', isAuthenticated, (req, res) => { const { page, limit, property, search, sort, order } = req.query; - const allowedFilters = [ - 'vendor', - 'purchaseOrder', - 'state', - 'courierService', - 'vendor._id', - 'purchaseOrder._id', - ]; + const allowedFilters = ['orderType', 'order', 'state', 'courierService', 'order._id', 'taxRate']; const filter = getFilter(req.query, allowedFilters); listShipmentsRouteHandler(req, res, page, limit, property, filter, search, sort, order); }); @@ -33,17 +29,17 @@ router.get('/', isAuthenticated, (req, res) => { router.get('/properties', isAuthenticated, (req, res) => { let properties = convertPropertiesString(req.query.properties); const allowedFilters = [ - 'vendor', - 'purchaseOrder', + 'orderType', + 'order', 'state.type', 'courierService', - 'vendor._id', - 'purchaseOrder._id', + 'order._id', + 'taxRate', ]; const filter = getFilter(req.query, allowedFilters, false); var masterFilter = {}; if (req.query.masterFilter) { - masterFilter = JSON.parse(req.query.masterFilter); + masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true); } listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter); }); @@ -79,4 +75,16 @@ router.delete('/:id', isAuthenticated, async (req, res) => { deleteShipmentRouteHandler(req, res); }); +router.post('/:id/ship', isAuthenticated, async (req, res) => { + shipShipmentRouteHandler(req, res); +}); + +router.post('/:id/receive', isAuthenticated, async (req, res) => { + receiveShipmentRouteHandler(req, res); +}); + +router.post('/:id/cancel', isAuthenticated, async (req, res) => { + cancelShipmentRouteHandler(req, res); +}); + export default router; diff --git a/src/services/inventory/shipments.js b/src/services/inventory/shipments.js index 09c54df..bb49a8d 100644 --- a/src/services/inventory/shipments.js +++ b/src/services/inventory/shipments.js @@ -7,13 +7,16 @@ import { 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, @@ -35,7 +38,7 @@ export const listShipmentsRouteHandler = async ( search, sort, order, - populate: ['purchaseOrder', 'vendor', 'courierService'], + populate: ['order', 'courierService', 'taxRate'], }); if (result?.error) { @@ -59,7 +62,7 @@ export const listShipmentsByPropertiesRouteHandler = async ( model: shipmentModel, properties, filter, - populate: ['purchaseOrder', 'vendor', 'courierService'], + populate: ['courierService'], masterFilter, }); @@ -78,7 +81,7 @@ export const getShipmentRouteHandler = async (req, res) => { const result = await getObject({ model: shipmentModel, id, - populate: ['purchaseOrder', 'vendor', 'courierService', 'items.item', 'items.taxRate'], + populate: ['order', 'courierService', 'taxRate'], }); if (result?.error) { logger.warn(`Shipment not found with supplied id.`); @@ -96,15 +99,13 @@ export const editShipmentRouteHandler = async (req, res) => { const updateData = { updatedAt: new Date(), - purchaseOrder: req.body.purchaseOrder, - vendor: req.body.vendor, + orderType: req.body.orderType, + order: req.body.order, courierService: req.body.courierService, trackingNumber: req.body.trackingNumber, - shippedDate: req.body.shippedDate, - expectedDeliveryDate: req.body.expectedDeliveryDate, - actualDeliveryDate: req.body.actualDeliveryDate, - state: req.body.state, - notes: req.body.notes, + amount: req.body.amount, + amountWithTax: req.body.amountWithTax, + taxRate: req.body.taxRate, }; // Create audit log before updating const result = await editObject({ @@ -125,20 +126,53 @@ export const editShipmentRouteHandler = async (req, res) => { 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(), - purchaseOrder: req.body.purchaseOrder, - vendor: req.body.vendor, + orderType: req.body.orderType, + order: req.body.order, courierService: req.body.courierService, trackingNumber: req.body.trackingNumber, - items: req.body.items, - cost: req.body.cost, - shippedDate: req.body.shippedDate, - expectedDeliveryDate: req.body.expectedDeliveryDate, - actualDeliveryDate: req.body.actualDeliveryDate, - state: req.body.state, - notes: req.body.notes, + 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, @@ -197,3 +231,212 @@ export const getShipmentHistoryRouteHandler = async (req, res) => { 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); +};