Compare commits
No commits in common. "4109523f59679e6eb6cee880143d7a3055238104" and "2dc2a9d296bbf2cbb9596652ae327fa3b474f8cd" have entirely different histories.
4109523f59
...
2dc2a9d296
@ -340,8 +340,7 @@ export const aggregateRollups = async ({
|
|||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Snapshot absolute rollup values at each point in time by reconstructing
|
// Reusable function to aggregate rollups over history using audit logs
|
||||||
// object state from the current documents plus audit logs.
|
|
||||||
export const aggregateRollupsHistory = async ({
|
export const aggregateRollupsHistory = async ({
|
||||||
model,
|
model,
|
||||||
baseFilter = {},
|
baseFilter = {},
|
||||||
@ -350,146 +349,74 @@ export const aggregateRollupsHistory = async ({
|
|||||||
endDate
|
endDate
|
||||||
}) => {
|
}) => {
|
||||||
if (!rollupConfigs.length) {
|
if (!rollupConfigs.length) {
|
||||||
return [];
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const end = endDate ? new Date(endDate) : new Date();
|
// Helper to map filter keys to audit log structure
|
||||||
const start = startDate
|
const mapFilterToAudit = filter => {
|
||||||
? new Date(startDate)
|
if (Array.isArray(filter)) {
|
||||||
: new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
return filter.map(mapFilterToAudit);
|
||||||
const parentType = model.modelName ? model.modelName : 'unknown';
|
|
||||||
|
|
||||||
const matchesFilter = (obj, filter) => {
|
|
||||||
if (!filter || Object.keys(filter).length === 0) return true;
|
|
||||||
|
|
||||||
for (const [key, expectedValue] of Object.entries(filter)) {
|
|
||||||
const actualValue = _.get(obj, key);
|
|
||||||
if (actualValue != expectedValue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const existedAt = (obj, bucketDate) => {
|
if (_.isPlainObject(filter)) {
|
||||||
if (!obj?.createdAt) return true;
|
const newFilter = {};
|
||||||
return new Date(obj.createdAt) <= bucketDate;
|
for (const key in filter) {
|
||||||
};
|
if (['$or', '$and', '$nor', '$in', '$nin'].includes(key)) {
|
||||||
|
newFilter[key] = mapFilterToAudit(filter[key]);
|
||||||
const snapshotRollups = (objects, bucketDate) => {
|
} else if (key.startsWith('$')) {
|
||||||
const bucketResult = {
|
newFilter[key] = mapFilterToAudit(filter[key]);
|
||||||
date: bucketDate.toISOString()
|
} else if (
|
||||||
};
|
[
|
||||||
|
'operation',
|
||||||
rollupConfigs.forEach(config => {
|
'parent',
|
||||||
const matchingObjects = objects.filter(
|
'parentType',
|
||||||
obj =>
|
'owner',
|
||||||
existedAt(obj, bucketDate) &&
|
'ownerType',
|
||||||
matchesFilter(obj, baseFilter) &&
|
'createdAt',
|
||||||
matchesFilter(obj, config.filter)
|
'updatedAt',
|
||||||
);
|
'_id',
|
||||||
|
'_reference',
|
||||||
(config.rollups || []).forEach(rollup => {
|
'changes'
|
||||||
let value = 0;
|
].includes(key)
|
||||||
if (rollup.operation === 'count') {
|
) {
|
||||||
value = matchingObjects.length;
|
newFilter[key] = mapFilterToAudit(filter[key]);
|
||||||
} else if (rollup.operation === 'sum') {
|
} else {
|
||||||
value = _.sumBy(matchingObjects, obj => _.get(obj, rollup.property) || 0);
|
newFilter[`changes.new.${key}`] = mapFilterToAudit(filter[key]);
|
||||||
} else if (rollup.operation === 'avg') {
|
|
||||||
const sum = _.sumBy(matchingObjects, obj => _.get(obj, rollup.property) || 0);
|
|
||||||
value = matchingObjects.length ? sum / matchingObjects.length : 0;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
|
||||||
bucketResult[rollup.name] = { [rollup.operation]: value };
|
return filter;
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return bucketResult;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const auditLogs = await auditLogModel
|
const matchQuery = {
|
||||||
.find({
|
parentType: model.modelName,
|
||||||
parentType,
|
...mapFilterToAudit(baseFilter)
|
||||||
createdAt: { $gte: start }
|
};
|
||||||
})
|
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
const currentObjects = await model.find(baseFilter).lean();
|
if (startDate || endDate) {
|
||||||
const objectMap = new Map();
|
matchQuery.createdAt = {};
|
||||||
currentObjects.forEach(obj => {
|
if (startDate) matchQuery.createdAt.$gte = new Date(startDate);
|
||||||
objectMap.set(obj._id.toString(), expandObjectIds(obj));
|
if (endDate) matchQuery.createdAt.$lte = new Date(endDate);
|
||||||
|
if (Object.keys(matchQuery.createdAt).length === 0)
|
||||||
|
delete matchQuery.createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedRollupConfigs = rollupConfigs.map(config => ({
|
||||||
|
...config,
|
||||||
|
filter: config.filter ? mapFilterToAudit(config.filter) : undefined,
|
||||||
|
rollups: (config.rollups || []).map(rollup => ({
|
||||||
|
...rollup,
|
||||||
|
property: `changes.new.${rollup.property}`
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
return await aggregateRollups({
|
||||||
|
model: auditLogModel,
|
||||||
|
baseFilter: matchQuery,
|
||||||
|
rollupConfigs: mappedRollupConfigs
|
||||||
});
|
});
|
||||||
|
|
||||||
const extraIds = [
|
|
||||||
...new Set(
|
|
||||||
auditLogs
|
|
||||||
.map(log => log.parent?.toString())
|
|
||||||
.filter(id => id && !objectMap.has(id))
|
|
||||||
)
|
|
||||||
];
|
|
||||||
if (extraIds.length) {
|
|
||||||
const extraObjects = await model.find({ _id: { $in: extraIds } }).lean();
|
|
||||||
extraObjects.forEach(obj => {
|
|
||||||
objectMap.set(obj._id.toString(), expandObjectIds(obj));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectMap.size === 0 && auditLogs.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const buckets = [];
|
|
||||||
let currentTime = new Date(end);
|
|
||||||
currentTime.setSeconds(0, 0);
|
|
||||||
|
|
||||||
while (currentTime >= start) {
|
|
||||||
buckets.push(new Date(currentTime));
|
|
||||||
currentTime = new Date(currentTime.getTime() - 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!buckets.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const workingObjects = new Map();
|
|
||||||
objectMap.forEach((val, key) => workingObjects.set(key, _.cloneDeep(val)));
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
let logIndex = 0;
|
|
||||||
|
|
||||||
for (const bucketDate of buckets) {
|
|
||||||
while (logIndex < auditLogs.length) {
|
|
||||||
const log = auditLogs[logIndex];
|
|
||||||
const logDate = new Date(log.createdAt);
|
|
||||||
|
|
||||||
if (logDate <= bucketDate) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectId = log.parent.toString();
|
|
||||||
const object = workingObjects.get(objectId);
|
|
||||||
|
|
||||||
if (log.operation === 'new') {
|
|
||||||
workingObjects.delete(objectId);
|
|
||||||
} else if (log.operation === 'delete' && log.changes?.old) {
|
|
||||||
if (!workingObjects.has(objectId)) {
|
|
||||||
workingObjects.set(
|
|
||||||
objectId,
|
|
||||||
expandObjectIds({ ...log.changes.old, _id: log.parent })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (object && log.changes?.old) {
|
|
||||||
mergeObjectUpdates(object, log.changes.old);
|
|
||||||
}
|
|
||||||
|
|
||||||
logIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(snapshotRollups(Array.from(workingObjects.values()), bucketDate));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.reverse();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getModelStats = async ({ model }) => {
|
export const getModelStats = async ({ model }) => {
|
||||||
|
|||||||
@ -1,412 +0,0 @@
|
|||||||
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,21 +58,6 @@ const rollupConfigs = [
|
|||||||
filter: {},
|
filter: {},
|
||||||
rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }],
|
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 () {
|
filamentStockSchema.statics.stats = async function () {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||||
|
|
||||||
// Define the main partStock schema
|
// Define the main partStock schema
|
||||||
const partStockSchema = new Schema(
|
const partStockSchema = new Schema(
|
||||||
@ -11,7 +11,6 @@ const partStockSchema = new Schema(
|
|||||||
type: { type: String, required: true },
|
type: { type: String, required: true },
|
||||||
progress: { type: Number, required: false },
|
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 },
|
partSku: { type: mongoose.Schema.Types.ObjectId, ref: 'partSku', required: true },
|
||||||
stockLocation: {
|
stockLocation: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
@ -33,34 +32,12 @@ const partStockSchema = new Schema(
|
|||||||
|
|
||||||
partStockSchema.index({ sourceType: 'text', 'state.type': 'text' });
|
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 = [
|
const rollupConfigs = [
|
||||||
{
|
{
|
||||||
name: 'totalCurrentQuantity',
|
name: 'totalCurrentQuantity',
|
||||||
filter: {},
|
filter: {},
|
||||||
rollups: [{ name: 'totalCurrentQuantity', property: 'currentQuantity', operation: 'sum' }],
|
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 () {
|
partStockSchema.statics.stats = async function () {
|
||||||
@ -84,22 +61,6 @@ partStockSchema.statics.history = async function (from, to) {
|
|||||||
return results;
|
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
|
// Add virtual id getter
|
||||||
partStockSchema.virtual('id').get(function () {
|
partStockSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
|
|||||||
@ -3,25 +3,12 @@ import { generateId } from '../../utils.js';
|
|||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||||
|
|
||||||
const partStockListItemSchema = new Schema({
|
const partStockUsageSchema = new Schema({
|
||||||
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
||||||
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
||||||
partStocks: [{ type: Schema.Types.ObjectId, ref: 'partStock', required: false }],
|
quantity: { type: Number, required: true },
|
||||||
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) => {
|
const toId = (value) => {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (typeof value === 'object' && value._id) return String(value._id);
|
if (typeof value === 'object' && value._id) return String(value._id);
|
||||||
@ -37,7 +24,6 @@ const productStockSchema = new Schema(
|
|||||||
progress: { type: Number, required: false },
|
progress: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
postedAt: { type: Date, 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 },
|
productSku: { type: mongoose.Schema.Types.ObjectId, ref: 'productSku', required: true },
|
||||||
stockLocation: {
|
stockLocation: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
@ -51,28 +37,13 @@ const productStockSchema = new Schema(
|
|||||||
timestamp: { type: Date, default: Date.now },
|
timestamp: { type: Date, default: Date.now },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
partStockList: [partStockListItemSchema],
|
partStocks: [partStockUsageSchema],
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
productStockSchema.index({ 'state.type': 'text' });
|
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 = [
|
const rollupConfigs = [
|
||||||
{
|
{
|
||||||
name: 'totalCurrentQuantity',
|
name: 'totalCurrentQuantity',
|
||||||
@ -85,19 +56,9 @@ const rollupConfigs = [
|
|||||||
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'new',
|
name: 'posted',
|
||||||
filter: { 'state.type': 'new' },
|
filter: { 'state.type': 'posted' },
|
||||||
rollups: [{ name: 'new', property: 'state.type', operation: 'count' }],
|
rollups: [{ name: 'posted', 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' }],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -122,30 +83,13 @@ productStockSchema.statics.history = async function (from, to) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
productStockSchema.statics.recalculate = async function (productStock, user) {
|
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 productSkuId = toId(productStock?.productSku);
|
||||||
const stockLocationId = toId(productStock?.stockLocation);
|
const stockLocationId = toId(productStock?.stockLocation);
|
||||||
if (!productSkuId || !stockLocationId) {
|
if (!productSkuId || !stockLocationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let productId = toId(productStock?.product) || toId(productStock?.productSku?.product);
|
let productId = toId(productStock?.productSku?.product);
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
const productSku = await mongoose.model('productSku').findById(productSkuId).select('product').lean();
|
const productSku = await mongoose.model('productSku').findById(productSkuId).select('product').lean();
|
||||||
productId = toId(productSku?.product);
|
productId = toId(productSku?.product);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
import { getObject, editObject, aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
import { getObject, editObject } from '../../database.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const parentStockModelNames = {
|
const parentStockModelNames = {
|
||||||
@ -12,7 +12,7 @@ const parentStockModelNames = {
|
|||||||
const initialStockStates = {
|
const initialStockStates = {
|
||||||
filamentStock: 'unconsumed',
|
filamentStock: 'unconsumed',
|
||||||
partStock: 'new',
|
partStock: 'new',
|
||||||
productStock: 'new',
|
productStock: 'posted',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStartingAmount = (parentType, parentStock) => {
|
const getStartingAmount = (parentType, parentStock) => {
|
||||||
@ -75,7 +75,7 @@ const getStockEventTotal = async (parentId, parentType) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildParentUpdateData = (parentType, parentStock, events, stockEvent) => {
|
const buildParentUpdateData = (parentType, parentStock, events) => {
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
let currentAmount;
|
let currentAmount;
|
||||||
|
|
||||||
@ -87,16 +87,8 @@ const buildParentUpdateData = (parentType, parentStock, events, stockEvent) => {
|
|||||||
updateData.currentWeight = { net, gross };
|
updateData.currentWeight = { net, gross };
|
||||||
currentAmount = net;
|
currentAmount = net;
|
||||||
} else {
|
} else {
|
||||||
const eventValue = Number(stockEvent?.value);
|
updateData.currentQuantity = events.total;
|
||||||
if (Number.isFinite(eventValue) && eventValue < 0) {
|
currentAmount = events.total;
|
||||||
updateData.currentQuantity = Math.max(
|
|
||||||
0,
|
|
||||||
(Number(parentStock.currentQuantity) || 0) + eventValue
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updateData.currentQuantity = events.total;
|
|
||||||
}
|
|
||||||
currentAmount = updateData.currentQuantity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = buildParentState(
|
const state = buildParentState(
|
||||||
@ -172,7 +164,7 @@ const appendParentHistoryIfChanged = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const recalculateParentStock = async (parentType, parentId, user, stockEvent) => {
|
const recalculateParentStock = async (parentType, parentId, user) => {
|
||||||
if (!parentType || !parentId) return;
|
if (!parentType || !parentId) return;
|
||||||
|
|
||||||
const modelName = parentStockModelNames[parentType];
|
const modelName = parentStockModelNames[parentType];
|
||||||
@ -182,6 +174,7 @@ const recalculateParentStock = async (parentType, parentId, user, stockEvent) =>
|
|||||||
const parentStock = await getObject({
|
const parentStock = await getObject({
|
||||||
model: parentModel,
|
model: parentModel,
|
||||||
id: parentId,
|
id: parentId,
|
||||||
|
cached: true,
|
||||||
});
|
});
|
||||||
if (!parentStock || parentStock.error) return;
|
if (!parentStock || parentStock.error) return;
|
||||||
|
|
||||||
@ -194,10 +187,10 @@ const recalculateParentStock = async (parentType, parentId, user, stockEvent) =>
|
|||||||
updateData: appendParentHistoryIfChanged(
|
updateData: appendParentHistoryIfChanged(
|
||||||
parentType,
|
parentType,
|
||||||
parentStock,
|
parentStock,
|
||||||
buildParentUpdateData(parentType, parentStock, events, stockEvent)
|
buildParentUpdateData(parentType, parentStock, events)
|
||||||
),
|
),
|
||||||
user,
|
user,
|
||||||
recalculate: parentType === 'productStock' || parentType === 'partStock',
|
recalculate: parentType === 'productStock',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -224,7 +217,7 @@ const stockEventSchema = new Schema(
|
|||||||
ownerType: {
|
ownerType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer', 'productStock'],
|
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer'],
|
||||||
},
|
},
|
||||||
history: [
|
history: [
|
||||||
{
|
{
|
||||||
@ -239,44 +232,6 @@ const stockEventSchema = new Schema(
|
|||||||
|
|
||||||
stockEventSchema.index({ parentType: 'text', ownerType: 'text', unit: 'text' });
|
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) {
|
stockEventSchema.statics.recalculate = async function (stockEvent, user) {
|
||||||
const history = stockEvent.history || [];
|
const history = stockEvent.history || [];
|
||||||
const lastEntry = history.at(-1);
|
const lastEntry = history.at(-1);
|
||||||
@ -298,7 +253,7 @@ stockEventSchema.statics.recalculate = async function (stockEvent, user) {
|
|||||||
|
|
||||||
const parentType = stockEvent.parentType;
|
const parentType = stockEvent.parentType;
|
||||||
const parentId = stockEvent.parent?._id || stockEvent.parent;
|
const parentId = stockEvent.parent?._id || stockEvent.parent;
|
||||||
await recalculateParentStock(parentType, parentId, user, stockEvent);
|
await recalculateParentStock(parentType, parentId, user);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add virtual id getter
|
// Add virtual id getter
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const productSkuSchema = new Schema(
|
|||||||
overridePrice: { type: Boolean, default: false },
|
overridePrice: { type: Boolean, default: false },
|
||||||
margin: { type: Number, required: false },
|
margin: { type: Number, required: false },
|
||||||
amount: { type: Number, required: false },
|
amount: { type: Number, required: false },
|
||||||
parts: { type: [partSkuUsageSchema], default: [] },
|
parts: [partSkuUsageSchema],
|
||||||
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
priceWithTax: { type: Number, required: false },
|
priceWithTax: { type: Number, required: false },
|
||||||
|
|||||||
@ -23,7 +23,6 @@ const userSettingsSchema = new mongoose.Schema({
|
|||||||
columnVisibility: { type: Schema.Types.Mixed, default: () => ({}) },
|
columnVisibility: { type: Schema.Types.Mixed, default: () => ({}) },
|
||||||
collapseState: { type: Schema.Types.Mixed, default: () => ({}) },
|
collapseState: { type: Schema.Types.Mixed, default: () => ({}) },
|
||||||
},
|
},
|
||||||
pageLayout: { type: Schema.Types.Mixed, default: () => ({}) },
|
|
||||||
|
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
|
||||||
|
|
||||||
const addressSchema = new mongoose.Schema({
|
const addressSchema = new mongoose.Schema({
|
||||||
building: { required: false, type: String },
|
building: { required: false, type: String },
|
||||||
@ -37,37 +36,4 @@ clientSchema.virtual('id').get(function () {
|
|||||||
|
|
||||||
clientSchema.set('toJSON', { virtuals: true });
|
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);
|
export const clientModel = mongoose.model('client', clientSchema);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
import { editObject, newObject, deleteObject, aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const listingSchema = new Schema(
|
const listingSchema = new Schema(
|
||||||
@ -73,155 +72,4 @@ 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);
|
export const listingModel = mongoose.model('listing', listingSchema);
|
||||||
|
|||||||
@ -3,12 +3,6 @@ import { generateId } from '../../utils.js';
|
|||||||
import { aggregateRollups, editObject } from '../../database.js';
|
import { aggregateRollups, editObject } from '../../database.js';
|
||||||
const { Schema } = mongoose;
|
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(
|
const listingVarientSchema = new Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
@ -35,14 +29,7 @@ const listingVarientSchema = new Schema(
|
|||||||
);
|
);
|
||||||
|
|
||||||
listingVarientSchema.index({ currency: 'text', 'state.type': 'text' });
|
listingVarientSchema.index({ currency: 'text', 'state.type': 'text' });
|
||||||
listingVarientSchema.index(
|
listingVarientSchema.index({ listing: 1, externalReference: 1 }, { unique: true, sparse: true });
|
||||||
{ listing: 1, externalReference: 1 },
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
name: 'listing_1_externalReference_1',
|
|
||||||
partialFilterExpression: { externalReference: { $type: 'string', $gt: '' } },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
listingVarientSchema.virtual('id').get(function () {
|
listingVarientSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
@ -61,46 +48,6 @@ listingVarientSchema.set('toJSON', {
|
|||||||
|
|
||||||
listingVarientSchema.statics.recalculate = async function (listingVarient, user) {
|
listingVarientSchema.statics.recalculate = async function (listingVarient, user) {
|
||||||
const listingId = listingVarient?.listing?._id || listingVarient?.listing;
|
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) {
|
if (!listingId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -128,22 +75,3 @@ listingVarientSchema.statics.recalculate = async function (listingVarient, user)
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const listingVarientModel = mongoose.model('listingVarient', listingVarientSchema);
|
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 mongoose from 'mongoose';
|
||||||
import { editObject, aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
import { editObject } from '../../database.js';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
|
||||||
const marketplaceSchema = new mongoose.Schema(
|
const marketplaceSchema = new mongoose.Schema(
|
||||||
@ -56,54 +56,6 @@ 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 });
|
marketplaceSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
export const marketplaceModel = mongoose.model('marketplace', marketplaceSchema);
|
export const marketplaceModel = mongoose.model('marketplace', marketplaceSchema);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { userSettingsModel } from '../database/schemas/misc/usersettings.schema.js';
|
import { userSettingsModel } from '../database/schemas/misc/usersettings.schema.js';
|
||||||
|
|
||||||
const DEFAULTS_CATEGORIES = [
|
const SETTINGS_CATEGORIES = [
|
||||||
'viewMode',
|
'viewMode',
|
||||||
'filterSidebarVisibility',
|
'filterSidebarVisibility',
|
||||||
'sortSidebarVisibility',
|
'sortSidebarVisibility',
|
||||||
@ -8,17 +8,12 @@ const DEFAULTS_CATEGORIES = [
|
|||||||
'collapseState'
|
'collapseState'
|
||||||
];
|
];
|
||||||
|
|
||||||
const TOP_LEVEL_CATEGORIES = ['pageLayout'];
|
|
||||||
|
|
||||||
const SETTINGS_CATEGORIES = [...DEFAULTS_CATEGORIES, ...TOP_LEVEL_CATEGORIES];
|
|
||||||
|
|
||||||
const createEmptySettings = () => ({
|
const createEmptySettings = () => ({
|
||||||
viewMode: {},
|
viewMode: {},
|
||||||
filterSidebarVisibility: {},
|
filterSidebarVisibility: {},
|
||||||
sortSidebarVisibility: {},
|
sortSidebarVisibility: {},
|
||||||
columnVisibility: {},
|
columnVisibility: {},
|
||||||
collapseState: {},
|
collapseState: {}
|
||||||
pageLayout: {}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeCategory = category =>
|
const normalizeCategory = category =>
|
||||||
@ -26,19 +21,13 @@ const normalizeCategory = category =>
|
|||||||
? category
|
? category
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
const normalizeSettings = (userSettings = {}) => {
|
const normalizeSettings = (defaults = {}) => ({
|
||||||
const defaults = userSettings?.defaults ?? {};
|
viewMode: normalizeCategory(defaults?.viewMode),
|
||||||
return {
|
filterSidebarVisibility: normalizeCategory(defaults?.filterSidebarVisibility),
|
||||||
viewMode: normalizeCategory(defaults?.viewMode),
|
sortSidebarVisibility: normalizeCategory(defaults?.sortSidebarVisibility),
|
||||||
filterSidebarVisibility: normalizeCategory(
|
columnVisibility: normalizeCategory(defaults?.columnVisibility),
|
||||||
defaults?.filterSidebarVisibility
|
collapseState: normalizeCategory(defaults?.collapseState)
|
||||||
),
|
});
|
||||||
sortSidebarVisibility: normalizeCategory(defaults?.sortSidebarVisibility),
|
|
||||||
columnVisibility: normalizeCategory(defaults?.columnVisibility),
|
|
||||||
collapseState: normalizeCategory(defaults?.collapseState),
|
|
||||||
pageLayout: normalizeCategory(userSettings?.pageLayout)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSafeSettingsKey = key =>
|
const isSafeSettingsKey = key =>
|
||||||
typeof key === 'string' &&
|
typeof key === 'string' &&
|
||||||
@ -48,11 +37,6 @@ const isSafeSettingsKey = key =>
|
|||||||
!key.includes('$') &&
|
!key.includes('$') &&
|
||||||
!key.includes('\0');
|
!key.includes('\0');
|
||||||
|
|
||||||
const getCategoryPath = (category, key) =>
|
|
||||||
TOP_LEVEL_CATEGORIES.includes(category)
|
|
||||||
? `${category}.${key}`
|
|
||||||
: `defaults.${category}.${key}`;
|
|
||||||
|
|
||||||
export class UserSettingsManager {
|
export class UserSettingsManager {
|
||||||
constructor(socketClient) {
|
constructor(socketClient) {
|
||||||
this.socketClient = socketClient;
|
this.socketClient = socketClient;
|
||||||
@ -70,10 +54,10 @@ export class UserSettingsManager {
|
|||||||
const userId = this.getUserId();
|
const userId = this.getUserId();
|
||||||
const userSettings = await userSettingsModel
|
const userSettings = await userSettingsModel
|
||||||
.findOne({ user: userId })
|
.findOne({ user: userId })
|
||||||
.select('defaults pageLayout')
|
.select('defaults')
|
||||||
.lean();
|
.lean();
|
||||||
|
|
||||||
return normalizeSettings(userSettings);
|
return normalizeSettings(userSettings?.defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserSettings({ category, key, value } = {}) {
|
async updateUserSettings({ category, key, value } = {}) {
|
||||||
@ -91,7 +75,7 @@ export class UserSettingsManager {
|
|||||||
|
|
||||||
const update = {
|
const update = {
|
||||||
$set: {
|
$set: {
|
||||||
[getCategoryPath(category, key)]: value,
|
[`defaults.${category}.${key}`]: value,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
$setOnInsert: {
|
$setOnInsert: {
|
||||||
@ -108,7 +92,7 @@ export class UserSettingsManager {
|
|||||||
upsert: true,
|
upsert: true,
|
||||||
setDefaultsOnInsert: true
|
setDefaultsOnInsert: true
|
||||||
})
|
})
|
||||||
.select('defaults pageLayout')
|
.select('defaults')
|
||||||
.lean();
|
.lean();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.code !== 11000) {
|
if (error?.code !== 11000) {
|
||||||
@ -121,11 +105,11 @@ export class UserSettingsManager {
|
|||||||
{ $set: update.$set },
|
{ $set: update.$set },
|
||||||
{ new: true }
|
{ new: true }
|
||||||
)
|
)
|
||||||
.select('defaults pageLayout')
|
.select('defaults')
|
||||||
.lean();
|
.lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizeSettings(userSettings);
|
return normalizeSettings(userSettings?.defaults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,45 +73,6 @@ describe('UserSettingsManager', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('writes pageLayout at the top level, not under defaults', async () => {
|
|
||||||
const pageLayout = {
|
|
||||||
InventoryOverview: {
|
|
||||||
sectionOrder: ['partStockStats'],
|
|
||||||
statsOrder: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
findOneAndUpdate.mockReturnValue(
|
|
||||||
queryResult({
|
|
||||||
defaults: createEmptySettings(),
|
|
||||||
pageLayout
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
manager.updateUserSettings({
|
|
||||||
category: 'pageLayout',
|
|
||||||
key: 'InventoryOverview',
|
|
||||||
value: pageLayout.InventoryOverview
|
|
||||||
})
|
|
||||||
).resolves.toEqual({
|
|
||||||
...createEmptySettings(),
|
|
||||||
pageLayout
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findOneAndUpdate).toHaveBeenCalledWith(
|
|
||||||
{ user: 'user-id' },
|
|
||||||
expect.objectContaining({
|
|
||||||
$set: expect.objectContaining({
|
|
||||||
'pageLayout.InventoryOverview': pageLayout.InventoryOverview
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
expect(findOneAndUpdate.mock.calls[0][1].$set).not.toHaveProperty(
|
|
||||||
'defaults.pageLayout.InventoryOverview'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalid categories and unsafe keys', async () => {
|
it('rejects invalid categories and unsafe keys', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
manager.updateUserSettings({
|
manager.updateUserSettings({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user