Add stock quantity recalculation tests and enhance inventory schemas
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
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.
This commit is contained in:
parent
91c88b5cbb
commit
4109523f59
412
src/database/schemas/__tests__/stockQuantity.recalculate.test.js
Normal file
412
src/database/schemas/__tests__/stockQuantity.recalculate.test.js
Normal file
@ -0,0 +1,412 @@
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -58,6 +58,21 @@ const rollupConfigs = [
|
||||
filter: {},
|
||||
rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }],
|
||||
},
|
||||
{
|
||||
name: 'unconsumed',
|
||||
filter: { 'state.type': 'unconsumed' },
|
||||
rollups: [{ name: 'unconsumed', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'used',
|
||||
filter: { 'state.type': 'used' },
|
||||
rollups: [{ name: 'used', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'consumed',
|
||||
filter: { 'state.type': 'consumed' },
|
||||
rollups: [{ name: 'consumed', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
];
|
||||
|
||||
filamentStockSchema.statics.stats = async function () {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||
|
||||
// Define the main partStock schema
|
||||
const partStockSchema = new Schema(
|
||||
@ -11,6 +11,7 @@ const partStockSchema = new Schema(
|
||||
type: { type: String, required: true },
|
||||
progress: { type: Number, required: false },
|
||||
},
|
||||
part: { type: mongoose.Schema.Types.ObjectId, ref: 'part', required: true },
|
||||
partSku: { type: mongoose.Schema.Types.ObjectId, ref: 'partSku', required: true },
|
||||
stockLocation: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
@ -32,12 +33,34 @@ const partStockSchema = new Schema(
|
||||
|
||||
partStockSchema.index({ sourceType: 'text', 'state.type': 'text' });
|
||||
|
||||
partStockSchema.pre('validate', async function () {
|
||||
if (!this.part && this.partSku) {
|
||||
const sku = await mongoose.model('partSku').findById(this.partSku).select('part').lean();
|
||||
if (sku?.part) this.part = sku.part;
|
||||
}
|
||||
});
|
||||
|
||||
const rollupConfigs = [
|
||||
{
|
||||
name: 'totalCurrentQuantity',
|
||||
filter: {},
|
||||
rollups: [{ name: 'totalCurrentQuantity', property: 'currentQuantity', operation: 'sum' }],
|
||||
},
|
||||
{
|
||||
name: 'new',
|
||||
filter: { 'state.type': 'new' },
|
||||
rollups: [{ name: 'new', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'used',
|
||||
filter: { 'state.type': 'used' },
|
||||
rollups: [{ name: 'used', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'consumed',
|
||||
filter: { 'state.type': 'consumed' },
|
||||
rollups: [{ name: 'consumed', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
];
|
||||
|
||||
partStockSchema.statics.stats = async function () {
|
||||
@ -61,6 +84,22 @@ partStockSchema.statics.history = async function (from, to) {
|
||||
return results;
|
||||
};
|
||||
|
||||
partStockSchema.statics.recalculate = async function (partStock, user) {
|
||||
if (!partStock?._id) return;
|
||||
if (partStock.state?.type === 'draft' || partStock.state?.type === 'consumed') return;
|
||||
if ((Number(partStock.currentQuantity) || 0) > 0) return;
|
||||
|
||||
await editObject({
|
||||
model: this,
|
||||
id: partStock._id,
|
||||
updateData: {
|
||||
state: { ...(partStock.state || {}), type: 'consumed', progress: 0 },
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
partStockSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
|
||||
@ -3,12 +3,25 @@ import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||
|
||||
const partStockUsageSchema = new Schema({
|
||||
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
||||
const partStockListItemSchema = new Schema({
|
||||
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
||||
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
||||
quantity: { type: Number, required: true },
|
||||
partStocks: [{ type: Schema.Types.ObjectId, ref: 'partStock', required: false }],
|
||||
requiredQuantity: { type: Number, required: true },
|
||||
});
|
||||
|
||||
partStockListItemSchema.virtual('remainingQuantity').get(function () {
|
||||
const required = this.requiredQuantity || 0;
|
||||
const stocks = Array.isArray(this.partStocks) ? this.partStocks : [];
|
||||
const available = stocks.reduce(
|
||||
(sum, stock) => sum + (Number(stock?.currentQuantity) || 0),
|
||||
0
|
||||
);
|
||||
return required - available;
|
||||
});
|
||||
|
||||
partStockListItemSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
const toId = (value) => {
|
||||
if (value == null) return null;
|
||||
if (typeof value === 'object' && value._id) return String(value._id);
|
||||
@ -24,6 +37,7 @@ const productStockSchema = new Schema(
|
||||
progress: { type: Number, required: false },
|
||||
},
|
||||
postedAt: { type: Date, required: false },
|
||||
product: { type: mongoose.Schema.Types.ObjectId, ref: 'product', required: true },
|
||||
productSku: { type: mongoose.Schema.Types.ObjectId, ref: 'productSku', required: true },
|
||||
stockLocation: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
@ -37,13 +51,28 @@ const productStockSchema = new Schema(
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
},
|
||||
],
|
||||
partStocks: [partStockUsageSchema],
|
||||
partStockList: [partStockListItemSchema],
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
productStockSchema.index({ 'state.type': 'text' });
|
||||
|
||||
productStockSchema.pre('validate', async function () {
|
||||
if (!this.product && this.productSku) {
|
||||
const sku = await mongoose.model('productSku').findById(this.productSku).select('product').lean();
|
||||
if (sku?.product) this.product = sku.product;
|
||||
}
|
||||
if (this.partStockList?.length) {
|
||||
for (const item of this.partStockList) {
|
||||
if (!item.part && item.partSku) {
|
||||
const sku = await mongoose.model('partSku').findById(item.partSku).select('part').lean();
|
||||
if (sku?.part) item.part = sku.part;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rollupConfigs = [
|
||||
{
|
||||
name: 'totalCurrentQuantity',
|
||||
@ -56,9 +85,19 @@ const rollupConfigs = [
|
||||
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'posted',
|
||||
filter: { 'state.type': 'posted' },
|
||||
rollups: [{ name: 'posted', property: 'state.type', operation: 'count' }],
|
||||
name: 'new',
|
||||
filter: { 'state.type': 'new' },
|
||||
rollups: [{ name: 'new', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'used',
|
||||
filter: { 'state.type': 'used' },
|
||||
rollups: [{ name: 'used', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'consumed',
|
||||
filter: { 'state.type': 'consumed' },
|
||||
rollups: [{ name: 'consumed', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
];
|
||||
|
||||
@ -83,13 +122,30 @@ productStockSchema.statics.history = async function (from, to) {
|
||||
};
|
||||
|
||||
productStockSchema.statics.recalculate = async function (productStock, user) {
|
||||
if (
|
||||
productStock?._id &&
|
||||
productStock.state?.type !== 'draft' &&
|
||||
productStock.state?.type !== 'consumed' &&
|
||||
(Number(productStock.currentQuantity) || 0) <= 0
|
||||
) {
|
||||
await editObject({
|
||||
model: this,
|
||||
id: productStock._id,
|
||||
updateData: {
|
||||
state: { ...(productStock.state || {}), type: 'consumed', progress: 0 },
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
}
|
||||
|
||||
const productSkuId = toId(productStock?.productSku);
|
||||
const stockLocationId = toId(productStock?.stockLocation);
|
||||
if (!productSkuId || !stockLocationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let productId = toId(productStock?.productSku?.product);
|
||||
let productId = toId(productStock?.product) || toId(productStock?.productSku?.product);
|
||||
if (!productId) {
|
||||
const productSku = await mongoose.model('productSku').findById(productSkuId).select('product').lean();
|
||||
productId = toId(productSku?.product);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
import { getObject, editObject } from '../../database.js';
|
||||
import { getObject, editObject, aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const parentStockModelNames = {
|
||||
@ -12,7 +12,7 @@ const parentStockModelNames = {
|
||||
const initialStockStates = {
|
||||
filamentStock: 'unconsumed',
|
||||
partStock: 'new',
|
||||
productStock: 'posted',
|
||||
productStock: 'new',
|
||||
};
|
||||
|
||||
const getStartingAmount = (parentType, parentStock) => {
|
||||
@ -75,7 +75,7 @@ const getStockEventTotal = async (parentId, parentType) => {
|
||||
};
|
||||
};
|
||||
|
||||
const buildParentUpdateData = (parentType, parentStock, events) => {
|
||||
const buildParentUpdateData = (parentType, parentStock, events, stockEvent) => {
|
||||
const updateData = {};
|
||||
let currentAmount;
|
||||
|
||||
@ -87,8 +87,16 @@ const buildParentUpdateData = (parentType, parentStock, events) => {
|
||||
updateData.currentWeight = { net, gross };
|
||||
currentAmount = net;
|
||||
} else {
|
||||
updateData.currentQuantity = events.total;
|
||||
currentAmount = events.total;
|
||||
const eventValue = Number(stockEvent?.value);
|
||||
if (Number.isFinite(eventValue) && eventValue < 0) {
|
||||
updateData.currentQuantity = Math.max(
|
||||
0,
|
||||
(Number(parentStock.currentQuantity) || 0) + eventValue
|
||||
);
|
||||
} else {
|
||||
updateData.currentQuantity = events.total;
|
||||
}
|
||||
currentAmount = updateData.currentQuantity;
|
||||
}
|
||||
|
||||
const state = buildParentState(
|
||||
@ -164,7 +172,7 @@ const appendParentHistoryIfChanged = (
|
||||
};
|
||||
};
|
||||
|
||||
const recalculateParentStock = async (parentType, parentId, user) => {
|
||||
const recalculateParentStock = async (parentType, parentId, user, stockEvent) => {
|
||||
if (!parentType || !parentId) return;
|
||||
|
||||
const modelName = parentStockModelNames[parentType];
|
||||
@ -174,7 +182,6 @@ const recalculateParentStock = async (parentType, parentId, user) => {
|
||||
const parentStock = await getObject({
|
||||
model: parentModel,
|
||||
id: parentId,
|
||||
cached: true,
|
||||
});
|
||||
if (!parentStock || parentStock.error) return;
|
||||
|
||||
@ -187,10 +194,10 @@ const recalculateParentStock = async (parentType, parentId, user) => {
|
||||
updateData: appendParentHistoryIfChanged(
|
||||
parentType,
|
||||
parentStock,
|
||||
buildParentUpdateData(parentType, parentStock, events)
|
||||
buildParentUpdateData(parentType, parentStock, events, stockEvent)
|
||||
),
|
||||
user,
|
||||
recalculate: parentType === 'productStock',
|
||||
recalculate: parentType === 'productStock' || parentType === 'partStock',
|
||||
});
|
||||
};
|
||||
|
||||
@ -217,7 +224,7 @@ const stockEventSchema = new Schema(
|
||||
ownerType: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer'],
|
||||
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer', 'productStock'],
|
||||
},
|
||||
history: [
|
||||
{
|
||||
@ -232,6 +239,44 @@ const stockEventSchema = new Schema(
|
||||
|
||||
stockEventSchema.index({ parentType: 'text', ownerType: 'text', unit: 'text' });
|
||||
|
||||
const rollupConfigs = [
|
||||
{
|
||||
name: 'partStock',
|
||||
filter: { parentType: 'partStock' },
|
||||
rollups: [{ name: 'partStock', property: 'parentType', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'filamentStock',
|
||||
filter: { parentType: 'filamentStock' },
|
||||
rollups: [{ name: 'filamentStock', property: 'parentType', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'productStock',
|
||||
filter: { parentType: 'productStock' },
|
||||
rollups: [{ name: 'productStock', property: 'parentType', operation: 'count' }],
|
||||
},
|
||||
];
|
||||
|
||||
stockEventSchema.statics.stats = async function () {
|
||||
const results = await aggregateRollups({
|
||||
model: this,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
stockEventSchema.statics.history = async function (from, to) {
|
||||
const results = await aggregateRollupsHistory({
|
||||
model: this,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
stockEventSchema.statics.recalculate = async function (stockEvent, user) {
|
||||
const history = stockEvent.history || [];
|
||||
const lastEntry = history.at(-1);
|
||||
@ -253,7 +298,7 @@ stockEventSchema.statics.recalculate = async function (stockEvent, user) {
|
||||
|
||||
const parentType = stockEvent.parentType;
|
||||
const parentId = stockEvent.parent?._id || stockEvent.parent;
|
||||
await recalculateParentStock(parentType, parentId, user);
|
||||
await recalculateParentStock(parentType, parentId, user, stockEvent);
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
|
||||
@ -23,7 +23,7 @@ const productSkuSchema = new Schema(
|
||||
overridePrice: { type: Boolean, default: false },
|
||||
margin: { type: Number, required: false },
|
||||
amount: { type: Number, required: false },
|
||||
parts: [partSkuUsageSchema],
|
||||
parts: { type: [partSkuUsageSchema], default: [] },
|
||||
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||
priceWithTax: { type: Number, required: false },
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
|
||||
const addressSchema = new mongoose.Schema({
|
||||
building: { required: false, type: String },
|
||||
@ -36,4 +37,37 @@ clientSchema.virtual('id').get(function () {
|
||||
|
||||
clientSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
const rollupConfigs = [
|
||||
{
|
||||
name: 'active',
|
||||
filter: { active: true },
|
||||
rollups: [{ name: 'active', property: 'active', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'inactive',
|
||||
filter: { active: false },
|
||||
rollups: [{ name: 'inactive', property: 'active', operation: 'count' }],
|
||||
},
|
||||
];
|
||||
|
||||
clientSchema.statics.stats = async function () {
|
||||
const results = await aggregateRollups({
|
||||
model: this,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
clientSchema.statics.history = async function (from, to) {
|
||||
const results = await aggregateRollupsHistory({
|
||||
model: this,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export const clientModel = mongoose.model('client', clientSchema);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
import { editObject, newObject, deleteObject, aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const listingSchema = new Schema(
|
||||
@ -72,4 +73,155 @@ listingSchema.set('toJSON', {
|
||||
},
|
||||
});
|
||||
|
||||
const refId = (value) => value?._id ?? value;
|
||||
|
||||
listingSchema.statics.recalculate = async function (listing, user) {
|
||||
const listingId = refId(listing);
|
||||
if (!listingId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listingVarientModel = mongoose.model('listingVarient');
|
||||
const productSkuModel = mongoose.model('productSku');
|
||||
const listingProductId = refId(listing.product);
|
||||
|
||||
const findVarients = () =>
|
||||
listingVarientModel.find({ listing: listingId }).sort({ createdAt: 1 }).lean();
|
||||
|
||||
let varients = await findVarients();
|
||||
|
||||
if (listingProductId) {
|
||||
const productSkus = await productSkuModel
|
||||
.find({ product: listingProductId })
|
||||
.sort({ createdAt: 1 })
|
||||
.lean();
|
||||
|
||||
const varientsBySkuId = new Map();
|
||||
const unmatchedVarients = [];
|
||||
for (const varient of varients) {
|
||||
const skuId = refId(varient.productSku);
|
||||
if (skuId && !varientsBySkuId.has(String(skuId))) {
|
||||
varientsBySkuId.set(String(skuId), varient);
|
||||
} else {
|
||||
unmatchedVarients.push(varient);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sku of productSkus) {
|
||||
const skuId = sku._id;
|
||||
const varientUpdateData = {
|
||||
product: listingProductId,
|
||||
productSku: skuId,
|
||||
};
|
||||
const existingVarient = varientsBySkuId.get(String(skuId)) || unmatchedVarients.shift();
|
||||
|
||||
if (existingVarient) {
|
||||
varientsBySkuId.delete(String(skuId));
|
||||
const existingProductId = refId(existingVarient.product);
|
||||
const existingSkuId = refId(existingVarient.productSku);
|
||||
if (
|
||||
String(existingProductId) === String(listingProductId) &&
|
||||
String(existingSkuId) === String(skuId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const varientResult = await editObject({
|
||||
model: listingVarientModel,
|
||||
id: existingVarient._id,
|
||||
updateData: varientUpdateData,
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
if (varientResult.error) {
|
||||
throw varientResult;
|
||||
}
|
||||
} else {
|
||||
const varientResult = await newObject({
|
||||
model: listingVarientModel,
|
||||
newData: {
|
||||
...varientUpdateData,
|
||||
listing: listingId,
|
||||
state: { type: listing.state?.type || 'draft' },
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
if (varientResult.error) {
|
||||
throw varientResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const extra of [...varientsBySkuId.values(), ...unmatchedVarients]) {
|
||||
const deleteResult = await deleteObject({
|
||||
model: listingVarientModel,
|
||||
id: extra._id,
|
||||
user,
|
||||
});
|
||||
if (deleteResult.error) {
|
||||
throw deleteResult;
|
||||
}
|
||||
}
|
||||
|
||||
varients = await findVarients();
|
||||
}
|
||||
|
||||
for (const varient of varients) {
|
||||
await listingVarientModel.recalculate(varient, user);
|
||||
}
|
||||
};
|
||||
|
||||
const rollupConfigs = [
|
||||
{
|
||||
name: 'draft',
|
||||
filter: { 'state.type': 'draft' },
|
||||
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
filter: { 'state.type': 'active' },
|
||||
rollups: [{ name: 'active', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'inactive',
|
||||
filter: { 'state.type': 'inactive' },
|
||||
rollups: [{ name: 'inactive', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'syncing',
|
||||
filter: { 'state.type': 'syncing' },
|
||||
rollups: [{ name: 'syncing', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'suspended',
|
||||
filter: { 'state.type': 'suspended' },
|
||||
rollups: [{ name: 'suspended', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'deleted',
|
||||
filter: { 'state.type': 'deleted' },
|
||||
rollups: [{ name: 'deleted', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
];
|
||||
|
||||
listingSchema.statics.stats = async function () {
|
||||
const results = await aggregateRollups({
|
||||
model: this,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
listingSchema.statics.history = async function (from, to) {
|
||||
const results = await aggregateRollupsHistory({
|
||||
model: this,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export const listingModel = mongoose.model('listing', listingSchema);
|
||||
|
||||
@ -3,6 +3,12 @@ import { generateId } from '../../utils.js';
|
||||
import { aggregateRollups, editObject } from '../../database.js';
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const toId = (value) => {
|
||||
if (value == null) return null;
|
||||
if (typeof value === 'object' && value._id) return String(value._id);
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const listingVarientSchema = new Schema(
|
||||
{
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
@ -29,7 +35,14 @@ const listingVarientSchema = new Schema(
|
||||
);
|
||||
|
||||
listingVarientSchema.index({ currency: 'text', 'state.type': 'text' });
|
||||
listingVarientSchema.index({ listing: 1, externalReference: 1 }, { unique: true, sparse: true });
|
||||
listingVarientSchema.index(
|
||||
{ listing: 1, externalReference: 1 },
|
||||
{
|
||||
unique: true,
|
||||
name: 'listing_1_externalReference_1',
|
||||
partialFilterExpression: { externalReference: { $type: 'string', $gt: '' } },
|
||||
}
|
||||
);
|
||||
|
||||
listingVarientSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
@ -48,6 +61,46 @@ listingVarientSchema.set('toJSON', {
|
||||
|
||||
listingVarientSchema.statics.recalculate = async function (listingVarient, user) {
|
||||
const listingId = listingVarient?.listing?._id || listingVarient?.listing;
|
||||
const varientId = listingVarient?._id;
|
||||
const productSkuId = toId(listingVarient?.productSku);
|
||||
|
||||
if (varientId && productSkuId && (await this.exists({ _id: varientId }))) {
|
||||
let listing = listingVarient.listing;
|
||||
if (!listing?.stockLocation) {
|
||||
listing = await mongoose
|
||||
.model('listing')
|
||||
.findById(listingId)
|
||||
.select('stockLocation product')
|
||||
.lean();
|
||||
}
|
||||
const stockLocationId = toId(listing?.stockLocation);
|
||||
if (stockLocationId) {
|
||||
const stockRollup = await aggregateRollups({
|
||||
model: mongoose.model('productStock'),
|
||||
baseFilter: {
|
||||
productSku: new mongoose.Types.ObjectId(productSkuId),
|
||||
stockLocation: new mongoose.Types.ObjectId(stockLocationId),
|
||||
},
|
||||
rollupConfigs: [
|
||||
{
|
||||
name: 'stockQuantity',
|
||||
rollups: [{ name: 'stockQuantity', property: 'currentQuantity', operation: 'sum' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
const stockQuantity = stockRollup.stockQuantity?.sum || 0;
|
||||
if (listingVarient.stockQuantity !== stockQuantity) {
|
||||
await editObject({
|
||||
model: this,
|
||||
id: varientId,
|
||||
updateData: { stockQuantity },
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!listingId) {
|
||||
return;
|
||||
}
|
||||
@ -75,3 +128,22 @@ listingVarientSchema.statics.recalculate = async function (listingVarient, user)
|
||||
};
|
||||
|
||||
export const listingVarientModel = mongoose.model('listingVarient', listingVarientSchema);
|
||||
|
||||
async function replaceSparseExternalReferenceIndex() {
|
||||
try {
|
||||
const indexes = await listingVarientModel.collection.indexes();
|
||||
const current = indexes.find((idx) => idx.name === 'listing_1_externalReference_1');
|
||||
if (current && (current.sparse || !current.partialFilterExpression)) {
|
||||
await listingVarientModel.collection.dropIndex('listing_1_externalReference_1');
|
||||
}
|
||||
await listingVarientModel.createIndexes();
|
||||
} catch {
|
||||
// Collection/index may not exist until Mongo is connected.
|
||||
}
|
||||
}
|
||||
|
||||
if (mongoose.connection.readyState === 1) {
|
||||
replaceSparseExternalReferenceIndex();
|
||||
} else {
|
||||
mongoose.connection.once('open', replaceSparseExternalReferenceIndex);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { editObject } from '../../database.js';
|
||||
import { editObject, aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
import { generateId } from '../../utils.js';
|
||||
|
||||
const marketplaceSchema = new mongoose.Schema(
|
||||
@ -56,6 +56,54 @@ marketplaceSchema.statics.recalculate = async function (marketplace, user) {
|
||||
});
|
||||
};
|
||||
|
||||
const rollupConfigs = [
|
||||
{
|
||||
name: 'ready',
|
||||
filter: { 'state.type': 'ready' },
|
||||
rollups: [{ name: 'ready', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'syncing',
|
||||
filter: { 'state.type': 'syncing' },
|
||||
rollups: [{ name: 'syncing', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'disconnected',
|
||||
filter: { 'state.type': 'disconnected' },
|
||||
rollups: [{ name: 'disconnected', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'inactive',
|
||||
filter: { 'state.type': 'inactive' },
|
||||
rollups: [{ name: 'inactive', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'offline',
|
||||
filter: { 'state.type': 'offline' },
|
||||
rollups: [{ name: 'offline', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
];
|
||||
|
||||
marketplaceSchema.statics.stats = async function () {
|
||||
const results = await aggregateRollups({
|
||||
model: this,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
marketplaceSchema.statics.history = async function (from, to) {
|
||||
const results = await aggregateRollupsHistory({
|
||||
model: this,
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
rollupConfigs: rollupConfigs,
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
marketplaceSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
export const marketplaceModel = mongoose.model('marketplace', marketplaceSchema);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user