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', }); }); });