Add shipment management functionality with new route handlers for shipping, receiving, and canceling shipments. Updated service methods to handle state validation and order item updates accordingly.
This commit is contained in:
parent
7541612d67
commit
a7bda46acf
@ -13,19 +13,15 @@ import {
|
|||||||
listShipmentsByPropertiesRouteHandler,
|
listShipmentsByPropertiesRouteHandler,
|
||||||
getShipmentStatsRouteHandler,
|
getShipmentStatsRouteHandler,
|
||||||
getShipmentHistoryRouteHandler,
|
getShipmentHistoryRouteHandler,
|
||||||
|
shipShipmentRouteHandler,
|
||||||
|
receiveShipmentRouteHandler,
|
||||||
|
cancelShipmentRouteHandler,
|
||||||
} from '../../services/inventory/shipments.js';
|
} from '../../services/inventory/shipments.js';
|
||||||
|
|
||||||
// list of shipments
|
// list of shipments
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = [
|
const allowedFilters = ['orderType', 'order', 'state', 'courierService', 'order._id', 'taxRate'];
|
||||||
'vendor',
|
|
||||||
'purchaseOrder',
|
|
||||||
'state',
|
|
||||||
'courierService',
|
|
||||||
'vendor._id',
|
|
||||||
'purchaseOrder._id',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listShipmentsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
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) => {
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
const allowedFilters = [
|
const allowedFilters = [
|
||||||
'vendor',
|
'orderType',
|
||||||
'purchaseOrder',
|
'order',
|
||||||
'state.type',
|
'state.type',
|
||||||
'courierService',
|
'courierService',
|
||||||
'vendor._id',
|
'order._id',
|
||||||
'purchaseOrder._id',
|
'taxRate',
|
||||||
];
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
var masterFilter = {};
|
var masterFilter = {};
|
||||||
if (req.query.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);
|
listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
});
|
});
|
||||||
@ -79,4 +75,16 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
|
|||||||
deleteShipmentRouteHandler(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;
|
export default router;
|
||||||
|
|||||||
@ -7,13 +7,16 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
|
editObjects,
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
|
checkStates,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
const logger = log4js.getLogger('Shipments');
|
const logger = log4js.getLogger('Shipments');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
|
||||||
|
|
||||||
export const listShipmentsRouteHandler = async (
|
export const listShipmentsRouteHandler = async (
|
||||||
req,
|
req,
|
||||||
@ -35,7 +38,7 @@ export const listShipmentsRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['purchaseOrder', 'vendor', 'courierService'],
|
populate: ['order', 'courierService', 'taxRate'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -59,7 +62,7 @@ export const listShipmentsByPropertiesRouteHandler = async (
|
|||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['purchaseOrder', 'vendor', 'courierService'],
|
populate: ['courierService'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -78,7 +81,7 @@ export const getShipmentRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
id,
|
id,
|
||||||
populate: ['purchaseOrder', 'vendor', 'courierService', 'items.item', 'items.taxRate'],
|
populate: ['order', 'courierService', 'taxRate'],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Shipment not found with supplied id.`);
|
logger.warn(`Shipment not found with supplied id.`);
|
||||||
@ -96,15 +99,13 @@ export const editShipmentRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
purchaseOrder: req.body.purchaseOrder,
|
orderType: req.body.orderType,
|
||||||
vendor: req.body.vendor,
|
order: req.body.order,
|
||||||
courierService: req.body.courierService,
|
courierService: req.body.courierService,
|
||||||
trackingNumber: req.body.trackingNumber,
|
trackingNumber: req.body.trackingNumber,
|
||||||
shippedDate: req.body.shippedDate,
|
amount: req.body.amount,
|
||||||
expectedDeliveryDate: req.body.expectedDeliveryDate,
|
amountWithTax: req.body.amountWithTax,
|
||||||
actualDeliveryDate: req.body.actualDeliveryDate,
|
taxRate: req.body.taxRate,
|
||||||
state: req.body.state,
|
|
||||||
notes: req.body.notes,
|
|
||||||
};
|
};
|
||||||
// Create audit log before updating
|
// Create audit log before updating
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
@ -125,20 +126,53 @@ export const editShipmentRouteHandler = async (req, res) => {
|
|||||||
res.send(result);
|
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) => {
|
export const newShipmentRouteHandler = async (req, res) => {
|
||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
purchaseOrder: req.body.purchaseOrder,
|
orderType: req.body.orderType,
|
||||||
vendor: req.body.vendor,
|
order: req.body.order,
|
||||||
courierService: req.body.courierService,
|
courierService: req.body.courierService,
|
||||||
trackingNumber: req.body.trackingNumber,
|
trackingNumber: req.body.trackingNumber,
|
||||||
items: req.body.items,
|
amount: req.body.amount,
|
||||||
cost: req.body.cost,
|
amountWithTax: req.body.amountWithTax,
|
||||||
shippedDate: req.body.shippedDate,
|
taxRate: req.body.taxRate,
|
||||||
expectedDeliveryDate: req.body.expectedDeliveryDate,
|
shippedAt: req.body.shippedAt,
|
||||||
actualDeliveryDate: req.body.actualDeliveryDate,
|
expectedAt: req.body.expectedAt,
|
||||||
state: req.body.state,
|
deliveredAt: req.body.deliveredAt,
|
||||||
notes: req.body.notes,
|
state: { type: 'draft' },
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
@ -197,3 +231,212 @@ export const getShipmentHistoryRouteHandler = async (req, res) => {
|
|||||||
logger.trace('Shipment history:', result);
|
logger.trace('Shipment history:', result);
|
||||||
res.send(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);
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user