This commit introduces new fields `syncHash` and `syncImageHash` to the `listing` and `listingVariant` schemas, enhancing the tracking of synchronization states. Additionally, a new module for sync fingerprinting is added, which includes functions to generate hashes for listings and their variants. The marketplace integration logic is updated to utilize these hashes for determining if updates are necessary, improving efficiency in syncing operations. Tests are also added to ensure the correctness of the new functionality and its integration with existing systems.
172 lines
5.2 KiB
JavaScript
172 lines
5.2 KiB
JavaScript
import mongoose from 'mongoose';
|
|
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 listingVarientAspectSchema = new Schema(
|
|
{
|
|
name: { type: String, required: true },
|
|
value: { type: String, required: true },
|
|
},
|
|
{ _id: true }
|
|
);
|
|
|
|
const listingVarientSchema = new Schema(
|
|
{
|
|
_reference: { type: String, default: () => generateId()() },
|
|
listing: { type: Schema.Types.ObjectId, ref: 'listing', required: true },
|
|
product: { type: Schema.Types.ObjectId, ref: 'product', required: false },
|
|
productSku: { type: Schema.Types.ObjectId, ref: 'productSku', required: false },
|
|
aspects: { type: [listingVarientAspectSchema], default: [] },
|
|
state: {
|
|
type: {
|
|
type: String,
|
|
enum: [
|
|
'draft',
|
|
'active',
|
|
'inactive',
|
|
'deleted',
|
|
'suspended',
|
|
'syncing',
|
|
'publishing',
|
|
'unpublishing',
|
|
],
|
|
default: 'draft',
|
|
},
|
|
message: { type: String, required: false },
|
|
},
|
|
externalReference: { type: String, required: false },
|
|
price: { type: Number, required: false },
|
|
currency: { type: String, required: false },
|
|
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
priceWithTax: { type: Number, required: false },
|
|
lastSyncedAt: { type: Date, required: false },
|
|
syncHash: { type: String, required: false },
|
|
syncImageHash: { type: String, required: false },
|
|
marketplaceImageUrls: [{ type: String, required: false }],
|
|
listingImages: [{ type: Schema.Types.ObjectId, ref: 'file', required: false }],
|
|
stockQuantity: { type: Number, required: false, default: 0 },
|
|
},
|
|
{ timestamps: true }
|
|
);
|
|
|
|
listingVarientSchema.index({ currency: 'text', 'state.type': 'text' });
|
|
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;
|
|
});
|
|
|
|
listingVarientSchema.set('toJSON', {
|
|
virtuals: true,
|
|
transform(doc, ret) {
|
|
if (!ret.state && ret.status) {
|
|
ret.state = { type: ret.status, message: null };
|
|
}
|
|
if (ret.status) delete ret.status;
|
|
return ret;
|
|
},
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
const rollupResults = await aggregateRollups({
|
|
model: this,
|
|
baseFilter: { listing: new mongoose.Types.ObjectId(listingId) },
|
|
rollupConfigs: [
|
|
{
|
|
name: 'stockQuantity',
|
|
rollups: [{ name: 'stockQuantity', property: 'stockQuantity', operation: 'sum' }],
|
|
},
|
|
],
|
|
});
|
|
|
|
await editObject({
|
|
model: mongoose.model('listing'),
|
|
id: listingId,
|
|
updateData: {
|
|
stockQuantity: rollupResults.stockQuantity?.sum || 0,
|
|
},
|
|
user,
|
|
recalculate: false,
|
|
});
|
|
};
|
|
|
|
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);
|
|
}
|