farmcontrol-ws/src/database/schemas/__tests__/stockQuantity.recalculate.test.js
Tom Butcher 4109523f59
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
Add stock quantity recalculation tests and enhance inventory schemas
- Introduced a new test suite for stock quantity recalculation in `stockQuantity.recalculate.test.js`, ensuring comprehensive coverage of listing variant recalculation logic.
- Enhanced the `filamentstock`, `partstock`, and `productstock` schemas by adding new rollup configurations for tracking unconsumed, used, and consumed states.
- Implemented a pre-validation hook in the `partstock` and `productstock` schemas to automatically set the `part` field based on the associated `partSku`.
- Updated the `stockevent` schema to include new rollup configurations and statistics methods for better inventory management.
- Improved the `listing` and `listingvarient` schemas with additional recalculation logic and statistics methods to enhance data integrity and reporting capabilities.
2026-08-25 01:59:22 +01:00

413 lines
14 KiB
JavaScript

import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import mongoose from 'mongoose';
jest.unstable_mockModule('../../database.js', () => ({
searchObjects: jest.fn(),
getPropertyValues: jest.fn(),
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
aggregateRollups: jest.fn(),
aggregateRollupsHistory: jest.fn(),
checkStates: jest.fn(),
getObjectNeighbors: jest.fn(),
}));
jest.unstable_mockModule('../../utils.js', () => ({
generateId: jest.fn(() => () => 'test-id'),
}));
const { aggregateRollups, editObject, newObject, deleteObject } = await import('../../database.js');
const { listingModel } = await import('../sales/listing.schema.js');
const { listingVarientModel } = await import('../sales/listingvarient.schema.js');
const { productSkuModel } = await import('../management/productsku.schema.js');
const { productStockModel } = await import('../inventory/productstock.schema.js');
const listingId = new mongoose.Types.ObjectId();
const productId = new mongoose.Types.ObjectId();
const productSkuId = new mongoose.Types.ObjectId();
const stockLocationId = new mongoose.Types.ObjectId();
const varientId = new mongoose.Types.ObjectId();
const mockFind = (docs) => ({
sort: () => ({ lean: async () => docs }),
});
describe('listing.recalculate', () => {
beforeEach(() => {
editObject.mockReset();
newObject.mockReset();
deleteObject.mockReset();
jest.restoreAllMocks();
});
it('calls recalculate on each listing varient', async () => {
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
jest.spyOn(listingVarientModel, 'find').mockReturnValue(
mockFind([{ _id: varientId, listing: listingId }])
);
const listing = { _id: listingId, stockLocation: stockLocationId };
await listingModel.recalculate(listing, 'user-1');
expect(listingVarientModel.find).toHaveBeenCalledWith({ listing: listingId });
expect(recalculate).toHaveBeenCalledWith({ _id: varientId, listing: listingId }, 'user-1');
expect(newObject).not.toHaveBeenCalled();
expect(deleteObject).not.toHaveBeenCalled();
});
it('creates listing varients from product skus when none exist', async () => {
const skuBId = new mongoose.Types.ObjectId();
const createdVarientId = new mongoose.Types.ObjectId();
const productSkus = [{ _id: productSkuId }, { _id: skuBId }];
const syncedVarients = [
{ _id: varientId, listing: listingId, product: productId, productSku: productSkuId },
{ _id: createdVarientId, listing: listingId, product: productId, productSku: skuBId },
];
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
jest
.spyOn(listingVarientModel, 'find')
.mockReturnValueOnce(mockFind([]))
.mockReturnValueOnce(mockFind(syncedVarients));
jest.spyOn(productSkuModel, 'find').mockReturnValue(mockFind(productSkus));
newObject.mockResolvedValue({ _id: createdVarientId });
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
expect(productSkuModel.find).toHaveBeenCalledWith({ product: productId });
expect(editObject).not.toHaveBeenCalled();
expect(deleteObject).not.toHaveBeenCalled();
expect(newObject).toHaveBeenCalledTimes(2);
expect(newObject).toHaveBeenCalledWith({
model: listingVarientModel,
newData: expect.objectContaining({
listing: listingId,
product: productId,
productSku: productSkuId,
state: { type: 'draft' },
}),
user: 'user-1',
recalculate: false,
});
expect(recalculate).toHaveBeenCalledTimes(2);
});
it('creates missing listing varients when one already matches a sku', async () => {
const skuBId = new mongoose.Types.ObjectId();
const skuCId = new mongoose.Types.ObjectId();
const existingVarient = {
_id: varientId,
listing: listingId,
product: productId,
productSku: productSkuId,
};
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
jest
.spyOn(listingVarientModel, 'find')
.mockReturnValueOnce(mockFind([existingVarient]))
.mockReturnValueOnce(
mockFind([
existingVarient,
{ _id: new mongoose.Types.ObjectId(), listing: listingId, product: productId, productSku: skuBId },
{ _id: new mongoose.Types.ObjectId(), listing: listingId, product: productId, productSku: skuCId },
])
);
jest
.spyOn(productSkuModel, 'find')
.mockReturnValue(mockFind([{ _id: productSkuId }, { _id: skuBId }, { _id: skuCId }]));
newObject.mockResolvedValue({ _id: new mongoose.Types.ObjectId() });
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
expect(editObject).not.toHaveBeenCalled();
expect(newObject).toHaveBeenCalledTimes(2);
expect(newObject).toHaveBeenCalledWith({
model: listingVarientModel,
newData: expect.objectContaining({
listing: listingId,
product: productId,
productSku: skuBId,
state: { type: 'draft' },
}),
user: 'user-1',
recalculate: false,
});
expect(recalculate).toHaveBeenCalledTimes(3);
});
it('rebuilds listing varients when an existing varient is missing a product sku', async () => {
const skuBId = new mongoose.Types.ObjectId();
jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
jest
.spyOn(listingVarientModel, 'find')
.mockReturnValueOnce(
mockFind([{ _id: varientId, listing: listingId, product: productId }])
)
.mockReturnValueOnce(
mockFind([
{ _id: varientId, listing: listingId, product: productId, productSku: productSkuId },
{
_id: new mongoose.Types.ObjectId(),
listing: listingId,
product: productId,
productSku: skuBId,
},
])
);
jest
.spyOn(productSkuModel, 'find')
.mockReturnValue(mockFind([{ _id: productSkuId }, { _id: skuBId }]));
editObject.mockResolvedValue({});
newObject.mockResolvedValue({ _id: new mongoose.Types.ObjectId() });
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
expect(editObject).toHaveBeenCalledWith({
model: listingVarientModel,
id: varientId,
updateData: expect.objectContaining({
product: productId,
productSku: productSkuId,
}),
user: 'user-1',
recalculate: false,
});
expect(newObject).toHaveBeenCalledTimes(1);
});
it('does not rebuild varients when they already match the product skus', async () => {
const existingVarient = {
_id: varientId,
listing: listingId,
product: productId,
productSku: productSkuId,
};
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
jest.spyOn(listingVarientModel, 'find').mockReturnValue(mockFind([existingVarient]));
jest.spyOn(productSkuModel, 'find').mockReturnValue(mockFind([{ _id: productSkuId }]));
await listingModel.recalculate(
{ _id: listingId, product: productId, stockLocation: stockLocationId },
'user-1'
);
expect(newObject).not.toHaveBeenCalled();
expect(deleteObject).not.toHaveBeenCalled();
expect(editObject).not.toHaveBeenCalled();
expect(recalculate).toHaveBeenCalledTimes(1);
});
it('creates and updates listing varients to match product skus when a product differs', async () => {
const otherProductId = new mongoose.Types.ObjectId();
const skuBId = new mongoose.Types.ObjectId();
const skuCId = new mongoose.Types.ObjectId();
const createdVarientId = new mongoose.Types.ObjectId();
const existingVarients = [{ _id: varientId, listing: listingId, product: otherProductId }];
const productSkus = [{ _id: productSkuId }, { _id: skuBId }, { _id: skuCId }];
const syncedVarients = [
{ _id: varientId, listing: listingId, product: productId, productSku: productSkuId },
{ _id: createdVarientId, listing: listingId, product: productId, productSku: skuBId },
{ _id: new mongoose.Types.ObjectId(), listing: listingId, product: productId, productSku: skuCId },
];
const recalculate = jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
jest
.spyOn(listingVarientModel, 'find')
.mockReturnValueOnce(mockFind(existingVarients))
.mockReturnValueOnce(mockFind(syncedVarients));
jest.spyOn(productSkuModel, 'find').mockReturnValue(mockFind(productSkus));
editObject.mockResolvedValue({});
newObject.mockResolvedValue({ _id: createdVarientId });
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
expect(editObject).toHaveBeenCalledWith({
model: listingVarientModel,
id: varientId,
updateData: expect.objectContaining({
product: productId,
productSku: productSkuId,
}),
user: 'user-1',
recalculate: false,
});
expect(newObject).toHaveBeenCalledTimes(2);
expect(newObject).toHaveBeenCalledWith({
model: listingVarientModel,
newData: expect.objectContaining({
listing: listingId,
product: productId,
productSku: skuBId,
state: { type: 'draft' },
}),
user: 'user-1',
recalculate: false,
});
expect(deleteObject).not.toHaveBeenCalled();
expect(recalculate).toHaveBeenCalledTimes(3);
});
it('deletes extra listing varients when the product has fewer skus', async () => {
const otherProductId = new mongoose.Types.ObjectId();
const extraVarientId = new mongoose.Types.ObjectId();
const existingVarients = [
{ _id: varientId, listing: listingId, product: otherProductId },
{ _id: extraVarientId, listing: listingId, product: otherProductId },
];
jest.spyOn(listingVarientModel, 'recalculate').mockResolvedValue();
jest
.spyOn(listingVarientModel, 'find')
.mockReturnValueOnce(mockFind(existingVarients))
.mockReturnValueOnce(
mockFind([{ _id: varientId, listing: listingId, product: productId, productSku: productSkuId }])
);
jest.spyOn(productSkuModel, 'find').mockReturnValue(mockFind([{ _id: productSkuId }]));
editObject.mockResolvedValue({});
deleteObject.mockResolvedValue({});
await listingModel.recalculate({ _id: listingId, product: productId }, 'user-1');
expect(editObject).toHaveBeenCalledTimes(1);
expect(newObject).not.toHaveBeenCalled();
expect(deleteObject).toHaveBeenCalledWith({
model: listingVarientModel,
id: extraVarientId,
user: 'user-1',
});
});
});
describe('listingVarient.recalculate', () => {
beforeEach(() => {
aggregateRollups.mockReset();
editObject.mockReset();
jest.restoreAllMocks();
});
it('sums sibling listing varient stock quantities onto the listing', async () => {
aggregateRollups.mockResolvedValue({ stockQuantity: { sum: 12 } });
editObject.mockResolvedValue({});
await listingVarientModel.recalculate({ listing: listingId, stockQuantity: 4 }, 'user-1');
expect(aggregateRollups).toHaveBeenCalledWith(
expect.objectContaining({
model: listingVarientModel,
baseFilter: { listing: listingId },
})
);
expect(editObject).toHaveBeenCalledWith({
model: listingModel,
id: listingId,
updateData: { stockQuantity: 12 },
user: 'user-1',
recalculate: false,
});
});
it('writes the product sku stock total onto the listing varient before rolling up', async () => {
jest.spyOn(listingVarientModel, 'exists').mockResolvedValue({ _id: varientId });
aggregateRollups.mockImplementation(async ({ model }) => {
if (model === productStockModel) {
return { stockQuantity: { sum: 9 } };
}
return { stockQuantity: { sum: 12 } };
});
editObject.mockResolvedValue({});
await listingVarientModel.recalculate(
{
_id: varientId,
listing: { _id: listingId, stockLocation: stockLocationId, product: productId },
product: productId,
productSku: productSkuId,
stockQuantity: 0,
},
'user-1'
);
expect(aggregateRollups).toHaveBeenCalledWith(
expect.objectContaining({
model: productStockModel,
baseFilter: {
productSku: productSkuId,
stockLocation: stockLocationId,
},
})
);
expect(editObject).toHaveBeenCalledWith({
model: listingVarientModel,
id: varientId,
updateData: { stockQuantity: 9 },
user: 'user-1',
recalculate: false,
});
expect(editObject).toHaveBeenCalledWith({
model: listingModel,
id: listingId,
updateData: { stockQuantity: 12 },
user: 'user-1',
recalculate: false,
});
});
});
describe('productStock.recalculate', () => {
beforeEach(() => {
aggregateRollups.mockReset();
editObject.mockReset();
jest.restoreAllMocks();
});
it('writes the sku/location total onto matching listing varients', async () => {
aggregateRollups.mockResolvedValue({ stockQuantity: { sum: 9 } });
editObject.mockResolvedValue({});
jest.spyOn(productSkuModel, 'findById').mockReturnValue({
select: () => ({ lean: async () => ({ product: productId }) }),
});
jest.spyOn(listingVarientModel, 'find').mockReturnValue({
populate: () => ({
lean: async () => [
{
_id: varientId,
product: productId,
productSku: productSkuId,
stockQuantity: 0,
listing: { product: productId, stockLocation: stockLocationId },
},
],
}),
});
await productStockModel.recalculate(
{ productSku: productSkuId, stockLocation: stockLocationId, currentQuantity: 9 },
'user-1'
);
expect(aggregateRollups).toHaveBeenCalledWith(
expect.objectContaining({
model: productStockModel,
baseFilter: {
productSku: productSkuId,
stockLocation: stockLocationId,
},
})
);
expect(editObject).toHaveBeenCalledWith({
model: listingVarientModel,
id: varientId,
updateData: { stockQuantity: 9 },
user: 'user-1',
});
});
});