diff --git a/src/database/schemas/inventory/filamentstock.schema.js b/src/database/schemas/inventory/filamentstock.schema.js index 1c42010..4033d94 100644 --- a/src/database/schemas/inventory/filamentstock.schema.js +++ b/src/database/schemas/inventory/filamentstock.schema.js @@ -15,9 +15,10 @@ const filamentStockSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, state: { - type: { type: String, required: true }, + type: { type: String, required: true, default: 'draft' }, progress: { type: Number, required: false }, }, + postedAt: { type: Date, required: false }, startingWeight: { net: { type: Number, required: true }, gross: { type: Number, required: true }, @@ -65,6 +66,11 @@ const rollupConfigs = [ filter: {}, rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }], }, + { + name: 'draft', + filter: { 'state.type': 'draft' }, + rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }], + }, { name: 'unconsumed', filter: { 'state.type': 'unconsumed' }, @@ -114,6 +120,8 @@ filamentStockSchema.statics.recalculate = async function (filamentStock, user) { stockLocationId, user, }); + + if (filamentStock.state?.type === 'draft' || filamentStock.state?.type === 'consumed') return; }; // Add virtual id getter diff --git a/src/routes/inventory/filamentstocks.js b/src/routes/inventory/filamentstocks.js index f369cc0..21aca5f 100644 --- a/src/routes/inventory/filamentstocks.js +++ b/src/routes/inventory/filamentstocks.js @@ -34,6 +34,7 @@ import { editMultipleFilamentStocksRouteHandler, newFilamentStockRouteHandler, deleteFilamentStockRouteHandler, + postFilamentStockRouteHandler, listFilamentStocksByPropertiesRouteHandler, getFilamentStockStatsRouteHandler, getFilamentStockHistoryRouteHandler, @@ -121,4 +122,8 @@ router.delete('/:id', isAuthenticated, async (req, res) => { deleteFilamentStockRouteHandler(req, res); }); +router.post('/:id/post', isAuthenticated, checkPermissions('filamentStock', 'post'), async (req, res) => { + postFilamentStockRouteHandler(req, res); +}); + export default router; diff --git a/src/services/inventory/__tests__/filamentstocks.test.js b/src/services/inventory/__tests__/filamentstocks.test.js index ea8012b..3fdedf3 100644 --- a/src/services/inventory/__tests__/filamentstocks.test.js +++ b/src/services/inventory/__tests__/filamentstocks.test.js @@ -6,6 +6,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({ listObjects: jest.fn(), getObject: jest.fn(), editObject: jest.fn(), + editObject: jest.fn(), editObjects: jest.fn(), newObject: jest.fn(), deleteObject: jest.fn(), @@ -13,6 +14,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({ getModelStats: jest.fn(), getModelHistory: jest.fn(), getObjectNeighbors: jest.fn(), + checkStates: jest.fn(), aggregateRollups: jest.fn(), aggregateRollupsHistory: jest.fn(), })); @@ -41,9 +43,12 @@ const { listFilamentStocksRouteHandler, getFilamentStockRouteHandler, newFilamentStockRouteHandler, + postFilamentStockRouteHandler, } = await import('../filamentstocks.js'); -const { listObjects, getObject, newObject } = await import('../../../database/database.js'); +const { listObjects, getObject, newObject, checkStates, editObject } = await import( + '../../../database/database.js' +); const { filamentStockModel } = await import( '../../../database/schemas/inventory/filamentstock.schema.js' ); @@ -80,21 +85,79 @@ describe('Filament Stock Service Route Handlers', () => { }); describe('newFilamentStockRouteHandler', () => { - it('should create a new filament stock', async () => { + it('should create a new draft filament stock without a stock event', async () => { req.body = { filament: 'filament123', startingWeight: { net: 1000, gross: 1100 }, currentWeight: { net: 1000, gross: 1100 }, }; - const mockStock = { _id: '456', ...req.body }; - const mockStockEvent = { _id: '789' }; - newObject.mockResolvedValueOnce(mockStock).mockResolvedValueOnce(mockStockEvent); + const mockStock = { _id: '456', ...req.body, state: { type: 'draft' } }; + newObject.mockResolvedValueOnce(mockStock); await newFilamentStockRouteHandler(req, res); - expect(newObject).toHaveBeenCalledTimes(2); + expect(newObject).toHaveBeenCalledTimes(1); + expect(newObject).toHaveBeenCalledWith( + expect.objectContaining({ + newData: expect.objectContaining({ state: { type: 'draft' } }), + }) + ); expect(res.send).toHaveBeenCalledWith(mockStock); }); }); + + describe('postFilamentStockRouteHandler', () => { + it('should post a draft filament stock and create an initial stock event', async () => { + req.params.id = '507f1f77bcf86cd799439011'; + req.user = { _id: 'test-user-id' }; + checkStates.mockResolvedValue(true); + getObject.mockResolvedValue({ + _id: '507f1f77bcf86cd799439011', + startingWeight: { net: 1000, gross: 1100 }, + }); + newObject.mockResolvedValue({ _id: '789' }); + editObject.mockResolvedValue({ + _id: '507f1f77bcf86cd799439011', + state: { type: 'unconsumed' }, + postedAt: expect.any(Date), + }); + + await postFilamentStockRouteHandler(req, res); + + expect(checkStates).toHaveBeenCalledWith( + expect.objectContaining({ states: ['draft'] }) + ); + expect(newObject).toHaveBeenCalledWith( + expect.objectContaining({ + newData: expect.objectContaining({ + value: 1000, + unit: 'g', + parentType: 'filamentStock', + }), + recalculate: true, + }) + ); + expect(editObject).toHaveBeenCalledWith( + expect.objectContaining({ + updateData: expect.objectContaining({ + state: { type: 'unconsumed' }, + }), + }) + ); + expect(res.send).toHaveBeenCalled(); + }); + + it('should fail if filament stock is not in draft state', async () => { + req.params.id = '507f1f77bcf86cd799439011'; + checkStates.mockResolvedValue(false); + + await postFilamentStockRouteHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.send).toHaveBeenCalledWith( + expect.objectContaining({ error: 'Filament stock is not in draft state.' }) + ); + }); + }); }); diff --git a/src/services/inventory/filamentstocks.js b/src/services/inventory/filamentstocks.js index 162eacd..06593f4 100644 --- a/src/services/inventory/filamentstocks.js +++ b/src/services/inventory/filamentstocks.js @@ -12,6 +12,7 @@ import { listObjectsByProperties, getModelStats, getModelHistory, + checkStates, searchObjects, getPropertyValues, getObjectNeighbors, @@ -20,6 +21,12 @@ import { stockEventModel } from '../../database/schemas/inventory/stockevent.sch const logger = log4js.getLogger('Filament Stocks'); logger.level = config.server.logLevel; +const FILAMENT_STOCK_POPULATE = [ + { path: 'filament' }, + { path: 'filamentSku', populate: 'filament' }, + { path: 'stockLocation' }, +]; + export const listFilamentStocksRouteHandler = async ( req, res, @@ -130,19 +137,33 @@ export const editFilamentStockRouteHandler = async (req, res) => { logger.trace(`Filament Stock with ID: ${id}`); + const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] }); + + if (checkStatesResult.error) { + logger.error('Error checking filament stock states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Filament stock is not in draft state.'); + res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 }); + return; + } + const updateData = { - stockLocation: req.body.stockLocation, + filament: req.body?.filament, + filamentSku: req.body?.filamentSku, + stockLocation: req.body?.stockLocation, + startingWeight: req.body?.startingWeight, + currentWeight: req.body?.currentWeight ?? req.body?.startingWeight, }; const result = await editObject({ model: filamentStockModel, id, updateData, user: req.user, - populate: [ - { path: 'filament' }, - { path: 'filamentSku', populate: 'filament' }, - { path: 'stockLocation' }, - ], + populate: FILAMENT_STOCK_POPULATE, }); if (result.error) { @@ -183,13 +204,14 @@ export const editMultipleFilamentStocksRouteHandler = async (req, res) => { }; export const newFilamentStockRouteHandler = async (req, res) => { + const startingWeight = req.body.startingWeight; const newData = { updatedAt: new Date(), - startingWeight: req.body.startingWeight, - currentWeight: req.body.currentWeight, + startingWeight, + currentWeight: req.body.currentWeight ?? startingWeight, filament: req.body.filament, filamentSku: req.body.filamentSku, - state: req.body.state, + state: req.body.state ?? { type: 'draft' }, stockLocation: req.body.stockLocation, }; const result = await newObject({ @@ -204,28 +226,6 @@ export const newFilamentStockRouteHandler = async (req, res) => { logger.debug(`New filament stock with ID: ${result._id}`); - const netStockEventData = { - updatedAt: new Date(), - value: req.body.startingWeight.net, - owner: req.user, - ownerType: 'user', - parent: result._id, - parentType: 'filamentStock', - unit: 'g', - }; - - const stockEventResult = await newObject({ - model: stockEventModel, - newData: netStockEventData, - user: req.user, - }); - if (stockEventResult.error) { - logger.error('No stock event created:', stockEventResult.error); - return res.status(stockEventResult.code).send(stockEventResult); - } - - logger.debug(`New stock event with ID: ${stockEventResult._id}`); - res.send(result); }; @@ -235,6 +235,20 @@ export const deleteFilamentStockRouteHandler = async (req, res) => { logger.trace(`Filament Stock with ID: ${id}`); + const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] }); + + if (checkStatesResult.error) { + logger.error('Error checking filament stock states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Filament stock is not in draft state.'); + res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 }); + return; + } + const result = await deleteObject({ model: filamentStockModel, id, @@ -250,6 +264,79 @@ export const deleteFilamentStockRouteHandler = async (req, res) => { res.send(result); }; +export const postFilamentStockRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Filament Stock with ID: ${id}`); + + const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] }); + + if (checkStatesResult.error) { + logger.error('Error checking filament stock states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Filament stock is not in draft state.'); + res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 }); + return; + } + + const filamentStock = await getObject({ + model: filamentStockModel, + id, + populate: FILAMENT_STOCK_POPULATE, + }); + if (filamentStock?.error) { + logger.error('Error loading filament stock to post:', filamentStock.error); + res.status(filamentStock.code || 500).send(filamentStock); + return; + } + + const initialStockEventResult = await newObject({ + model: stockEventModel, + newData: { + value: filamentStock.startingWeight.net, + unit: 'g', + parent: { _id: id }, + parentType: 'filamentStock', + owner: { _id: req.user._id }, + ownerType: 'user', + }, + recalculate: true, + user: req.user, + }); + if (initialStockEventResult?.error) { + logger.error('Error creating initial stock event:', initialStockEventResult.error); + res.status(initialStockEventResult.code || 500).send(initialStockEventResult); + return; + } + + const updateData = { + updatedAt: new Date(), + state: { type: 'unconsumed' }, + postedAt: new Date(), + currentWeight: filamentStock.startingWeight, + }; + const result = await editObject({ + model: filamentStockModel, + id, + updateData, + user: req.user, + populate: FILAMENT_STOCK_POPULATE, + }); + + if (result.error) { + logger.error('Error posting filament stock:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Posted filament stock with ID: ${id}`); + res.send(result); +}; + export const getFilamentStockStatsRouteHandler = async (req, res) => { const result = await getModelStats({ model: filamentStockModel }); if (result?.error) { diff --git a/src/services/misc/spotlight.js b/src/services/misc/spotlight.js index 36679fb..bba26a1 100644 --- a/src/services/misc/spotlight.js +++ b/src/services/misc/spotlight.js @@ -51,6 +51,15 @@ const trimSpotlightObject = (object, objectType) => { online: object.online || undefined, amount: object.amount || undefined, unit: object.unit || undefined, + currentWeight: object.currentWeight || undefined, + currentQuantity: object.currentQuantity || undefined, + grandTotalAmount: object.grandTotalAmount || undefined, + totalAmount: object.totalAmount || undefined, + totalAmountWithTax: object.totalAmountWithTax || undefined, + cost: object.cost || undefined, + costWithTax: object.costWithTax || undefined, + price: object.price || undefined, + priceWithTax: object.priceWithTax || undefined, }; };