Enhance filament stock management by adding default state and posting functionality
This commit updates the filament stock schema to set a default state of 'draft' and introduces a new route handler for posting filament stocks. The posting process includes validation to ensure stocks are in the draft state before transitioning to 'unconsumed', along with the creation of an initial stock event. Additionally, the service layer is updated to handle these changes, and tests are added to verify the new functionality.
This commit is contained in:
parent
2af78c76f4
commit
edb8282c41
@ -15,9 +15,10 @@ const filamentStockSchema = new Schema(
|
|||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
state: {
|
state: {
|
||||||
type: { type: String, required: true },
|
type: { type: String, required: true, default: 'draft' },
|
||||||
progress: { type: Number, required: false },
|
progress: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
|
postedAt: { type: Date, required: false },
|
||||||
startingWeight: {
|
startingWeight: {
|
||||||
net: { type: Number, required: true },
|
net: { type: Number, required: true },
|
||||||
gross: { type: Number, required: true },
|
gross: { type: Number, required: true },
|
||||||
@ -65,6 +66,11 @@ const rollupConfigs = [
|
|||||||
filter: {},
|
filter: {},
|
||||||
rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }],
|
rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'draft',
|
||||||
|
filter: { 'state.type': 'draft' },
|
||||||
|
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'unconsumed',
|
name: 'unconsumed',
|
||||||
filter: { 'state.type': 'unconsumed' },
|
filter: { 'state.type': 'unconsumed' },
|
||||||
@ -114,6 +120,8 @@ filamentStockSchema.statics.recalculate = async function (filamentStock, user) {
|
|||||||
stockLocationId,
|
stockLocationId,
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (filamentStock.state?.type === 'draft' || filamentStock.state?.type === 'consumed') return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add virtual id getter
|
// Add virtual id getter
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
editMultipleFilamentStocksRouteHandler,
|
editMultipleFilamentStocksRouteHandler,
|
||||||
newFilamentStockRouteHandler,
|
newFilamentStockRouteHandler,
|
||||||
deleteFilamentStockRouteHandler,
|
deleteFilamentStockRouteHandler,
|
||||||
|
postFilamentStockRouteHandler,
|
||||||
listFilamentStocksByPropertiesRouteHandler,
|
listFilamentStocksByPropertiesRouteHandler,
|
||||||
getFilamentStockStatsRouteHandler,
|
getFilamentStockStatsRouteHandler,
|
||||||
getFilamentStockHistoryRouteHandler,
|
getFilamentStockHistoryRouteHandler,
|
||||||
@ -121,4 +122,8 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
|
|||||||
deleteFilamentStockRouteHandler(req, res);
|
deleteFilamentStockRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/:id/post', isAuthenticated, checkPermissions('filamentStock', 'post'), async (req, res) => {
|
||||||
|
postFilamentStockRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
|
|||||||
listObjects: jest.fn(),
|
listObjects: jest.fn(),
|
||||||
getObject: jest.fn(),
|
getObject: jest.fn(),
|
||||||
editObject: jest.fn(),
|
editObject: jest.fn(),
|
||||||
|
editObject: jest.fn(),
|
||||||
editObjects: jest.fn(),
|
editObjects: jest.fn(),
|
||||||
newObject: jest.fn(),
|
newObject: jest.fn(),
|
||||||
deleteObject: jest.fn(),
|
deleteObject: jest.fn(),
|
||||||
@ -13,6 +14,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
|
|||||||
getModelStats: jest.fn(),
|
getModelStats: jest.fn(),
|
||||||
getModelHistory: jest.fn(),
|
getModelHistory: jest.fn(),
|
||||||
getObjectNeighbors: jest.fn(),
|
getObjectNeighbors: jest.fn(),
|
||||||
|
checkStates: jest.fn(),
|
||||||
aggregateRollups: jest.fn(),
|
aggregateRollups: jest.fn(),
|
||||||
aggregateRollupsHistory: jest.fn(),
|
aggregateRollupsHistory: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@ -41,9 +43,12 @@ const {
|
|||||||
listFilamentStocksRouteHandler,
|
listFilamentStocksRouteHandler,
|
||||||
getFilamentStockRouteHandler,
|
getFilamentStockRouteHandler,
|
||||||
newFilamentStockRouteHandler,
|
newFilamentStockRouteHandler,
|
||||||
|
postFilamentStockRouteHandler,
|
||||||
} = await import('../filamentstocks.js');
|
} = 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(
|
const { filamentStockModel } = await import(
|
||||||
'../../../database/schemas/inventory/filamentstock.schema.js'
|
'../../../database/schemas/inventory/filamentstock.schema.js'
|
||||||
);
|
);
|
||||||
@ -80,21 +85,79 @@ describe('Filament Stock Service Route Handlers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('newFilamentStockRouteHandler', () => {
|
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 = {
|
req.body = {
|
||||||
filament: 'filament123',
|
filament: 'filament123',
|
||||||
startingWeight: { net: 1000, gross: 1100 },
|
startingWeight: { net: 1000, gross: 1100 },
|
||||||
currentWeight: { net: 1000, gross: 1100 },
|
currentWeight: { net: 1000, gross: 1100 },
|
||||||
};
|
};
|
||||||
const mockStock = { _id: '456', ...req.body };
|
const mockStock = { _id: '456', ...req.body, state: { type: 'draft' } };
|
||||||
const mockStockEvent = { _id: '789' };
|
newObject.mockResolvedValueOnce(mockStock);
|
||||||
newObject.mockResolvedValueOnce(mockStock).mockResolvedValueOnce(mockStockEvent);
|
|
||||||
|
|
||||||
await newFilamentStockRouteHandler(req, res);
|
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);
|
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.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
|
checkStates,
|
||||||
searchObjects,
|
searchObjects,
|
||||||
getPropertyValues,
|
getPropertyValues,
|
||||||
getObjectNeighbors,
|
getObjectNeighbors,
|
||||||
@ -20,6 +21,12 @@ import { stockEventModel } from '../../database/schemas/inventory/stockevent.sch
|
|||||||
const logger = log4js.getLogger('Filament Stocks');
|
const logger = log4js.getLogger('Filament Stocks');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
const FILAMENT_STOCK_POPULATE = [
|
||||||
|
{ path: 'filament' },
|
||||||
|
{ path: 'filamentSku', populate: 'filament' },
|
||||||
|
{ path: 'stockLocation' },
|
||||||
|
];
|
||||||
|
|
||||||
export const listFilamentStocksRouteHandler = async (
|
export const listFilamentStocksRouteHandler = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@ -130,19 +137,33 @@ export const editFilamentStockRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
logger.trace(`Filament Stock with ID: ${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 updateData = {
|
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({
|
const result = await editObject({
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: [
|
populate: FILAMENT_STOCK_POPULATE,
|
||||||
{ path: 'filament' },
|
|
||||||
{ path: 'filamentSku', populate: 'filament' },
|
|
||||||
{ path: 'stockLocation' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -183,13 +204,14 @@ export const editMultipleFilamentStocksRouteHandler = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const newFilamentStockRouteHandler = async (req, res) => {
|
export const newFilamentStockRouteHandler = async (req, res) => {
|
||||||
|
const startingWeight = req.body.startingWeight;
|
||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
startingWeight: req.body.startingWeight,
|
startingWeight,
|
||||||
currentWeight: req.body.currentWeight,
|
currentWeight: req.body.currentWeight ?? startingWeight,
|
||||||
filament: req.body.filament,
|
filament: req.body.filament,
|
||||||
filamentSku: req.body.filamentSku,
|
filamentSku: req.body.filamentSku,
|
||||||
state: req.body.state,
|
state: req.body.state ?? { type: 'draft' },
|
||||||
stockLocation: req.body.stockLocation,
|
stockLocation: req.body.stockLocation,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
@ -204,28 +226,6 @@ export const newFilamentStockRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
logger.debug(`New filament stock with ID: ${result._id}`);
|
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);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -235,6 +235,20 @@ export const deleteFilamentStockRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
logger.trace(`Filament Stock with ID: ${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 result = await deleteObject({
|
const result = await deleteObject({
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
id,
|
id,
|
||||||
@ -250,6 +264,79 @@ export const deleteFilamentStockRouteHandler = async (req, res) => {
|
|||||||
res.send(result);
|
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) => {
|
export const getFilamentStockStatsRouteHandler = async (req, res) => {
|
||||||
const result = await getModelStats({ model: filamentStockModel });
|
const result = await getModelStats({ model: filamentStockModel });
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
|
|||||||
@ -51,6 +51,15 @@ const trimSpotlightObject = (object, objectType) => {
|
|||||||
online: object.online || undefined,
|
online: object.online || undefined,
|
||||||
amount: object.amount || undefined,
|
amount: object.amount || undefined,
|
||||||
unit: object.unit || 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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user