From 2630976f9ef9d4ed93405585e5d089ece10161d6 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 27 Dec 2025 20:47:10 +0000 Subject: [PATCH] Add client and sales order management functionality - Introduced new schemas for clients and sales orders. - Implemented route handlers for CRUD operations on clients and sales orders. - Updated the main application routes to include client and sales order routes. - Enhanced the models to support new data structures and relationships. --- src/database/schemas/models.js | 4 + src/database/schemas/sales/client.schema.js | 34 ++ .../schemas/sales/salesorder.schema.js | 107 ++++ src/index.js | 4 + src/routes/index.js | 4 + src/routes/sales/clients.js | 59 +++ src/routes/sales/salesorders.js | 84 ++++ src/services/sales/clients.js | 189 +++++++ src/services/sales/salesorders.js | 460 ++++++++++++++++++ 9 files changed, 945 insertions(+) create mode 100644 src/database/schemas/sales/client.schema.js create mode 100644 src/routes/sales/clients.js create mode 100644 src/routes/sales/salesorders.js create mode 100644 src/services/sales/clients.js create mode 100644 src/services/sales/salesorders.js diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 215fd7f..21315df 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -28,6 +28,8 @@ import { taxRateModel } from './management/taxrate.schema.js'; import { taxRecordModel } from './management/taxrecord.schema.js'; import { shipmentModel } from './inventory/shipment.schema.js'; import { invoiceModel } from './finance/invoice.schema.js'; +import { clientModel } from './sales/client.schema.js'; +import { salesOrderModel } from './sales/salesorder.schema.js'; // Map prefixes to models and id fields export const models = { @@ -102,4 +104,6 @@ export const models = { TXD: { model: taxRecordModel, idField: '_id', type: 'taxRecord', referenceField: '_reference' }, SHP: { model: shipmentModel, idField: '_id', type: 'shipment', referenceField: '_reference' }, INV: { model: invoiceModel, idField: '_id', type: 'invoice', referenceField: '_reference' }, + CLI: { model: clientModel, idField: '_id', type: 'client', referenceField: '_reference' }, + SOR: { model: salesOrderModel, idField: '_id', type: 'salesOrder', referenceField: '_reference' }, }; diff --git a/src/database/schemas/sales/client.schema.js b/src/database/schemas/sales/client.schema.js new file mode 100644 index 0000000..711ab6f --- /dev/null +++ b/src/database/schemas/sales/client.schema.js @@ -0,0 +1,34 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const addressSchema = new mongoose.Schema({ + building: { required: false, type: String }, + addressLine1: { required: false, type: String }, + addressLine2: { required: false, type: String }, + city: { required: false, type: String }, + state: { required: false, type: String }, + postcode: { required: false, type: String }, + country: { required: false, type: String }, +}); + +const clientSchema = new mongoose.Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { required: true, type: String }, + email: { required: false, type: String }, + phone: { required: false, type: String }, + country: { required: false, type: String }, + active: { required: true, type: Boolean, default: true }, + address: { required: false, type: addressSchema }, + tags: [{ required: false, type: String }], + }, + { timestamps: true } +); + +clientSchema.virtual('id').get(function () { + return this._id; +}); + +clientSchema.set('toJSON', { virtuals: true }); + +export const clientModel = mongoose.model('client', clientSchema); diff --git a/src/database/schemas/sales/salesorder.schema.js b/src/database/schemas/sales/salesorder.schema.js index e69de29..2874d69 100644 --- a/src/database/schemas/sales/salesorder.schema.js +++ b/src/database/schemas/sales/salesorder.schema.js @@ -0,0 +1,107 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +const { Schema } = mongoose; +import { aggregateRollups, aggregateRollupsHistory } from '../../database.js'; + +const salesOrderSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + totalAmount: { type: Number, required: true, default: 0 }, + totalAmountWithTax: { type: Number, required: true, default: 0 }, + shippingAmount: { type: Number, required: true, default: 0 }, + shippingAmountWithTax: { type: Number, required: true, default: 0 }, + grandTotalAmount: { type: Number, required: true, default: 0 }, + totalTaxAmount: { type: Number, required: true, default: 0 }, + timestamp: { type: Date, default: Date.now }, + client: { type: Schema.Types.ObjectId, ref: 'client', required: true }, + state: { + type: { type: String, required: true, default: 'draft' }, + }, + postedAt: { type: Date, required: false }, + confirmedAt: { type: Date, required: false }, + cancelledAt: { type: Date, required: false }, + completedAt: { type: Date, required: false }, + }, + { timestamps: true } +); + +const rollupConfigs = [ + { + name: 'draft', + filter: { 'state.type': 'draft' }, + rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }], + }, + { + name: 'sent', + filter: { 'state.type': 'sent' }, + rollups: [{ name: 'sent', property: 'state.type', operation: 'count' }], + }, + { + name: 'confirmed', + filter: { 'state.type': 'confirmed' }, + rollups: [{ name: 'confirmed', property: 'state.type', operation: 'count' }], + }, + { + name: 'partiallyShipped', + filter: { 'state.type': 'partiallyShipped' }, + rollups: [{ name: 'partiallyShipped', property: 'state.type', operation: 'count' }], + }, + { + name: 'shipped', + filter: { 'state.type': 'shipped' }, + rollups: [{ name: 'shipped', property: 'state.type', operation: 'count' }], + }, + { + name: 'partiallyDelivered', + filter: { 'state.type': 'partiallyDelivered' }, + rollups: [{ name: 'partiallyDelivered', property: 'state.type', operation: 'count' }], + }, + { + name: 'delivered', + filter: { 'state.type': 'delivered' }, + rollups: [{ name: 'delivered', property: 'state.type', operation: 'count' }], + }, + { + name: 'cancelled', + filter: { 'state.type': 'cancelled' }, + rollups: [{ name: 'cancelled', property: 'state.type', operation: 'count' }], + }, + { + name: 'completed', + filter: { 'state.type': 'completed' }, + rollups: [{ name: 'completed', property: 'state.type', operation: 'count' }], + }, +]; + +salesOrderSchema.statics.stats = async function () { + const results = await aggregateRollups({ + model: this, + rollupConfigs: rollupConfigs, + }); + + // Transform the results to match the expected format + return results; +}; + +salesOrderSchema.statics.history = async function (from, to) { + const results = await aggregateRollupsHistory({ + model: this, + startDate: from, + endDate: to, + rollupConfigs: rollupConfigs, + }); + + // Return time-series data array + return results; +}; + +// Add virtual id getter +salesOrderSchema.virtual('id').get(function () { + return this._id; +}); + +// Configure JSON serialization to include virtuals +salesOrderSchema.set('toJSON', { virtuals: true }); + +// Create and export the model +export const salesOrderModel = mongoose.model('salesOrder', salesOrderSchema); diff --git a/src/index.js b/src/index.js index 5e34145..90d8aa0 100644 --- a/src/index.js +++ b/src/index.js @@ -38,6 +38,8 @@ import { taxRateRoutes, taxRecordRoutes, invoiceRoutes, + clientRoutes, + salesOrderRoutes, } from './routes/index.js'; import path from 'path'; import * as fs from 'fs'; @@ -141,6 +143,8 @@ app.use('/courierservices', courierServiceRoutes); app.use('/taxrates', taxRateRoutes); app.use('/taxrecords', taxRecordRoutes); app.use('/invoices', invoiceRoutes); +app.use('/clients', clientRoutes); +app.use('/salesorders', salesOrderRoutes); app.use('/notes', noteRoutes); // Start the application diff --git a/src/routes/index.js b/src/routes/index.js index ed2da69..b7a65ba 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -30,6 +30,8 @@ import courierServiceRoutes from './management/courierservice.js'; import taxRateRoutes from './management/taxrates.js'; import taxRecordRoutes from './management/taxrecords.js'; import invoiceRoutes from './finance/invoices.js'; +import clientRoutes from './sales/clients.js'; +import salesOrderRoutes from './sales/salesorders.js'; import noteRoutes from './misc/notes.js'; export { @@ -66,4 +68,6 @@ export { taxRateRoutes, taxRecordRoutes, invoiceRoutes, + clientRoutes, + salesOrderRoutes, }; diff --git a/src/routes/sales/clients.js b/src/routes/sales/clients.js new file mode 100644 index 0000000..6da73bd --- /dev/null +++ b/src/routes/sales/clients.js @@ -0,0 +1,59 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { getFilter, convertPropertiesString } from '../../utils.js'; + +const router = express.Router(); +import { + listClientsRouteHandler, + getClientRouteHandler, + editClientRouteHandler, + newClientRouteHandler, + deleteClientRouteHandler, + listClientsByPropertiesRouteHandler, + getClientStatsRouteHandler, + getClientHistoryRouteHandler, +} from '../../services/sales/clients.js'; + +// list of clients +router.get('/', isAuthenticated, (req, res) => { + const { page, limit, property, search, sort, order } = req.query; + const allowedFilters = ['country', 'active', 'createdAt', 'updatedAt']; + const filter = getFilter(req.query, allowedFilters); + listClientsRouteHandler(req, res, page, limit, property, filter, search, sort, order); +}); + +router.get('/properties', isAuthenticated, (req, res) => { + let properties = convertPropertiesString(req.query.properties); + const allowedFilters = ['country', 'active', 'createdAt', 'updatedAt']; + const filter = getFilter(req.query, allowedFilters, false); + listClientsByPropertiesRouteHandler(req, res, properties, filter); +}); + +router.post('/', isAuthenticated, (req, res) => { + newClientRouteHandler(req, res); +}); + +// get client stats +router.get('/stats', isAuthenticated, (req, res) => { + getClientStatsRouteHandler(req, res); +}); + +// get clients history +router.get('/history', isAuthenticated, (req, res) => { + getClientHistoryRouteHandler(req, res); +}); + +router.get('/:id', isAuthenticated, (req, res) => { + getClientRouteHandler(req, res); +}); + +router.put('/:id', isAuthenticated, async (req, res) => { + editClientRouteHandler(req, res); +}); + +router.delete('/:id', isAuthenticated, async (req, res) => { + deleteClientRouteHandler(req, res); +}); + +export default router; + diff --git a/src/routes/sales/salesorders.js b/src/routes/sales/salesorders.js new file mode 100644 index 0000000..6080397 --- /dev/null +++ b/src/routes/sales/salesorders.js @@ -0,0 +1,84 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { getFilter, convertPropertiesString } from '../../utils.js'; + +const router = express.Router(); +import { + listSalesOrdersRouteHandler, + getSalesOrderRouteHandler, + editSalesOrderRouteHandler, + editMultipleSalesOrdersRouteHandler, + newSalesOrderRouteHandler, + deleteSalesOrderRouteHandler, + listSalesOrdersByPropertiesRouteHandler, + getSalesOrderStatsRouteHandler, + getSalesOrderHistoryRouteHandler, + postSalesOrderRouteHandler, + confirmSalesOrderRouteHandler, + cancelSalesOrderRouteHandler, +} from '../../services/sales/salesorders.js'; + +// list of sales orders +router.get('/', isAuthenticated, (req, res) => { + const { page, limit, property, search, sort, order } = req.query; + const allowedFilters = ['client', 'state', 'value', 'client._id']; + const filter = getFilter(req.query, allowedFilters); + listSalesOrdersRouteHandler(req, res, page, limit, property, filter, search, sort, order); +}); + +router.get('/properties', isAuthenticated, (req, res) => { + let properties = convertPropertiesString(req.query.properties); + const allowedFilters = ['client', 'state.type', 'value', 'client._id']; + const filter = getFilter(req.query, allowedFilters, false); + var masterFilter = {}; + if (req.query.masterFilter) { + masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true); + } + listSalesOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter); +}); + +router.post('/', isAuthenticated, (req, res) => { + newSalesOrderRouteHandler(req, res); +}); + +// get sales order stats +router.get('/stats', isAuthenticated, (req, res) => { + getSalesOrderStatsRouteHandler(req, res); +}); + +// get sales orders history +router.get('/history', isAuthenticated, (req, res) => { + getSalesOrderHistoryRouteHandler(req, res); +}); + +router.get('/:id', isAuthenticated, (req, res) => { + getSalesOrderRouteHandler(req, res); +}); + +// update multiple sales orders +router.put('/', isAuthenticated, async (req, res) => { + editMultipleSalesOrdersRouteHandler(req, res); +}); + +router.put('/:id', isAuthenticated, async (req, res) => { + editSalesOrderRouteHandler(req, res); +}); + +router.delete('/:id', isAuthenticated, async (req, res) => { + deleteSalesOrderRouteHandler(req, res); +}); + +router.post('/:id/post', isAuthenticated, async (req, res) => { + postSalesOrderRouteHandler(req, res); +}); + +router.post('/:id/confirm', isAuthenticated, async (req, res) => { + confirmSalesOrderRouteHandler(req, res); +}); + +router.post('/:id/cancel', isAuthenticated, async (req, res) => { + cancelSalesOrderRouteHandler(req, res); +}); + +export default router; + diff --git a/src/services/sales/clients.js b/src/services/sales/clients.js new file mode 100644 index 0000000..075f920 --- /dev/null +++ b/src/services/sales/clients.js @@ -0,0 +1,189 @@ +import config from '../../config.js'; +import { clientModel } from '../../database/schemas/sales/client.schema.js'; +import log4js from 'log4js'; +import mongoose from 'mongoose'; +import { + deleteObject, + listObjects, + getObject, + editObject, + newObject, + listObjectsByProperties, + getModelStats, + getModelHistory, +} from '../../database/database.js'; +const logger = log4js.getLogger('Clients'); +logger.level = config.server.logLevel; + +export const listClientsRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = '', + filter = {}, + search = '', + sort = '', + order = 'ascend' +) => { + const result = await listObjects({ + model: clientModel, + page, + limit, + property, + filter, + search, + sort, + order, + }); + + if (result?.error) { + logger.error('Error listing clients.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of clients (Page ${page}, Limit ${limit}). Count: ${result.length}.`); + res.send(result); +}; + +export const listClientsByPropertiesRouteHandler = async ( + req, + res, + properties = '', + filter = {} +) => { + const result = await listObjectsByProperties({ + model: clientModel, + properties, + filter, + }); + + if (result?.error) { + logger.error('Error listing clients.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of clients. Count: ${result.length}`); + res.send(result); +}; + +export const getClientRouteHandler = async (req, res) => { + const id = req.params.id; + const result = await getObject({ + model: clientModel, + id, + }); + if (result?.error) { + logger.warn(`Client not found with supplied id.`); + return res.status(result.code).send(result); + } + logger.debug(`Retreived client with ID: ${id}`); + res.send(result); +}; + +export const editClientRouteHandler = async (req, res) => { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Client with ID: ${id}`); + + const updateData = { + updatedAt: new Date(), + country: req.body.country, + name: req.body.name, + phone: req.body.phone, + email: req.body.email, + address: req.body.address, + active: req.body.active, + tags: req.body.tags, + }; + // Create audit log before updating + const result = await editObject({ + model: clientModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error editing client:', result.error); + res.status(result).send(result); + return; + } + + logger.debug(`Edited client with ID: ${id}`); + + res.send(result); +}; + +export const newClientRouteHandler = async (req, res) => { + const newData = { + updatedAt: new Date(), + country: req.body.country, + name: req.body.name, + phone: req.body.phone, + email: req.body.email, + address: req.body.address, + active: req.body.active, + tags: req.body.tags, + }; + const result = await newObject({ + model: clientModel, + newData, + user: req.user, + }); + if (result.error) { + logger.error('No client created:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`New client with ID: ${result._id}`); + + res.send(result); +}; + +export const deleteClientRouteHandler = async (req, res) => { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Client with ID: ${id}`); + + const result = await deleteObject({ + model: clientModel, + id, + user: req.user, + }); + if (result.error) { + logger.error('No client deleted:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`Deleted client with ID: ${result._id}`); + + res.send(result); +}; + +export const getClientStatsRouteHandler = async (req, res) => { + const result = await getModelStats({ model: clientModel }); + if (result?.error) { + logger.error('Error fetching client stats:', result.error); + return res.status(result.code).send(result); + } + logger.trace('Client stats:', result); + res.send(result); +}; + +export const getClientHistoryRouteHandler = async (req, res) => { + const from = req.query.from; + const to = req.query.to; + const result = await getModelHistory({ model: clientModel, from, to }); + if (result?.error) { + logger.error('Error fetching client history:', result.error); + return res.status(result.code).send(result); + } + logger.trace('Client history:', result); + res.send(result); +}; + diff --git a/src/services/sales/salesorders.js b/src/services/sales/salesorders.js new file mode 100644 index 0000000..dcc0a98 --- /dev/null +++ b/src/services/sales/salesorders.js @@ -0,0 +1,460 @@ +import config from '../../config.js'; +import { salesOrderModel } from '../../database/schemas/sales/salesorder.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'; +import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js'; +import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js'; + +const logger = log4js.getLogger('Sales Orders'); +logger.level = config.server.logLevel; + +export const listSalesOrdersRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = '', + filter = {}, + search = '', + sort = '', + order = 'ascend' +) => { + const result = await listObjects({ + model: salesOrderModel, + page, + limit, + property, + filter, + search, + sort, + order, + populate: ['client'], + }); + + if (result?.error) { + logger.error('Error listing sales orders.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of sales orders (Page ${page}, Limit ${limit}). Count: ${result.length}`); + res.send(result); +}; + +export const listSalesOrdersByPropertiesRouteHandler = async ( + req, + res, + properties = '', + filter = {}, + masterFilter = {} +) => { + const result = await listObjectsByProperties({ + model: salesOrderModel, + properties, + filter, + populate: ['client'], + masterFilter, + }); + + if (result?.error) { + logger.error('Error listing sales orders.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of sales orders. Count: ${result.length}`); + res.send(result); +}; + +export const getSalesOrderRouteHandler = async (req, res) => { + const id = req.params.id; + const result = await getObject({ + model: salesOrderModel, + id, + populate: ['client'], + }); + if (result?.error) { + logger.warn(`Sales Order not found with supplied id.`); + return res.status(result.code).send(result); + } + logger.debug(`Retreived sales order with ID: ${id}`); + res.send(result); +}; + +export const editSalesOrderRouteHandler = async (req, res) => { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Sales Order with ID: ${id}`); + + const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['draft'] }); + + if (checkStatesResult.error) { + logger.error('Error checking sales order states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Sales order is not in draft state.'); + res.status(400).send({ error: 'Sales order is not in draft state.', code: 400 }); + return; + } + + const updateData = { + updatedAt: new Date(), + client: req.body.client, + }; + // Create audit log before updating + const result = await editObject({ + model: salesOrderModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error editing sales order:', result.error); + res.status(result).send(result); + return; + } + + logger.debug(`Edited sales order with ID: ${id}`); + + res.send(result); +}; + +export const editMultipleSalesOrdersRouteHandler = async (req, res) => { + const updates = req.body.map((update) => ({ + _id: update._id, + client: update.client, + })); + + if (!Array.isArray(updates)) { + return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 }); + } + + const result = await editObjects({ + model: salesOrderModel, + updates, + user: req.user, + }); + + if (result.error) { + logger.error('Error editing sales orders:', result.error); + res.status(result.code || 500).send(result); + return; + } + + logger.debug(`Edited ${updates.length} sales orders`); + + res.send(result); +}; + +export const newSalesOrderRouteHandler = async (req, res) => { + const newData = { + updatedAt: new Date(), + client: req.body.client, + totalAmount: 0, + totalAmountWithTax: 0, + totalTaxAmount: 0, + }; + const result = await newObject({ + model: salesOrderModel, + newData, + user: req.user, + }); + if (result.error) { + logger.error('No sales order created:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`New sales order with ID: ${result._id}`); + + res.send(result); +}; + +export const deleteSalesOrderRouteHandler = async (req, res) => { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Sales Order with ID: ${id}`); + + const result = await deleteObject({ + model: salesOrderModel, + id, + user: req.user, + }); + if (result.error) { + logger.error('No sales order deleted:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`Deleted sales order with ID: ${result._id}`); + + res.send(result); +}; + +export const getSalesOrderStatsRouteHandler = async (req, res) => { + const result = await getModelStats({ model: salesOrderModel }); + if (result?.error) { + logger.error('Error fetching sales order stats:', result.error); + return res.status(result.code).send(result); + } + logger.trace('Sales order stats:', result); + res.send(result); +}; + +export const getSalesOrderHistoryRouteHandler = async (req, res) => { + const from = req.query.from; + const to = req.query.to; + const result = await getModelHistory({ model: salesOrderModel, from, to }); + if (result?.error) { + logger.error('Error fetching sales order history:', result.error); + return res.status(result.code).send(result); + } + logger.trace('Sales order history:', result); + res.send(result); +}; + +export const postSalesOrderRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Sales Order with ID: ${id}`); + + const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['draft'] }); + + if (checkStatesResult.error) { + logger.error('Error checking sales order states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Sales order is not in draft state.'); + res.status(400).send({ error: 'Sales order is not in draft state.', code: 400 }); + return; + } + + const orderItemsResult = await listObjects({ + model: orderItemModel, + filter: { order: id, orderType: 'salesOrder' }, + pagination: false, + }); + + const shipmentsResult = await listObjects({ + model: shipmentModel, + filter: { order: id, orderType: 'salesOrder' }, + pagination: false, + }); + + for (const orderItem of orderItemsResult) { + if (orderItem.state.type != 'draft') { + logger.warn(`Order item ${orderItem._id} is not in draft state.`); + return res + .status(400) + .send({ error: `Order item ${orderItem._reference} not in draft state.`, code: 400 }); + } + if (!orderItem?.shipment || orderItem?.shipment == null) { + logger.warn(`Order item ${orderItem._id} does not have a shipment.`); + return res + .status(400) + .send({ error: `Order item ${orderItem._reference} does not have a shipment.`, code: 400 }); + } + } + + for (const shipment of shipmentsResult) { + if (shipment.state.type != 'draft') { + logger.warn(`Shipment ${shipment._id} is not in draft state.`); + return res + .status(400) + .send({ error: `Shipment ${shipment._reference} not in draft state.`, code: 400 }); + } + } + + for (const orderItem of orderItemsResult) { + await editObject({ + model: orderItemModel, + id: orderItem._id, + updateData: { + state: { type: 'ordered' }, + orderedAt: new Date(), + }, + user: req.user, + }); + } + + for (const shipment of shipmentsResult) { + await editObject({ + model: shipmentModel, + id: shipment._id, + updateData: { + state: { type: 'planned' }, + }, + user: req.user, + }); + } + + const updateData = { + updatedAt: new Date(), + state: { type: 'sent' }, + postedAt: new Date(), + }; + const result = await editObject({ + model: salesOrderModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error posting sales order:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Posted sales order with ID: ${id}`); + res.send(result); +}; + +export const confirmSalesOrderRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Sales Order with ID: ${id}`); + + const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['sent'] }); + + if (checkStatesResult.error) { + logger.error('Error checking sales order states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Sales order is not in sent state.'); + res.status(400).send({ error: 'Sales order is not in sent state.', code: 400 }); + return; + } + + const updateData = { + updatedAt: new Date(), + state: { type: 'confirmed' }, + confirmedAt: new Date(), + }; + const result = await editObject({ + model: salesOrderModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error confirming sales order:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Confirmed sales order with ID: ${id}`); + res.send(result); +}; + +export const cancelSalesOrderRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Sales Order with ID: ${id}`); + + const checkStatesResult = await checkStates({ + model: salesOrderModel, + id, + states: ['sent', 'confirmed', 'partiallyShipped', 'shipped', 'partiallyDelivered'], + }); + + if (checkStatesResult.error) { + logger.error('Error checking sales order states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Sales order is not in a cancellable state.'); + res.status(400).send({ + error: 'Sales order is not in a cancellable state (must be sent, confirmed, partiallyShipped, shipped, or partiallyDelivered).', + code: 400, + }); + return; + } + + const orderItemsResult = await listObjects({ + model: orderItemModel, + filter: { order: id, orderType: 'salesOrder' }, + pagination: false, + }); + + const shipmentsResult = await listObjects({ + model: shipmentModel, + filter: { order: id, orderType: 'salesOrder' }, + pagination: false, + }); + + const allowedOrderItemStates = ['ordered', 'shipped']; + const allowedShipmentStates = ['shipped', 'planned']; + + for (const orderItem of orderItemsResult) { + if (allowedOrderItemStates.includes(orderItem.state.type)) { + await editObject({ + model: orderItemModel, + id: orderItem._id, + updateData: { + state: { type: 'cancelled' }, + }, + user: req.user, + }); + } + } + + for (const shipment of shipmentsResult) { + if (allowedShipmentStates.includes(shipment.state.type)) { + await editObject({ + model: shipmentModel, + id: shipment._id, + updateData: { + state: { type: 'cancelled' }, + }, + user: req.user, + }); + } + } + const updateData = { + updatedAt: new Date(), + state: { type: 'cancelled' }, + cancelledAt: new Date(), + }; + const result = await editObject({ + model: salesOrderModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error cancelling sales order:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Cancelled sales order with ID: ${id}`); + res.send(result); +}; +