All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
- 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.
413 lines
14 KiB
JavaScript
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',
|
|
});
|
|
});
|
|
});
|