Update configuration and enhance marketplace integration with new policies
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit modifies the log level in the configuration file from "trace" to "debug" for improved logging clarity. It also introduces new routes for fulfillment, return, and payment policies in the index file, enhancing the marketplace integration capabilities. Additionally, the initialization process is updated to start the marketplace worker, ensuring that the application can handle marketplace operations effectively. Various schemas are updated to include new fields and relationships for policies, improving the overall functionality and flexibility of the marketplace management system.
This commit is contained in:
parent
f5551f4f03
commit
e98828bfc7
@ -2,7 +2,7 @@
|
|||||||
"development": {
|
"development": {
|
||||||
"server": {
|
"server": {
|
||||||
"port": 8787,
|
"port": 8787,
|
||||||
"logLevel": "trace",
|
"logLevel": "debug",
|
||||||
"corsOrigins": [
|
"corsOrigins": [
|
||||||
"https://web.farmcontrol.app",
|
"https://web.farmcontrol.app",
|
||||||
"https://dev.tombutcher.work",
|
"https://dev.tombutcher.work",
|
||||||
|
|||||||
@ -466,10 +466,7 @@ export const aggregateRollupsHistory = async ({
|
|||||||
workingObjects.delete(objectId);
|
workingObjects.delete(objectId);
|
||||||
} else if (log.operation === 'delete' && log.changes?.old) {
|
} else if (log.operation === 'delete' && log.changes?.old) {
|
||||||
if (!workingObjects.has(objectId)) {
|
if (!workingObjects.has(objectId)) {
|
||||||
workingObjects.set(
|
workingObjects.set(objectId, expandObjectIds({ ...log.changes.old, _id: log.parent }));
|
||||||
objectId,
|
|
||||||
expandObjectIds({ ...log.changes.old, _id: log.parent })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (object && log.changes?.old) {
|
} else if (object && log.changes?.old) {
|
||||||
mergeObjectUpdates(object, log.changes.old);
|
mergeObjectUpdates(object, log.changes.old);
|
||||||
@ -956,12 +953,9 @@ export const listObjectsByProperties = async ({
|
|||||||
// Build aggregation pipeline
|
// Build aggregation pipeline
|
||||||
const pipeline = [];
|
const pipeline = [];
|
||||||
|
|
||||||
logger.debug('Master filter:', masterFilter);
|
|
||||||
|
|
||||||
// Match before populate so reference fields (e.g. filament) are still ObjectIds
|
// Match before populate so reference fields (e.g. filament) are still ObjectIds
|
||||||
if (Object.keys(masterFilter).length > 0) {
|
if (Object.keys(masterFilter).length > 0) {
|
||||||
const convertedFilter = convertObjectIdStringsInFilter(masterFilter);
|
const convertedFilter = convertObjectIdStringsInFilter(masterFilter);
|
||||||
logger.debug('Converted filter:', convertedFilter);
|
|
||||||
pipeline.push({ $match: convertedFilter });
|
pipeline.push({ $match: convertedFilter });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1255,7 +1249,6 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
|||||||
object: updatedObject,
|
object: updatedObject,
|
||||||
populate,
|
populate,
|
||||||
});
|
});
|
||||||
console.log('updatedObject', updatedObject);
|
|
||||||
await invalidateNeighborsCacheForObject({ model, id });
|
await invalidateNeighborsCacheForObject({ model, id });
|
||||||
|
|
||||||
if (model.recalculate && recalculate == true) {
|
if (model.recalculate && recalculate == true) {
|
||||||
|
|||||||
@ -30,6 +30,22 @@ const loadPermissionsFromMongo = async (userId) => {
|
|||||||
return userDoc?.permissions || {};
|
return userDoc?.permissions || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const countTruePermissions = (obj) => {
|
||||||
|
if (obj == null || typeof obj !== 'object') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(obj).reduce((count, value) => {
|
||||||
|
if (value === true) {
|
||||||
|
return count + 1;
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return count + countTruePermissions(value);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
export const hasPermission = async (user, objectType, action) => {
|
export const hasPermission = async (user, objectType, action) => {
|
||||||
const userId = getUserId(user);
|
const userId = getUserId(user);
|
||||||
if (!userId || !objectType || !action) {
|
if (!userId || !objectType || !action) {
|
||||||
@ -46,7 +62,13 @@ export const hasPermission = async (user, objectType, action) => {
|
|||||||
await saveUserPermissionsToRedis(userId, permissions);
|
await saveUserPermissionsToRedis(userId, permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return permissions?.[objectType]?.[action] === true;
|
const allowed = permissions?.[objectType]?.[action] === true;
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Retrieved permissions for user: ${userId}, Allowed: ${allowed ? 'Yes' : 'No'}, Allowed Count: ${countTruePermissions(permissions)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return allowed;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkPermissions = (objectType, action) => async (req, res, next) => {
|
export const checkPermissions = (objectType, action) => async (req, res, next) => {
|
||||||
|
|||||||
25
src/database/schemas/finance/paymentpolicy.schema.js
Normal file
25
src/database/schemas/finance/paymentpolicy.schema.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
import { marketplaceSyncMappingSchema } from '../sales/marketplaceMapping.schema.js';
|
||||||
|
|
||||||
|
const paymentPolicySchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
description: { type: String, required: false },
|
||||||
|
immediatePay: { type: Boolean, required: false, default: true },
|
||||||
|
paymentInstructions: { type: String, required: false },
|
||||||
|
marketplaces: { type: [marketplaceSyncMappingSchema()], default: [] },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
paymentPolicySchema.index({ name: 'text', description: 'text', paymentInstructions: 'text' });
|
||||||
|
|
||||||
|
paymentPolicySchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
paymentPolicySchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const paymentPolicyModel = mongoose.model('paymentPolicy', paymentPolicySchema);
|
||||||
@ -1,16 +1,16 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||||
|
|
||||||
// Define the main partStock schema
|
// Define the main partStock schema
|
||||||
const partStockSchema = new Schema(
|
const partStockSchema = new mongoose.Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
state: {
|
state: {
|
||||||
type: { type: String, required: true },
|
type: { type: String, required: true, default: 'draft' },
|
||||||
progress: { type: Number, required: false },
|
progress: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
|
postedAt: { type: Date, required: false },
|
||||||
part: { type: mongoose.Schema.Types.ObjectId, ref: 'part', required: true },
|
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: {
|
||||||
@ -25,13 +25,11 @@ const partStockSchema = new Schema(
|
|||||||
timestamp: { type: Date, default: Date.now },
|
timestamp: { type: Date, default: Date.now },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sourceType: { type: String, required: true },
|
|
||||||
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },
|
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
partStockSchema.index({ sourceType: 'text', 'state.type': 'text' });
|
partStockSchema.index({ 'state.type': 'text' });
|
||||||
|
|
||||||
partStockSchema.pre('validate', async function () {
|
partStockSchema.pre('validate', async function () {
|
||||||
if (!this.part && this.partSku) {
|
if (!this.part && this.partSku) {
|
||||||
@ -46,6 +44,11 @@ const rollupConfigs = [
|
|||||||
filter: {},
|
filter: {},
|
||||||
rollups: [{ name: 'totalCurrentQuantity', property: 'currentQuantity', operation: 'sum' }],
|
rollups: [{ name: 'totalCurrentQuantity', property: 'currentQuantity', operation: 'sum' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'draft',
|
||||||
|
filter: { 'state.type': 'draft' },
|
||||||
|
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'new',
|
name: 'new',
|
||||||
filter: { 'state.type': 'new' },
|
filter: { 'state.type': 'new' },
|
||||||
|
|||||||
@ -1,10 +1,20 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const marketplaceMappingSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
marketplace: { type: Schema.Types.ObjectId, ref: 'marketplace', required: true },
|
||||||
|
externalReference: { type: String, required: false },
|
||||||
|
},
|
||||||
|
{ _id: true }
|
||||||
|
);
|
||||||
|
|
||||||
const productCategorySchema = new mongoose.Schema(
|
const productCategorySchema = new mongoose.Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
name: { required: true, type: String },
|
name: { required: true, type: String },
|
||||||
|
marketplaces: { type: [marketplaceMappingSchema], default: [] },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { marketplaceSyncMappingSchema } from '../sales/marketplaceMapping.schema.js';
|
||||||
|
|
||||||
const taxRateSchema = new mongoose.Schema(
|
const taxRateSchema = new mongoose.Schema(
|
||||||
{
|
{
|
||||||
@ -10,8 +11,11 @@ const taxRateSchema = new mongoose.Schema(
|
|||||||
active: { required: true, type: Boolean, default: true },
|
active: { required: true, type: Boolean, default: true },
|
||||||
description: { required: false, type: String },
|
description: { required: false, type: String },
|
||||||
country: { required: false, type: String },
|
country: { required: false, type: String },
|
||||||
|
jurisdiction: { required: false, type: String },
|
||||||
|
shippingAndHandlingTaxed: { required: false, type: Boolean, default: false },
|
||||||
effectiveFrom: { required: false, type: Date },
|
effectiveFrom: { required: false, type: Date },
|
||||||
effectiveTo: { required: false, type: Date },
|
effectiveTo: { required: false, type: Date },
|
||||||
|
marketplaces: { type: [marketplaceSyncMappingSchema()], default: [] },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -23,6 +23,7 @@ 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,
|
||||||
|
|||||||
@ -50,365 +50,79 @@ import { listingModel } from './sales/listing.schema.js';
|
|||||||
import { listingVarientModel } from './sales/listingvarient.schema.js';
|
import { listingVarientModel } from './sales/listingvarient.schema.js';
|
||||||
import { marketplaceEventModel } from './sales/marketplaceevent.schema.js';
|
import { marketplaceEventModel } from './sales/marketplaceevent.schema.js';
|
||||||
import { paymentModel } from './finance/payment.schema.js';
|
import { paymentModel } from './finance/payment.schema.js';
|
||||||
|
import { paymentPolicyModel } from './finance/paymentpolicy.schema.js';
|
||||||
|
import { fulfillmentPolicyModel } from './sales/fulfillmentpolicy.schema.js';
|
||||||
|
import { returnPolicyModel } from './sales/returnpolicy.schema.js';
|
||||||
|
|
||||||
// Map prefixes to models and id fields
|
function modelEntry(getModel, type, label) {
|
||||||
export const models = {
|
return {
|
||||||
PRN: {
|
get model() {
|
||||||
model: printerModel,
|
return getModel();
|
||||||
idField: '_id',
|
|
||||||
type: 'printer',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Printer',
|
|
||||||
},
|
},
|
||||||
PPF: {
|
|
||||||
model: printerProfileModel,
|
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'printerProfile',
|
type,
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
label: 'Printer Profile',
|
label,
|
||||||
},
|
};
|
||||||
FPF: {
|
}
|
||||||
model: filamentProfileModel,
|
|
||||||
idField: '_id',
|
// Map prefixes to models and id fields.
|
||||||
type: 'filamentProfile',
|
// Model getters are lazy so circular ESM imports (schema -> utils -> models -> schema)
|
||||||
referenceField: '_reference',
|
// do not read bindings that are still in the temporal dead zone.
|
||||||
label: 'Filament Profile',
|
export const models = {
|
||||||
},
|
PRN: modelEntry(() => printerModel, 'printer', 'Printer'),
|
||||||
FIL: {
|
PPF: modelEntry(() => printerProfileModel, 'printerProfile', 'Printer Profile'),
|
||||||
model: filamentModel,
|
FPF: modelEntry(() => filamentProfileModel, 'filamentProfile', 'Filament Profile'),
|
||||||
idField: '_id',
|
FIL: modelEntry(() => filamentModel, 'filament', 'Filament'),
|
||||||
type: 'filament',
|
FSU: modelEntry(() => filamentSkuModel, 'filamentSku', 'Filament SKU'),
|
||||||
referenceField: '_reference',
|
GCF: modelEntry(() => gcodeFileModel, 'gcodeFile', 'G-Code File'),
|
||||||
label: 'Filament',
|
JOB: modelEntry(() => jobModel, 'job', 'Job'),
|
||||||
},
|
PRT: modelEntry(() => partModel, 'part', 'Part'),
|
||||||
FSU: {
|
PSU: modelEntry(() => partSkuModel, 'partSku', 'Part SKU'),
|
||||||
model: filamentSkuModel,
|
PRD: modelEntry(() => productModel, 'product', 'Product'),
|
||||||
idField: '_id',
|
PCG: modelEntry(() => productCategoryModel, 'productCategory', 'Product Category'),
|
||||||
type: 'filamentSku',
|
SKU: modelEntry(() => productSkuModel, 'productSku', 'Product SKU'),
|
||||||
referenceField: '_reference',
|
VEN: modelEntry(() => vendorModel, 'vendor', 'Vendor'),
|
||||||
label: 'Filament SKU',
|
MAT: modelEntry(() => materialModel, 'material', 'Material'),
|
||||||
},
|
SJB: modelEntry(() => subJobModel, 'subJob', 'Sub Job'),
|
||||||
GCF: {
|
FLS: modelEntry(() => filamentStockModel, 'filamentStock', 'Filament Stock'),
|
||||||
model: gcodeFileModel,
|
SEV: modelEntry(() => stockEventModel, 'stockEvent', 'Stock Event'),
|
||||||
idField: '_id',
|
SAU: modelEntry(() => stockAuditModel, 'stockAudit', 'Stock Audit'),
|
||||||
type: 'gcodeFile',
|
PTS: modelEntry(() => partStockModel, 'partStock', 'Part Stock'),
|
||||||
referenceField: '_reference',
|
PDS: modelEntry(() => productStockModel, 'productStock', 'Product Stock'),
|
||||||
label: 'G-Code File',
|
SLN: modelEntry(() => stockLocationModel, 'stockLocation', 'Stock Location'),
|
||||||
},
|
STT: modelEntry(() => stockTransferModel, 'stockTransfer', 'Stock Transfer'),
|
||||||
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference', label: 'Job' },
|
ADL: modelEntry(() => auditLogModel, 'auditLog', 'Audit Log'),
|
||||||
PRT: {
|
USR: modelEntry(() => userModel, 'user', 'User'),
|
||||||
model: partModel,
|
UGP: modelEntry(() => userGroupModel, 'userGroup', 'User Group'),
|
||||||
idField: '_id',
|
PMS: modelEntry(() => permissionSettingModel, 'permissionSetting', 'Permission Settings'),
|
||||||
type: 'part',
|
APP: modelEntry(() => appPasswordModel, 'appPassword', 'App Password'),
|
||||||
referenceField: '_reference',
|
NTY: modelEntry(() => noteTypeModel, 'noteType', 'Note Type'),
|
||||||
label: 'Part',
|
NTE: modelEntry(() => noteModel, 'note', 'Note'),
|
||||||
},
|
NTF: modelEntry(() => notificationModel, 'notification', 'Notification'),
|
||||||
PSU: {
|
ONF: modelEntry(() => userNotifierModel, 'userNotifier', 'User Notifier'),
|
||||||
model: partSkuModel,
|
DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'),
|
||||||
idField: '_id',
|
DTP: modelEntry(() => documentTemplateModel, 'documentTemplate', 'Document Template'),
|
||||||
type: 'partSku',
|
DPR: modelEntry(() => documentPrinterModel, 'documentPrinter', 'Document Printer'),
|
||||||
referenceField: '_reference',
|
DJB: modelEntry(() => documentJobModel, 'documentJob', 'Document Job'),
|
||||||
label: 'Part SKU',
|
HST: modelEntry(() => hostModel, 'host', 'Host'),
|
||||||
},
|
FLE: modelEntry(() => fileModel, 'file', 'File'),
|
||||||
PRD: {
|
POR: modelEntry(() => purchaseOrderModel, 'purchaseOrder', 'Purchase Order'),
|
||||||
model: productModel,
|
ODI: modelEntry(() => orderItemModel, 'orderItem', 'Order Item'),
|
||||||
idField: '_id',
|
COS: modelEntry(() => courierServiceModel, 'courierService', 'Courier Service'),
|
||||||
type: 'product',
|
COR: modelEntry(() => courierModel, 'courier', 'Courier'),
|
||||||
referenceField: '_reference',
|
TXR: modelEntry(() => taxRateModel, 'taxRate', 'Tax Rate'),
|
||||||
label: 'Product',
|
TXD: modelEntry(() => taxRecordModel, 'taxRecord', 'Tax Record'),
|
||||||
},
|
SHP: modelEntry(() => shipmentModel, 'shipment', 'Shipment'),
|
||||||
PCG: {
|
INV: modelEntry(() => invoiceModel, 'invoice', 'Invoice'),
|
||||||
model: productCategoryModel,
|
CLI: modelEntry(() => clientModel, 'client', 'Client'),
|
||||||
idField: '_id',
|
SOR: modelEntry(() => salesOrderModel, 'salesOrder', 'Sales Order'),
|
||||||
type: 'productCategory',
|
MKT: modelEntry(() => marketplaceModel, 'marketplace', 'Marketplace'),
|
||||||
referenceField: '_reference',
|
LST: modelEntry(() => listingModel, 'listing', 'Listing'),
|
||||||
label: 'Product Category',
|
LVR: modelEntry(() => listingVarientModel, 'listingVarient', 'Listing Varient'),
|
||||||
},
|
MKE: modelEntry(() => marketplaceEventModel, 'marketplaceEvent', 'Marketplace Event'),
|
||||||
SKU: {
|
PAY: modelEntry(() => paymentModel, 'payment', 'Payment'),
|
||||||
model: productSkuModel,
|
PPL: modelEntry(() => paymentPolicyModel, 'paymentPolicy', 'Payment Policy'),
|
||||||
idField: '_id',
|
FPL: modelEntry(() => fulfillmentPolicyModel, 'fulfillmentPolicy', 'Fulfillment Policy'),
|
||||||
type: 'productSku',
|
RPL: modelEntry(() => returnPolicyModel, 'returnPolicy', 'Return Policy'),
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Product SKU',
|
|
||||||
},
|
|
||||||
VEN: {
|
|
||||||
model: vendorModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'vendor',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Vendor',
|
|
||||||
},
|
|
||||||
MAT: {
|
|
||||||
model: materialModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'material',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Material',
|
|
||||||
},
|
|
||||||
SJB: {
|
|
||||||
model: subJobModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'subJob',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Sub Job',
|
|
||||||
},
|
|
||||||
FLS: {
|
|
||||||
model: filamentStockModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'filamentStock',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Filament Stock',
|
|
||||||
},
|
|
||||||
SEV: {
|
|
||||||
model: stockEventModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'stockEvent',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Stock Event',
|
|
||||||
},
|
|
||||||
SAU: {
|
|
||||||
model: stockAuditModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'stockAudit',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Stock Audit',
|
|
||||||
},
|
|
||||||
PTS: {
|
|
||||||
model: partStockModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'partStock',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Part Stock',
|
|
||||||
},
|
|
||||||
PDS: {
|
|
||||||
model: productStockModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'productStock',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Product Stock',
|
|
||||||
},
|
|
||||||
SLN: {
|
|
||||||
model: stockLocationModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'stockLocation',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Stock Location',
|
|
||||||
},
|
|
||||||
STT: {
|
|
||||||
model: stockTransferModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'stockTransfer',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Stock Transfer',
|
|
||||||
},
|
|
||||||
ADL: {
|
|
||||||
model: auditLogModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'auditLog',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Audit Log',
|
|
||||||
},
|
|
||||||
USR: {
|
|
||||||
model: userModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'user',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'User',
|
|
||||||
},
|
|
||||||
UGP: {
|
|
||||||
model: userGroupModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'userGroup',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'User Group',
|
|
||||||
},
|
|
||||||
PMS: {
|
|
||||||
model: permissionSettingModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'permissionSetting',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Permission Settings',
|
|
||||||
},
|
|
||||||
APP: {
|
|
||||||
model: appPasswordModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'appPassword',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'App Password',
|
|
||||||
},
|
|
||||||
NTY: {
|
|
||||||
model: noteTypeModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'noteType',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Note Type',
|
|
||||||
},
|
|
||||||
NTE: {
|
|
||||||
model: noteModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'note',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Note',
|
|
||||||
},
|
|
||||||
NTF: {
|
|
||||||
model: notificationModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'notification',
|
|
||||||
label: 'Notification',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
ONF: {
|
|
||||||
model: userNotifierModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'userNotifier',
|
|
||||||
label: 'User Notifier',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
DSZ: {
|
|
||||||
model: documentSizeModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'documentSize',
|
|
||||||
label: 'Document Size',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
DTP: {
|
|
||||||
model: documentTemplateModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'documentTemplate',
|
|
||||||
label: 'Document Template',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
DPR: {
|
|
||||||
model: documentPrinterModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'documentPrinter',
|
|
||||||
label: 'Document Printer',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
DJB: {
|
|
||||||
model: documentJobModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'documentJob',
|
|
||||||
label: 'Document Job',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
HST: {
|
|
||||||
model: hostModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'host',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Host',
|
|
||||||
},
|
|
||||||
FLE: {
|
|
||||||
model: fileModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'file',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'File',
|
|
||||||
},
|
|
||||||
POR: {
|
|
||||||
model: purchaseOrderModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'purchaseOrder',
|
|
||||||
label: 'Purchase Order',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
ODI: {
|
|
||||||
model: orderItemModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'orderItem',
|
|
||||||
label: 'Order Item',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
COS: {
|
|
||||||
model: courierServiceModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'courierService',
|
|
||||||
label: 'Courier Service',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
COR: {
|
|
||||||
model: courierModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'courier',
|
|
||||||
label: 'Courier',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
TXR: {
|
|
||||||
model: taxRateModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'taxRate',
|
|
||||||
label: 'Tax Rate',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
TXD: {
|
|
||||||
model: taxRecordModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'taxRecord',
|
|
||||||
label: 'Tax Record',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
SHP: {
|
|
||||||
model: shipmentModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'shipment',
|
|
||||||
label: 'Shipment',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
INV: {
|
|
||||||
model: invoiceModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'invoice',
|
|
||||||
label: 'Invoice',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
CLI: {
|
|
||||||
model: clientModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'client',
|
|
||||||
label: 'Client',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
SOR: {
|
|
||||||
model: salesOrderModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'salesOrder',
|
|
||||||
label: 'Sales Order',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
MKT: {
|
|
||||||
model: marketplaceModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'marketplace',
|
|
||||||
label: 'Marketplace',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
LST: {
|
|
||||||
model: listingModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'listing',
|
|
||||||
label: 'Listing',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
LVR: {
|
|
||||||
model: listingVarientModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'listingVarient',
|
|
||||||
label: 'Listing Varient',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
MKE: {
|
|
||||||
model: marketplaceEventModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'marketplaceEvent',
|
|
||||||
label: 'Marketplace Event',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
PAY: {
|
|
||||||
model: paymentModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'payment',
|
|
||||||
label: 'Payment',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
31
src/database/schemas/sales/fulfillmentpolicy.schema.js
Normal file
31
src/database/schemas/sales/fulfillmentpolicy.schema.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
import { marketplaceSyncMappingSchema } from './marketplaceMapping.schema.js';
|
||||||
|
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const fulfillmentPolicySchema = new Schema(
|
||||||
|
{
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
description: { type: String, required: false },
|
||||||
|
handlingTime: { type: Number, required: false, default: 1 },
|
||||||
|
localPickup: { type: Boolean, required: false, default: false },
|
||||||
|
globalShipping: { type: Boolean, required: false, default: false },
|
||||||
|
freightShipping: { type: Boolean, required: false, default: false },
|
||||||
|
pickupDropOff: { type: Boolean, required: false, default: false },
|
||||||
|
courierServices: [{ type: Schema.Types.ObjectId, ref: 'courierService', required: false }],
|
||||||
|
marketplaces: { type: [marketplaceSyncMappingSchema()], default: [] },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
fulfillmentPolicySchema.index({ name: 'text', description: 'text' });
|
||||||
|
|
||||||
|
fulfillmentPolicySchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
fulfillmentPolicySchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const fulfillmentPolicyModel = mongoose.model('fulfillmentPolicy', fulfillmentPolicySchema);
|
||||||
@ -1,6 +1,12 @@
|
|||||||
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';
|
import {
|
||||||
|
editObject,
|
||||||
|
newObject,
|
||||||
|
deleteObject,
|
||||||
|
aggregateRollups,
|
||||||
|
aggregateRollupsHistory,
|
||||||
|
} from '../../database.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const listingSchema = new Schema(
|
const listingSchema = new Schema(
|
||||||
@ -14,13 +20,24 @@ const listingSchema = new Schema(
|
|||||||
state: {
|
state: {
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ['draft', 'active', 'inactive', 'deleted', 'suspended', 'syncing'],
|
enum: [
|
||||||
|
'draft',
|
||||||
|
'active',
|
||||||
|
'inactive',
|
||||||
|
'deleted',
|
||||||
|
'suspended',
|
||||||
|
'syncing',
|
||||||
|
'publishing',
|
||||||
|
'unpublishing',
|
||||||
|
],
|
||||||
default: 'draft',
|
default: 'draft',
|
||||||
},
|
},
|
||||||
|
progress: { type: Number, required: false },
|
||||||
message: { type: String, required: false },
|
message: { type: String, required: false },
|
||||||
},
|
},
|
||||||
url: { type: String, required: false },
|
url: { type: String, required: false },
|
||||||
description: { type: String, required: false },
|
description: { type: String, required: false },
|
||||||
|
listingImages: [{ type: Schema.Types.ObjectId, ref: 'file', required: false }],
|
||||||
externalReference: { type: String, required: false },
|
externalReference: { type: String, required: false },
|
||||||
price: { type: Number, required: false },
|
price: { type: Number, required: false },
|
||||||
currency: { type: String, required: false },
|
currency: { type: String, required: false },
|
||||||
@ -51,6 +68,9 @@ const listingSchema = new Schema(
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
courierServices: [{ type: Schema.Types.ObjectId, ref: 'courierService', required: true }],
|
courierServices: [{ type: Schema.Types.ObjectId, ref: 'courierService', required: true }],
|
||||||
|
fulfillmentPolicy: { type: Schema.Types.ObjectId, ref: 'fulfillmentPolicy', required: false },
|
||||||
|
paymentPolicy: { type: Schema.Types.ObjectId, ref: 'paymentPolicy', required: false },
|
||||||
|
returnPolicy: { type: Schema.Types.ObjectId, ref: 'returnPolicy', required: false },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
@ -192,6 +212,16 @@ const rollupConfigs = [
|
|||||||
filter: { 'state.type': 'syncing' },
|
filter: { 'state.type': 'syncing' },
|
||||||
rollups: [{ name: 'syncing', property: 'state.type', operation: 'count' }],
|
rollups: [{ name: 'syncing', property: 'state.type', operation: 'count' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'publishing',
|
||||||
|
filter: { 'state.type': 'publishing' },
|
||||||
|
rollups: [{ name: 'publishing', property: 'state.type', operation: 'count' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unpublishing',
|
||||||
|
filter: { 'state.type': 'unpublishing' },
|
||||||
|
rollups: [{ name: 'unpublishing', property: 'state.type', operation: 'count' }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'suspended',
|
name: 'suspended',
|
||||||
filter: { 'state.type': 'suspended' },
|
filter: { 'state.type': 'suspended' },
|
||||||
|
|||||||
@ -9,16 +9,34 @@ const toId = (value) => {
|
|||||||
return String(value);
|
return String(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listingVarientAspectSchema = new Schema(
|
||||||
|
{
|
||||||
|
name: { type: String, required: true },
|
||||||
|
value: { type: String, required: true },
|
||||||
|
},
|
||||||
|
{ _id: true }
|
||||||
|
);
|
||||||
|
|
||||||
const listingVarientSchema = new Schema(
|
const listingVarientSchema = new Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
listing: { type: Schema.Types.ObjectId, ref: 'listing', required: true },
|
listing: { type: Schema.Types.ObjectId, ref: 'listing', required: true },
|
||||||
product: { type: Schema.Types.ObjectId, ref: 'product', required: false },
|
product: { type: Schema.Types.ObjectId, ref: 'product', required: false },
|
||||||
productSku: { type: Schema.Types.ObjectId, ref: 'productSku', required: false },
|
productSku: { type: Schema.Types.ObjectId, ref: 'productSku', required: false },
|
||||||
|
aspects: { type: [listingVarientAspectSchema], default: [] },
|
||||||
state: {
|
state: {
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ['draft', 'active', 'inactive', 'deleted', 'suspended', 'syncing'],
|
enum: [
|
||||||
|
'draft',
|
||||||
|
'active',
|
||||||
|
'inactive',
|
||||||
|
'deleted',
|
||||||
|
'suspended',
|
||||||
|
'syncing',
|
||||||
|
'publishing',
|
||||||
|
'unpublishing',
|
||||||
|
],
|
||||||
default: 'draft',
|
default: 'draft',
|
||||||
},
|
},
|
||||||
message: { type: String, required: false },
|
message: { type: String, required: false },
|
||||||
@ -29,6 +47,7 @@ const listingVarientSchema = new Schema(
|
|||||||
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
priceWithTax: { type: Number, required: false },
|
priceWithTax: { type: Number, required: false },
|
||||||
lastSyncedAt: { type: Date, required: false },
|
lastSyncedAt: { type: Date, required: false },
|
||||||
|
listingImages: [{ type: Schema.Types.ObjectId, ref: 'file', required: false }],
|
||||||
stockQuantity: { type: Number, required: false, default: 0 },
|
stockQuantity: { type: Number, required: false, default: 0 },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
|
|||||||
@ -24,8 +24,24 @@ const marketplaceSchema = new mongoose.Schema(
|
|||||||
},
|
},
|
||||||
// Provider-specific API configuration (flexible for eBay, Etsy, TikTok Shop)
|
// Provider-specific API configuration (flexible for eBay, Etsy, TikTok Shop)
|
||||||
config: { type: mongoose.Schema.Types.Mixed, default: {} },
|
config: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
|
defaultFulfillmentPolicy: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'fulfillmentPolicy',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
defaultPaymentPolicy: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'paymentPolicy',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
defaultReturnPolicy: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'returnPolicy',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
eBay: {
|
eBay: {
|
||||||
availableShippingServices: { type: [String], default: [] },
|
availableShippingServices: { type: [String], default: [] },
|
||||||
|
categoryReferences: { type: [mongoose.Schema.Types.Mixed], default: [] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
|
|||||||
23
src/database/schemas/sales/marketplaceMapping.schema.js
Normal file
23
src/database/schemas/sales/marketplaceMapping.schema.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
export const MARKETPLACE_MAPPING_STATES = ['pending', 'syncing', 'ready', 'failed'];
|
||||||
|
|
||||||
|
export function marketplaceSyncMappingSchema() {
|
||||||
|
return new Schema(
|
||||||
|
{
|
||||||
|
marketplace: { type: Schema.Types.ObjectId, ref: 'marketplace', required: true },
|
||||||
|
externalReference: { type: String, required: false },
|
||||||
|
state: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: MARKETPLACE_MAPPING_STATES,
|
||||||
|
default: 'pending',
|
||||||
|
},
|
||||||
|
message: { type: String, required: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/database/schemas/sales/returnpolicy.schema.js
Normal file
46
src/database/schemas/sales/returnpolicy.schema.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
import { marketplaceSyncMappingSchema } from './marketplaceMapping.schema.js';
|
||||||
|
|
||||||
|
const returnPolicySchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
description: { type: String, required: false },
|
||||||
|
returnsAccepted: { type: Boolean, required: true, default: true },
|
||||||
|
returnPeriodDays: { type: Number, required: false, default: 30 },
|
||||||
|
returnShippingCostPayer: {
|
||||||
|
type: String,
|
||||||
|
enum: ['buyer', 'seller'],
|
||||||
|
required: false,
|
||||||
|
default: 'buyer',
|
||||||
|
},
|
||||||
|
refundMethod: {
|
||||||
|
type: String,
|
||||||
|
enum: ['moneyBack', 'merchandiseCredit'],
|
||||||
|
required: false,
|
||||||
|
default: 'moneyBack',
|
||||||
|
},
|
||||||
|
restockingFeePercentage: { type: Number, required: false },
|
||||||
|
returnInstructions: { type: String, required: false },
|
||||||
|
internationalReturnsAccepted: { type: Boolean, required: false },
|
||||||
|
internationalReturnPeriodDays: { type: Number, required: false },
|
||||||
|
internationalReturnShippingCostPayer: {
|
||||||
|
type: String,
|
||||||
|
enum: ['buyer', 'seller'],
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
marketplaces: { type: [marketplaceSyncMappingSchema()], default: [] },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
returnPolicySchema.index({ name: 'text', description: 'text', returnInstructions: 'text' });
|
||||||
|
|
||||||
|
returnPolicySchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
returnPolicySchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const returnPolicyModel = mongoose.model('returnPolicy', returnPolicySchema);
|
||||||
13
src/index.js
13
src/index.js
@ -56,6 +56,9 @@ import {
|
|||||||
marketplaceRoutes,
|
marketplaceRoutes,
|
||||||
listingRoutes,
|
listingRoutes,
|
||||||
listingVarientRoutes,
|
listingVarientRoutes,
|
||||||
|
fulfillmentPolicyRoutes,
|
||||||
|
returnPolicyRoutes,
|
||||||
|
paymentPolicyRoutes,
|
||||||
userNotifierRoutes,
|
userNotifierRoutes,
|
||||||
notificationRoutes,
|
notificationRoutes,
|
||||||
odataRoutes,
|
odataRoutes,
|
||||||
@ -134,6 +137,13 @@ async function initializeApp() {
|
|||||||
logger.error('Failed to start persistent Puppeteer browser:', err);
|
logger.error('Failed to start persistent Puppeteer browser:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { startMarketplaceWorker } = await import('./integrations/marketplace.js');
|
||||||
|
await startMarketplaceWorker();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to start marketplace worker:', err);
|
||||||
|
}
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, () => logger.info(`Server listening to port ${PORT}`));
|
app.listen(PORT, () => logger.info(`Server listening to port ${PORT}`));
|
||||||
logger.info(`Allowed origins: ${allowedOrigins.join(', ')}`);
|
logger.info(`Allowed origins: ${allowedOrigins.join(', ')}`);
|
||||||
@ -213,6 +223,9 @@ app.use('/salesorders', salesOrderRoutes);
|
|||||||
app.use('/marketplaces', marketplaceRoutes);
|
app.use('/marketplaces', marketplaceRoutes);
|
||||||
app.use('/listings', listingRoutes);
|
app.use('/listings', listingRoutes);
|
||||||
app.use('/listingvarients', listingVarientRoutes);
|
app.use('/listingvarients', listingVarientRoutes);
|
||||||
|
app.use('/fulfillmentpolicies', fulfillmentPolicyRoutes);
|
||||||
|
app.use('/returnpolicies', returnPolicyRoutes);
|
||||||
|
app.use('/paymentpolicies', paymentPolicyRoutes);
|
||||||
app.use('/notes', noteRoutes);
|
app.use('/notes', noteRoutes);
|
||||||
app.use('/usernotifiers', userNotifierRoutes);
|
app.use('/usernotifiers', userNotifierRoutes);
|
||||||
app.use('/notifications', notificationRoutes);
|
app.use('/notifications', notificationRoutes);
|
||||||
|
|||||||
69
src/integrations/__tests__/marketplace.test.js
Normal file
69
src/integrations/__tests__/marketplace.test.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { expect, jest } from '@jest/globals';
|
||||||
|
import { hasIntegration, MARKETPLACE_BUSY_STATES, canAuthorize, syncMappedEbayPolicies } from '../marketplace.js';
|
||||||
|
|
||||||
|
describe('marketplace client', () => {
|
||||||
|
it('reports integrated providers without talking to the worker', () => {
|
||||||
|
expect(hasIntegration('ebay')).toBe(true);
|
||||||
|
expect(hasIntegration('tiktokShop')).toBe(true);
|
||||||
|
expect(hasIntegration('etsy')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats integrated providers as authorizable', () => {
|
||||||
|
expect(canAuthorize({ provider: 'ebay' })).toBe(true);
|
||||||
|
expect(canAuthorize({ provider: 'etsy' })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports the busy states used by publish/unpublish/sync', () => {
|
||||||
|
expect(MARKETPLACE_BUSY_STATES).toEqual(['syncing', 'publishing', 'unpublishing']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('syncMappedEbayPolicies', () => {
|
||||||
|
it('syncs each eBay mapping and skips other providers', async () => {
|
||||||
|
const syncFn = jest.fn().mockResolvedValue({});
|
||||||
|
const ebayMarketplace = { _id: 'mp-ebay', provider: 'ebay', name: 'eBay GB' };
|
||||||
|
const etsyMarketplace = { _id: 'mp-etsy', provider: 'etsy', name: 'Etsy' };
|
||||||
|
const policy = {
|
||||||
|
_id: 'ppl-1',
|
||||||
|
marketplaces: [
|
||||||
|
{ marketplace: ebayMarketplace },
|
||||||
|
{ marketplace: etsyMarketplace },
|
||||||
|
{ marketplace: 'mp-id-only' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(syncMappedEbayPolicies(policy, { _id: 'user-1' }, syncFn)).resolves.toBe(1);
|
||||||
|
expect(syncFn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(syncFn).toHaveBeenCalledWith(ebayMarketplace, { _id: 'user-1' }, policy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs marketplace stubs that omit provider', async () => {
|
||||||
|
const syncFn = jest.fn().mockResolvedValue({});
|
||||||
|
const policy = {
|
||||||
|
_id: 'ppl-1',
|
||||||
|
marketplaces: [{ marketplace: { _id: 'mp-ebay', name: 'eBay GB' } }],
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(syncMappedEbayPolicies(policy, { _id: 'user-1' }, syncFn)).resolves.toBe(1);
|
||||||
|
expect(syncFn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('continues when one marketplace sync fails', async () => {
|
||||||
|
const syncFn = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(new Error('eBay timeout'))
|
||||||
|
.mockResolvedValueOnce({});
|
||||||
|
const logger = { error: jest.fn() };
|
||||||
|
const policy = {
|
||||||
|
_id: 'ppl-1',
|
||||||
|
marketplaces: [
|
||||||
|
{ marketplace: { _id: 'mp-1', provider: 'ebay', name: 'One' } },
|
||||||
|
{ marketplace: { _id: 'mp-2', provider: 'ebay', name: 'Two' } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(syncMappedEbayPolicies(policy, {}, syncFn, logger)).resolves.toBe(2);
|
||||||
|
expect(syncFn).toHaveBeenCalledTimes(2);
|
||||||
|
expect(logger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
481
src/integrations/marketplace.js
Normal file
481
src/integrations/marketplace.js
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
import { fork } from 'child_process';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import config from '../config.js';
|
||||||
|
import { marketplaceSku, marketplaceActor } from './marketplaces/ids.js';
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('Marketplace');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
const INTEGRATED_PROVIDERS = new Set(['ebay', 'tiktokShop']);
|
||||||
|
const WORKER_PATH = fileURLToPath(new URL('./marketplaceworker.js', import.meta.url));
|
||||||
|
const DEFAULT_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
|
export const MARKETPLACE_BUSY_STATES = ['syncing', 'publishing', 'unpublishing'];
|
||||||
|
|
||||||
|
let workerProcess = null;
|
||||||
|
let workerReady = null;
|
||||||
|
let shuttingDown = false;
|
||||||
|
const pending = new Map();
|
||||||
|
|
||||||
|
export function hasIntegration(provider) {
|
||||||
|
return INTEGRATED_PROVIDERS.has(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
function idOf(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (value._id) return String(value._id);
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeListing(listing) {
|
||||||
|
if (!listing) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(listing));
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
_id: idOf(listing),
|
||||||
|
_reference: listing._reference,
|
||||||
|
externalReference: listing.externalReference,
|
||||||
|
marketplace: idOf(listing.marketplace),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inTestProcess() {
|
||||||
|
return process.env.NODE_ENV === 'test' || process.env.MARKETPLACE_WORKER === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectPending(error) {
|
||||||
|
for (const [id, waiter] of pending) {
|
||||||
|
clearTimeout(waiter.timer);
|
||||||
|
waiter.reject(error);
|
||||||
|
pending.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachWorkerHandlers(child) {
|
||||||
|
child.on('message', (message) => {
|
||||||
|
if (!message) return;
|
||||||
|
if (message.type === 'ready') return;
|
||||||
|
const waiter = pending.get(message.id);
|
||||||
|
if (!waiter) return;
|
||||||
|
clearTimeout(waiter.timer);
|
||||||
|
pending.delete(message.id);
|
||||||
|
if (message.ok) waiter.resolve(message.result);
|
||||||
|
else waiter.reject(new Error(message.error || 'Marketplace worker job failed'));
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
logger.error('Marketplace worker error:', err);
|
||||||
|
rejectPending(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code) => {
|
||||||
|
logger.warn(`Marketplace worker exited with code ${code}`);
|
||||||
|
workerProcess = null;
|
||||||
|
workerReady = null;
|
||||||
|
rejectPending(new Error('Marketplace worker exited'));
|
||||||
|
if (!shuttingDown && process.env.NODE_ENV !== 'test') {
|
||||||
|
setTimeout(() => {
|
||||||
|
startMarketplaceWorker().catch((err) => {
|
||||||
|
logger.error('Failed to restart marketplace worker:', err.message);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function forkWorker() {
|
||||||
|
const child = fork(WORKER_PATH, [], {
|
||||||
|
env: { ...process.env, MARKETPLACE_WORKER: '1' },
|
||||||
|
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
||||||
|
serialization: 'json',
|
||||||
|
});
|
||||||
|
attachWorkerHandlers(child);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForReady(child, timeoutMs = 30000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
child.off('message', onMessage);
|
||||||
|
reject(new Error('Marketplace worker failed to become ready'));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
function onMessage(message) {
|
||||||
|
if (message?.type !== 'ready') return;
|
||||||
|
clearTimeout(timer);
|
||||||
|
child.off('message', onMessage);
|
||||||
|
resolve(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
child.on('message', onMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startMarketplaceWorker() {
|
||||||
|
if (inTestProcess()) return null;
|
||||||
|
if (workerProcess && workerReady) return workerReady;
|
||||||
|
|
||||||
|
shuttingDown = false;
|
||||||
|
workerProcess = forkWorker();
|
||||||
|
workerReady = waitForReady(workerProcess).catch((err) => {
|
||||||
|
workerProcess = null;
|
||||||
|
workerReady = null;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
return workerReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopMarketplaceWorker() {
|
||||||
|
shuttingDown = true;
|
||||||
|
rejectPending(new Error('Marketplace worker stopped'));
|
||||||
|
if (workerProcess) {
|
||||||
|
workerProcess.kill();
|
||||||
|
workerProcess = null;
|
||||||
|
}
|
||||||
|
workerReady = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWorker() {
|
||||||
|
if (inTestProcess()) return null;
|
||||||
|
if (!workerProcess || !workerReady) {
|
||||||
|
await startMarketplaceWorker();
|
||||||
|
}
|
||||||
|
await workerReady;
|
||||||
|
if (!workerProcess) {
|
||||||
|
throw new Error('Marketplace worker is not running');
|
||||||
|
}
|
||||||
|
return workerProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runInProcess(action, payload) {
|
||||||
|
const { runJob } = await import('./marketplaceworker.js');
|
||||||
|
return runJob(action, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendJob(
|
||||||
|
action,
|
||||||
|
payload,
|
||||||
|
{ wait = false, timeout = DEFAULT_TIMEOUT_MS, inProcess = false } = {}
|
||||||
|
) {
|
||||||
|
if (inTestProcess() || inProcess) {
|
||||||
|
const result = runInProcess(action, payload);
|
||||||
|
if (wait) return result;
|
||||||
|
result.catch((err) => {
|
||||||
|
logger.warn(`In-process marketplace job "${action}" failed: ${err.message}`);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = await getWorker();
|
||||||
|
const id = randomUUID();
|
||||||
|
if (!wait) {
|
||||||
|
worker.send({ id, action, payload, wait: false });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
pending.delete(id);
|
||||||
|
reject(new Error(`Marketplace worker timed out waiting for ${action}`));
|
||||||
|
}, timeout);
|
||||||
|
pending.set(id, { resolve, reject, timer });
|
||||||
|
worker.send({ id, action, payload, wait: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function marketplacePayload(marketplace, extra = {}) {
|
||||||
|
return {
|
||||||
|
marketplaceId: idOf(marketplace),
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function userPayload(user) {
|
||||||
|
return { userId: idOf(user) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canAuthorize(marketplace) {
|
||||||
|
return hasIntegration(marketplace?.provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canVerifyWebhookSignature(marketplace) {
|
||||||
|
return hasIntegration(marketplace?.provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createListing(marketplace, user, listingData) {
|
||||||
|
return sendJob('createListing', {
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
listingId: idOf(listingData),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateListing(marketplace, user, listingData) {
|
||||||
|
return sendJob('updateListing', {
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
listingId: idOf(listingData),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteListing(marketplace, user, listingData) {
|
||||||
|
return sendJob('deleteListing', {
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
listing: serializeListing(listingData),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function publishListing(
|
||||||
|
marketplace,
|
||||||
|
user,
|
||||||
|
listing,
|
||||||
|
{ varientIds, restoreStateType, varientRestoreStateType } = {}
|
||||||
|
) {
|
||||||
|
return sendJob('publishListing', {
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
listingId: idOf(listing),
|
||||||
|
varientIds: (varientIds || []).map((id) => String(id)),
|
||||||
|
restoreStateType,
|
||||||
|
varientRestoreStateType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unpublishListing(
|
||||||
|
marketplace,
|
||||||
|
user,
|
||||||
|
listing,
|
||||||
|
{ varientIds, restoreStateType, varientRestoreStateType } = {}
|
||||||
|
) {
|
||||||
|
return sendJob('unpublishListing', {
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
listingId: idOf(listing),
|
||||||
|
varientIds: (varientIds || []).map((id) => String(id)),
|
||||||
|
restoreStateType,
|
||||||
|
varientRestoreStateType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncItems(marketplace, user) {
|
||||||
|
return sendJob('syncItems', {
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncOrders(marketplace, user, { startTime, endTime } = {}) {
|
||||||
|
return sendJob('syncOrders', {
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncMarketplaceMetadata(marketplace, user) {
|
||||||
|
return sendJob(
|
||||||
|
'syncMarketplaceMetadata',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
},
|
||||||
|
{ wait: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAccountPolicyJob(action, marketplace, user) {
|
||||||
|
return sendJob(
|
||||||
|
action,
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
},
|
||||||
|
{ wait: false, inProcess: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncFulfillmentPolicies(marketplace, user) {
|
||||||
|
return syncAccountPolicyJob('syncFulfillmentPolicies', marketplace, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncPaymentPolicies(marketplace, user) {
|
||||||
|
return syncAccountPolicyJob('syncPaymentPolicies', marketplace, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncReturnPolicies(marketplace, user) {
|
||||||
|
return syncAccountPolicyJob('syncReturnPolicies', marketplace, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncTaxRates(marketplace, user) {
|
||||||
|
return syncAccountPolicyJob('syncTaxRates', marketplace, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncOutboundPolicyJob(action, marketplace, user, policy) {
|
||||||
|
return sendJob(
|
||||||
|
action,
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
policyId: idOf(policy),
|
||||||
|
},
|
||||||
|
{ wait: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncPaymentPolicy(marketplace, user, policy) {
|
||||||
|
return syncOutboundPolicyJob('syncPaymentPolicy', marketplace, user, policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncReturnPolicy(marketplace, user, policy) {
|
||||||
|
return syncOutboundPolicyJob('syncReturnPolicy', marketplace, user, policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncFulfillmentPolicy(marketplace, user, policy) {
|
||||||
|
return syncOutboundPolicyJob('syncFulfillmentPolicy', marketplace, user, policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncTaxRate(marketplace, user, policy) {
|
||||||
|
return syncOutboundPolicyJob('syncTaxRate', marketplace, user, policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncMappedEbayPolicies(policy, user, syncFn, logger) {
|
||||||
|
const mappings = Array.isArray(policy?.marketplaces) ? policy.marketplaces : [];
|
||||||
|
let attempted = 0;
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
const marketplace = mapping?.marketplace;
|
||||||
|
if (!marketplace || typeof marketplace !== 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (marketplace.provider && marketplace.provider !== 'ebay') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attempted += 1;
|
||||||
|
try {
|
||||||
|
await syncFn(marketplace, user, policy);
|
||||||
|
} catch (err) {
|
||||||
|
logger?.error?.(
|
||||||
|
`Failed to sync ${policy._reference || policy._id} to marketplace ${
|
||||||
|
marketplace.name || marketplace._id
|
||||||
|
}: ${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attempted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureWebhookSubscriptions(marketplace, user, { wait = false } = {}) {
|
||||||
|
return sendJob(
|
||||||
|
'ensureWebhookSubscriptions',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
},
|
||||||
|
{ wait }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushMarketplaceShipmentFulfillment(marketplace, user, shipment) {
|
||||||
|
return sendJob('pushShipmentFulfillment', {
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
shipmentId: idOf(shipment),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthorizationUrl(marketplace, { state } = {}) {
|
||||||
|
return sendJob(
|
||||||
|
'getAuthorizationUrl',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
state,
|
||||||
|
},
|
||||||
|
{ wait: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exchangeAuthorizationCode(marketplace, user, { code, state } = {}) {
|
||||||
|
return sendJob(
|
||||||
|
'exchangeAuthorizationCode',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
code,
|
||||||
|
state,
|
||||||
|
},
|
||||||
|
{ wait: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshMarketplaceAuth(marketplace, user) {
|
||||||
|
return sendJob(
|
||||||
|
'refreshMarketplaceAuth',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
},
|
||||||
|
{ wait: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleWebhook(marketplace, event, { rawBody, signature } = {}) {
|
||||||
|
return sendJob(
|
||||||
|
'handleWebhook',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
event,
|
||||||
|
rawBody,
|
||||||
|
signature,
|
||||||
|
},
|
||||||
|
{ wait: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWebhookChallengeResponse(marketplace, query) {
|
||||||
|
return sendJob(
|
||||||
|
'buildWebhookChallengeResponse',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
{ wait: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyWebhookSignature(marketplace, rawBody, signature) {
|
||||||
|
return sendJob(
|
||||||
|
'verifyWebhookSignature',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
rawBody,
|
||||||
|
signature,
|
||||||
|
},
|
||||||
|
{ wait: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debugMarketplaceGet(marketplace, user, path, params = {}) {
|
||||||
|
return sendJob(
|
||||||
|
'debugMarketplaceGet',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
{ wait: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
shuttingDown = true;
|
||||||
|
if (workerProcess) workerProcess.kill();
|
||||||
|
});
|
||||||
|
|
||||||
|
export { marketplaceSku, marketplaceActor };
|
||||||
@ -303,6 +303,7 @@ async function upsertInboundListing(marketplace, mapped, actor) {
|
|||||||
lastSyncedAt: new Date(),
|
lastSyncedAt: new Date(),
|
||||||
product: product?._id || listing.product,
|
product: product?._id || listing.product,
|
||||||
productSku: productSku?._id,
|
productSku: productSku?._id,
|
||||||
|
...(Array.isArray(mappedVarient.aspects) ? { aspects: mappedVarient.aspects } : {}),
|
||||||
};
|
};
|
||||||
const existingVarient = await listingVarientModel.findOne({
|
const existingVarient = await listingVarientModel.findOne({
|
||||||
listing: listing._id,
|
listing: listing._id,
|
||||||
@ -335,7 +336,7 @@ export async function importExternalItems(marketplace, provider, actor) {
|
|||||||
const results = [];
|
const results = [];
|
||||||
for (const item of externalItems || []) {
|
for (const item of externalItems || []) {
|
||||||
try {
|
try {
|
||||||
const mapped = provider.mapProductToListing(item);
|
const mapped = provider.mapProductToListing(item, marketplace);
|
||||||
const result = await upsertInboundListing(marketplace, mapped, actor);
|
const result = await upsertInboundListing(marketplace, mapped, actor);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -0,0 +1,283 @@
|
|||||||
|
import { describe, expect, it, jest } from '@jest/globals';
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../shared.js', () => ({
|
||||||
|
makeRequest: jest.fn(),
|
||||||
|
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
||||||
|
getEbayMarketplaceId: (marketplace) => marketplace?.config?.marketplaceId || 'EBAY_GB',
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../../database/database.js', () => ({
|
||||||
|
deleteObjectCache: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../../utils.js', () => ({
|
||||||
|
distributeUpdate: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { upsertLocalPolicyFromRemote, resolveListingPolicy, persistMarketplaceMapping, idOf, updateAccountPolicy } =
|
||||||
|
await import('../accountPolicies.js');
|
||||||
|
const { makeRequest } = await import('../shared.js');
|
||||||
|
const { buildPaymentPolicy } = await import('../paymentPolicies.js');
|
||||||
|
const { buildReturnPolicy } = await import('../returnPolicies.js');
|
||||||
|
const {
|
||||||
|
isEbayTaxTableSupported,
|
||||||
|
taxExternalReference,
|
||||||
|
buildSalesTaxEntry,
|
||||||
|
} = await import('../salesTax.js');
|
||||||
|
|
||||||
|
const marketplace = { _id: 'mp1', config: { marketplaceId: 'EBAY_GB' } };
|
||||||
|
|
||||||
|
function createModelMock({ existing = null } = {}) {
|
||||||
|
const leanQuery = (result) => ({
|
||||||
|
lean: jest.fn().mockResolvedValue(result),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
findOne: jest.fn().mockReturnValue(leanQuery(existing)),
|
||||||
|
findById: jest.fn().mockReturnValue(leanQuery(existing)),
|
||||||
|
updateOne: jest.fn().mockResolvedValue({}),
|
||||||
|
create: jest.fn().mockImplementation(async (doc) => ({
|
||||||
|
...doc,
|
||||||
|
_id: 'new-id',
|
||||||
|
toObject() {
|
||||||
|
return { ...doc, _id: 'new-id' };
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('eBay payment and return policy payloads', () => {
|
||||||
|
it('builds a payment policy with ALL_EXCLUDING_MOTORS_VEHICLES and no paymentMethods', () => {
|
||||||
|
const payload = buildPaymentPolicy(
|
||||||
|
{
|
||||||
|
name: 'Immediate Pay',
|
||||||
|
description: 'Managed payments',
|
||||||
|
immediatePay: true,
|
||||||
|
paymentInstructions: 'Pay now',
|
||||||
|
},
|
||||||
|
marketplace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload).toEqual({
|
||||||
|
name: 'Immediate Pay',
|
||||||
|
marketplaceId: 'EBAY_GB',
|
||||||
|
categoryTypes: [{ name: 'ALL_EXCLUDING_MOTORS_VEHICLES' }],
|
||||||
|
immediatePay: true,
|
||||||
|
description: 'Managed payments',
|
||||||
|
paymentInstructions: 'Pay now',
|
||||||
|
});
|
||||||
|
expect(payload.paymentMethods).toBeUndefined();
|
||||||
|
expect(payload.categoryTypes[0].default).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps return periods and payers for eBay', () => {
|
||||||
|
const payload = buildReturnPolicy(
|
||||||
|
{
|
||||||
|
name: '30 Day Returns',
|
||||||
|
returnsAccepted: true,
|
||||||
|
returnPeriodDays: 30,
|
||||||
|
returnShippingCostPayer: 'seller',
|
||||||
|
refundMethod: 'moneyBack',
|
||||||
|
internationalReturnsAccepted: true,
|
||||||
|
internationalReturnPeriodDays: 14,
|
||||||
|
internationalReturnShippingCostPayer: 'buyer',
|
||||||
|
},
|
||||||
|
marketplace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload.categoryTypes).toEqual([{ name: 'ALL_EXCLUDING_MOTORS_VEHICLES' }]);
|
||||||
|
expect(payload.returnsAccepted).toBe(true);
|
||||||
|
expect(payload.returnPeriod).toEqual({ value: 30, unit: 'DAY' });
|
||||||
|
expect(payload.returnShippingCostPayer).toBe('SELLER');
|
||||||
|
expect(payload.refundMethod).toBe('MONEY_BACK');
|
||||||
|
expect(payload.internationalOverride).toEqual({
|
||||||
|
returnsAccepted: true,
|
||||||
|
returnPeriod: { value: 14, unit: 'DAY' },
|
||||||
|
returnShippingCostPayer: 'BUYER',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('snaps unsupported return periods to 14, 30, or 60 days and omits restocking fees', () => {
|
||||||
|
const payload = buildReturnPolicy(
|
||||||
|
{
|
||||||
|
name: 'Test',
|
||||||
|
returnsAccepted: true,
|
||||||
|
returnPeriodDays: 20,
|
||||||
|
restockingFeePercentage: 0,
|
||||||
|
internationalReturnsAccepted: false,
|
||||||
|
},
|
||||||
|
marketplace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload.returnPeriod).toEqual({ value: 30, unit: 'DAY' });
|
||||||
|
expect(payload.restockingFeePercentage).toBeUndefined();
|
||||||
|
expect(payload.internationalOverride).toEqual({ returnsAccepted: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('eBay tax table jurisdictions', () => {
|
||||||
|
it('allows US territories and Canada, and skips US states', () => {
|
||||||
|
expect(isEbayTaxTableSupported({ country: 'US', jurisdiction: 'GU' })).toBe(true);
|
||||||
|
expect(isEbayTaxTableSupported({ country: 'US', jurisdiction: 'VI' })).toBe(true);
|
||||||
|
expect(isEbayTaxTableSupported({ country: 'CA', jurisdiction: 'ON' })).toBe(true);
|
||||||
|
expect(isEbayTaxTableSupported({ country: 'US', jurisdiction: 'CA' })).toBe(false);
|
||||||
|
expect(isEbayTaxTableSupported({ country: 'US', jurisdiction: 'NY' })).toBe(false);
|
||||||
|
expect(isEbayTaxTableSupported({ country: 'GB', jurisdiction: 'ENG' })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds COUNTRY:JURISDICTION external references', () => {
|
||||||
|
expect(taxExternalReference('US', 'GU')).toBe('US:GU');
|
||||||
|
expect(buildSalesTaxEntry({ country: 'us', jurisdiction: 'gu', rate: 4, shippingAndHandlingTaxed: true })).toEqual({
|
||||||
|
countryCode: 'US',
|
||||||
|
jurisdictionId: 'GU',
|
||||||
|
salesTaxPercentage: '4',
|
||||||
|
shippingAndHandlingTaxed: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('inbound policy upsert', () => {
|
||||||
|
it('creates a local policy with externalReference and ready state', async () => {
|
||||||
|
const model = createModelMock();
|
||||||
|
const created = await upsertLocalPolicyFromRemote({
|
||||||
|
model,
|
||||||
|
marketplace,
|
||||||
|
remoteId: 'pay-99',
|
||||||
|
name: 'Immediate Pay',
|
||||||
|
fields: { immediatePay: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.create).toHaveBeenCalledWith({
|
||||||
|
immediatePay: true,
|
||||||
|
name: 'Immediate Pay',
|
||||||
|
marketplaces: [
|
||||||
|
{
|
||||||
|
marketplace: 'mp1',
|
||||||
|
externalReference: 'pay-99',
|
||||||
|
state: { type: 'ready' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(created.marketplaces[0].externalReference).toBe('pay-99');
|
||||||
|
expect(created.marketplaces[0].state.type).toBe('ready');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates an existing policy mapping to ready', async () => {
|
||||||
|
const existing = {
|
||||||
|
_id: 'local-1',
|
||||||
|
name: 'Immediate Pay',
|
||||||
|
marketplaces: [{ marketplace: 'mp1', externalReference: '', state: { type: 'pending' } }],
|
||||||
|
};
|
||||||
|
const model = createModelMock({ existing });
|
||||||
|
await upsertLocalPolicyFromRemote({
|
||||||
|
model,
|
||||||
|
marketplace,
|
||||||
|
remoteId: 'pay-99',
|
||||||
|
name: 'Immediate Pay',
|
||||||
|
fields: { immediatePay: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.updateOne).toHaveBeenCalledWith(
|
||||||
|
{ _id: 'local-1' },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
immediatePay: true,
|
||||||
|
name: 'Immediate Pay',
|
||||||
|
marketplaces: [
|
||||||
|
expect.objectContaining({
|
||||||
|
marketplace: 'mp1',
|
||||||
|
externalReference: 'pay-99',
|
||||||
|
state: { type: 'ready' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('persistMarketplaceMapping', () => {
|
||||||
|
it('matches mappings stored as raw ObjectIds', async () => {
|
||||||
|
const marketplaceId = {
|
||||||
|
toString() {
|
||||||
|
return '507f1f77bcf86cd799439011';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const existing = {
|
||||||
|
_id: 'ppl-1',
|
||||||
|
marketplaces: [
|
||||||
|
{
|
||||||
|
_id: 'map-1',
|
||||||
|
marketplace: marketplaceId,
|
||||||
|
state: { type: 'pending' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const model = {
|
||||||
|
modelName: 'paymentPolicy',
|
||||||
|
findById: jest.fn().mockReturnValue({
|
||||||
|
lean: jest.fn().mockResolvedValue(existing),
|
||||||
|
populate: jest.fn().mockReturnValue({
|
||||||
|
lean: jest.fn().mockResolvedValue(existing),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
updateOne: jest.fn().mockResolvedValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(idOf(marketplaceId)).toBe('507f1f77bcf86cd799439011');
|
||||||
|
|
||||||
|
await persistMarketplaceMapping(
|
||||||
|
model,
|
||||||
|
existing,
|
||||||
|
{ _id: '507f1f77bcf86cd799439011', provider: 'ebay' },
|
||||||
|
{ stateType: 'syncing' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(model.updateOne).toHaveBeenCalledTimes(1);
|
||||||
|
const mappings = model.updateOne.mock.calls[0][1].$set.marketplaces;
|
||||||
|
expect(mappings).toHaveLength(1);
|
||||||
|
expect(mappings[0].state.type).toBe('syncing');
|
||||||
|
expect(idOf(mappings[0].marketplace)).toBe('507f1f77bcf86cd799439011');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateAccountPolicy', () => {
|
||||||
|
it('treats an unchanged eBay policy as already synced', async () => {
|
||||||
|
makeRequest.mockRejectedValueOnce(
|
||||||
|
new Error(
|
||||||
|
'eBay API error (400): Business profile information in the request is the same as in the system'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
updateAccountPolicy(marketplace, '/sell/account/v1/payment_policy/6246724000', {
|
||||||
|
name: 'eBay Managed Payments',
|
||||||
|
categoryTypes: [{ name: 'ALL_EXCLUDING_MOTORS_VEHICLES', default: true }],
|
||||||
|
immediatePay: true,
|
||||||
|
})
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listing policy resolution', () => {
|
||||||
|
it('prefers the listing policy over marketplace defaults', () => {
|
||||||
|
expect(
|
||||||
|
resolveListingPolicy(
|
||||||
|
{ paymentPolicy: { _id: 'listing-pay' }, marketplace: { defaultPaymentPolicy: { _id: 'default-pay' } } },
|
||||||
|
{ defaultPaymentPolicy: { _id: 'default-pay' } },
|
||||||
|
'paymentPolicy',
|
||||||
|
'defaultPaymentPolicy'
|
||||||
|
)
|
||||||
|
).toEqual({ _id: 'listing-pay' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to marketplace defaults', () => {
|
||||||
|
expect(
|
||||||
|
resolveListingPolicy(
|
||||||
|
{ marketplace: { defaultReturnPolicy: { _id: 'default-ret' } } },
|
||||||
|
{ defaultReturnPolicy: { _id: 'default-ret' } },
|
||||||
|
'returnPolicy',
|
||||||
|
'defaultReturnPolicy'
|
||||||
|
)
|
||||||
|
).toEqual({ _id: 'default-ret' });
|
||||||
|
});
|
||||||
|
});
|
||||||
128
src/integrations/marketplaces/ebay/__tests__/categories.test.js
Normal file
128
src/integrations/marketplaces/ebay/__tests__/categories.test.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../../database/schemas/management/product.schema.js', () => ({
|
||||||
|
productModel: { findById: jest.fn() },
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../../../../database/schemas/management/productcategory.schema.js', () => ({
|
||||||
|
productCategoryModel: { findById: jest.fn() },
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../shared.js', () => ({
|
||||||
|
makeRequest: jest.fn(),
|
||||||
|
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { productModel } = await import('../../../../database/schemas/management/product.schema.js');
|
||||||
|
const { productCategoryModel } = await import(
|
||||||
|
'../../../../database/schemas/management/productcategory.schema.js'
|
||||||
|
);
|
||||||
|
const { makeRequest } = await import('../shared.js');
|
||||||
|
const { syncProductCategory } = await import('../categories.js');
|
||||||
|
|
||||||
|
const marketplace = { _id: 'mp1', config: { marketplaceId: 'EBAY_GB' } };
|
||||||
|
|
||||||
|
function listingWithCategory(productCategory) {
|
||||||
|
return {
|
||||||
|
_reference: 'LST-1',
|
||||||
|
product: {
|
||||||
|
_id: 'prod-1',
|
||||||
|
productCategory,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('eBay product category mapping', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the marketplace mapping externalReference as the eBay categoryId', async () => {
|
||||||
|
const category = await syncProductCategory(
|
||||||
|
marketplace,
|
||||||
|
listingWithCategory({
|
||||||
|
_id: 'cat-1',
|
||||||
|
name: 'Seeds',
|
||||||
|
marketplaces: [
|
||||||
|
{
|
||||||
|
marketplace: { _id: 'mp1' },
|
||||||
|
externalReference: '12345',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(category).toEqual({ categoryId: '12345' });
|
||||||
|
expect(makeRequest).not.toHaveBeenCalled();
|
||||||
|
expect(productModel.findById).not.toHaveBeenCalled();
|
||||||
|
expect(productCategoryModel.findById).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the product category has no mapping for the listing marketplace', async () => {
|
||||||
|
await expect(
|
||||||
|
syncProductCategory(
|
||||||
|
marketplace,
|
||||||
|
listingWithCategory({
|
||||||
|
_id: 'cat-1',
|
||||||
|
name: 'Seeds',
|
||||||
|
marketplaces: [
|
||||||
|
{
|
||||||
|
marketplace: { _id: 'mp-other' },
|
||||||
|
externalReference: '99999',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).rejects.toThrow(
|
||||||
|
'Product category "Seeds" requires an eBay category ID before it can be used on an eBay listing.'
|
||||||
|
);
|
||||||
|
expect(makeRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the matching mapping has an empty externalReference', async () => {
|
||||||
|
await expect(
|
||||||
|
syncProductCategory(
|
||||||
|
marketplace,
|
||||||
|
listingWithCategory({
|
||||||
|
_id: 'cat-1',
|
||||||
|
name: 'Seeds',
|
||||||
|
marketplaces: [
|
||||||
|
{
|
||||||
|
marketplace: { _id: 'mp1' },
|
||||||
|
externalReference: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).rejects.toThrow(
|
||||||
|
'Product category "Seeds" requires an eBay category ID before it can be used on an eBay listing.'
|
||||||
|
);
|
||||||
|
expect(makeRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call the taxonomy suggestions API', async () => {
|
||||||
|
await syncProductCategory(
|
||||||
|
marketplace,
|
||||||
|
listingWithCategory({
|
||||||
|
_id: 'cat-1',
|
||||||
|
name: 'Seeds',
|
||||||
|
marketplaces: [
|
||||||
|
{
|
||||||
|
marketplace: 'mp1',
|
||||||
|
externalReference: '67890',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(makeRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns listing.categoryId when already set', async () => {
|
||||||
|
const category = await syncProductCategory(marketplace, {
|
||||||
|
_reference: 'LST-1',
|
||||||
|
categoryId: '11111',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(category).toEqual({ categoryId: '11111' });
|
||||||
|
expect(makeRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { describe, expect, it } from '@jest/globals';
|
||||||
|
import { countCategoryReferences, mapCategoryTreeToReferences } from '../categoryTree.js';
|
||||||
|
|
||||||
|
const sampleTree = {
|
||||||
|
category: { categoryId: '0', categoryName: 'Root' },
|
||||||
|
childCategoryTreeNodes: [
|
||||||
|
{
|
||||||
|
category: { categoryId: '11450', categoryName: 'Clothing, Shoes & Accessories' },
|
||||||
|
categoryTreeNodeLevel: 1,
|
||||||
|
leafCategoryTreeNode: false,
|
||||||
|
childCategoryTreeNodes: [
|
||||||
|
{
|
||||||
|
category: { categoryId: '15724', categoryName: "Women's Clothing" },
|
||||||
|
categoryTreeNodeLevel: 2,
|
||||||
|
leafCategoryTreeNode: false,
|
||||||
|
childCategoryTreeNodes: [
|
||||||
|
{
|
||||||
|
category: { categoryId: '63861', categoryName: 'Dresses' },
|
||||||
|
categoryTreeNodeLevel: 3,
|
||||||
|
leafCategoryTreeNode: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: { categoryId: '99', categoryName: 'Everything Else' },
|
||||||
|
categoryTreeNodeLevel: 1,
|
||||||
|
leafCategoryTreeNode: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('eBay category tree mapping', () => {
|
||||||
|
it('maps the taxonomy tree into treeSelect references and skips the root', () => {
|
||||||
|
expect(mapCategoryTreeToReferences(sampleTree)).toEqual([
|
||||||
|
{
|
||||||
|
title: 'Clothing, Shoes & Accessories (11450)',
|
||||||
|
value: '11450',
|
||||||
|
selectable: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: "Women's Clothing (15724)",
|
||||||
|
value: '15724',
|
||||||
|
selectable: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Dresses (63861)',
|
||||||
|
value: '63861',
|
||||||
|
selectable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Everything Else (99)',
|
||||||
|
value: '99',
|
||||||
|
selectable: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty list when the tree is missing', () => {
|
||||||
|
expect(mapCategoryTreeToReferences(null)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts mapped category references', () => {
|
||||||
|
expect(countCategoryReferences(mapCategoryTreeToReferences(sampleTree))).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from '@jest/globals';
|
||||||
|
import { toEbayHtmlDescription, toEbayPlainDescription } from '../description.js';
|
||||||
|
|
||||||
|
describe('toEbayPlainDescription', () => {
|
||||||
|
it('strips markdown headings and HTML entities from TipTap output', () => {
|
||||||
|
expect(toEbayPlainDescription('# Test description. :)\n\n ')).toBe('Test description. :)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves plain text unchanged', () => {
|
||||||
|
expect(toEbayPlainDescription('Custom copy')).toBe('Custom copy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toEbayHtmlDescription', () => {
|
||||||
|
it('converts markdown headings to HTML and drops empty nbsp paragraphs', () => {
|
||||||
|
expect(toEbayHtmlDescription('# Test description. :)\n\n ')).toBe(
|
||||||
|
'<p><strong>Test description. :)</strong></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves plain text unchanged', () => {
|
||||||
|
expect(toEbayHtmlDescription('Custom copy')).toBe('Custom copy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts markdown lists and emphasis', () => {
|
||||||
|
expect(toEbayHtmlDescription('- **Red**\n- Blue')).toBe(
|
||||||
|
'<ul><li><strong>Red</strong></li><li>Blue</li></ul>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
188
src/integrations/marketplaces/ebay/__tests__/images.test.js
Normal file
188
src/integrations/marketplaces/ebay/__tests__/images.test.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../../database/ceph.js', () => ({
|
||||||
|
downloadFile: jest.fn(),
|
||||||
|
BUCKETS: { FILES: 'farmcontrol' },
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../shared.js', () => ({
|
||||||
|
makeRequest: jest.fn(),
|
||||||
|
getMediaApiBaseUrl: jest.fn(() => 'https://apim.sandbox.ebay.com'),
|
||||||
|
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { downloadFile } = await import('../../../../database/ceph.js');
|
||||||
|
const { makeRequest } = await import('../shared.js');
|
||||||
|
const {
|
||||||
|
attachImageUrlsToListingAndVarients,
|
||||||
|
buildImageUploadBody,
|
||||||
|
fileUploadName,
|
||||||
|
filesToImageUrls,
|
||||||
|
getListingImageFiles,
|
||||||
|
resolveListingImageUrls,
|
||||||
|
streamToBuffer,
|
||||||
|
} = await import('../images.js');
|
||||||
|
|
||||||
|
describe('getListingImageFiles', () => {
|
||||||
|
it('prefers varient listingImages over listing listingImages', () => {
|
||||||
|
expect(
|
||||||
|
getListingImageFiles(
|
||||||
|
{ listingImages: [{ _id: 'listing-img' }] },
|
||||||
|
{ listingImages: [{ _id: 'varient-img' }] }
|
||||||
|
)
|
||||||
|
).toEqual([{ _id: 'varient-img' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to listing listingImages', () => {
|
||||||
|
expect(getListingImageFiles({ listingImages: [{ _id: 'listing-img' }] }, {})).toEqual([
|
||||||
|
{ _id: 'listing-img' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fileUploadName', () => {
|
||||||
|
it('uses the file name when present', () => {
|
||||||
|
expect(fileUploadName({ name: 'Product Photo.jpg' })).toBe('Product_Photo.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to id and extension', () => {
|
||||||
|
expect(fileUploadName({ _id: 'file-1', extension: '.png' })).toBe('file-1.png');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildImageUploadBody', () => {
|
||||||
|
it('wraps image bytes in a multipart/form-data image part', () => {
|
||||||
|
const bytes = Buffer.from('jpeg-bytes');
|
||||||
|
const { body, contentType } = buildImageUploadBody(
|
||||||
|
{ _id: 'file-1', name: 'red.jpg', type: 'image/jpeg' },
|
||||||
|
bytes
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(contentType).toMatch(/^multipart\/form-data; boundary=/);
|
||||||
|
const boundary = contentType.split('boundary=')[1];
|
||||||
|
const text = body.toString('latin1');
|
||||||
|
expect(text).toContain(`name="image"`);
|
||||||
|
expect(text).toContain('filename="red.jpg"');
|
||||||
|
expect(text).toContain('Content-Type: image/jpeg');
|
||||||
|
expect(text).toContain('jpeg-bytes');
|
||||||
|
expect(text.startsWith(`--${boundary}\r\n`)).toBe(true);
|
||||||
|
expect(text.endsWith(`\r\n--${boundary}--\r\n`)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('streamToBuffer', () => {
|
||||||
|
it('returns buffers unchanged', async () => {
|
||||||
|
const buf = Buffer.from('abc');
|
||||||
|
expect(await streamToBuffer(buf)).toBe(buf);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads async iterables', async () => {
|
||||||
|
async function* chunks() {
|
||||||
|
yield Buffer.from('a');
|
||||||
|
yield Buffer.from('b');
|
||||||
|
}
|
||||||
|
expect((await streamToBuffer(chunks())).toString()).toBe('ab');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filesToImageUrls', () => {
|
||||||
|
const marketplace = { config: { accessToken: 'token' } };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
downloadFile.mockReset();
|
||||||
|
makeRequest.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('downloads each file and uploads it to the eBay Media API', async () => {
|
||||||
|
downloadFile.mockResolvedValue(Buffer.from('jpeg-bytes'));
|
||||||
|
makeRequest.mockResolvedValue({ imageUrl: 'https://i.ebayimg.com/img.jpg' });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
filesToImageUrls(marketplace, [{ _id: 'file-1', extension: '.jpg', type: 'image/jpeg' }])
|
||||||
|
).resolves.toEqual(['https://i.ebayimg.com/img.jpg']);
|
||||||
|
|
||||||
|
expect(downloadFile).toHaveBeenCalledWith('farmcontrol', 'files/file-1.jpg');
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/commerce/media/v1_beta/image/create_image_from_file',
|
||||||
|
baseUrl: 'https://apim.sandbox.ebay.com',
|
||||||
|
rawBody: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const upload = makeRequest.mock.calls[0][0];
|
||||||
|
expect(upload.contentType).toMatch(/^multipart\/form-data; boundary=/);
|
||||||
|
expect(Buffer.isBuffer(upload.body)).toBe(true);
|
||||||
|
expect(upload.body.toString('latin1')).toContain('name="image"');
|
||||||
|
expect(upload.body.includes(Buffer.from('jpeg-bytes'))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveListingImageUrls', () => {
|
||||||
|
const marketplace = { config: { accessToken: 'token' } };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
downloadFile.mockReset();
|
||||||
|
makeRequest.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to listing.imageUrls when there are no listingImages', async () => {
|
||||||
|
await expect(
|
||||||
|
resolveListingImageUrls(marketplace, { imageUrls: ['https://example.com/red.jpg'] })
|
||||||
|
).resolves.toEqual(['https://example.com/red.jpg']);
|
||||||
|
expect(downloadFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('attachImageUrlsToListingAndVarients', () => {
|
||||||
|
const marketplace = { config: { accessToken: 'token' } };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
downloadFile.mockReset();
|
||||||
|
makeRequest.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses listing image URLs on varients that have no listingImages', async () => {
|
||||||
|
downloadFile.mockResolvedValue(Buffer.from('jpeg-bytes'));
|
||||||
|
makeRequest.mockResolvedValue({ imageUrl: 'https://i.ebayimg.com/listing.jpg' });
|
||||||
|
|
||||||
|
const result = await attachImageUrlsToListingAndVarients(
|
||||||
|
marketplace,
|
||||||
|
{
|
||||||
|
_reference: 'LST-1',
|
||||||
|
listingImages: [{ _id: 'file-1', extension: '.jpg', type: 'image/jpeg' }],
|
||||||
|
},
|
||||||
|
[{ _reference: 'SKU-RED' }, { _reference: 'SKU-BLUE' }]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.listing.imageUrls).toEqual(['https://i.ebayimg.com/listing.jpg']);
|
||||||
|
expect(result.varients[0].imageUrls).toEqual(['https://i.ebayimg.com/listing.jpg']);
|
||||||
|
expect(result.varients[1].imageUrls).toEqual(['https://i.ebayimg.com/listing.jpg']);
|
||||||
|
expect(downloadFile).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uploads varient listingImages independently', async () => {
|
||||||
|
downloadFile.mockResolvedValue(Buffer.from('jpeg-bytes'));
|
||||||
|
makeRequest
|
||||||
|
.mockResolvedValueOnce({ imageUrl: 'https://i.ebayimg.com/listing.jpg' })
|
||||||
|
.mockResolvedValueOnce({ imageUrl: 'https://i.ebayimg.com/red.jpg' });
|
||||||
|
|
||||||
|
const result = await attachImageUrlsToListingAndVarients(
|
||||||
|
marketplace,
|
||||||
|
{
|
||||||
|
_reference: 'LST-1',
|
||||||
|
listingImages: [{ _id: 'file-1', extension: '.jpg', type: 'image/jpeg' }],
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
_reference: 'SKU-RED',
|
||||||
|
listingImages: [{ _id: 'file-2', extension: '.jpg', type: 'image/jpeg' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.listing.imageUrls).toEqual(['https://i.ebayimg.com/listing.jpg']);
|
||||||
|
expect(result.varients[0].imageUrls).toEqual(['https://i.ebayimg.com/red.jpg']);
|
||||||
|
expect(downloadFile).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../../database/schemas/sales/fulfillmentpolicy.schema.js', () => ({
|
||||||
|
fulfillmentPolicyModel: { findById: jest.fn() },
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../../../../database/schemas/finance/paymentpolicy.schema.js', () => ({
|
||||||
|
paymentPolicyModel: { findById: jest.fn() },
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../../../../database/schemas/sales/returnpolicy.schema.js', () => ({
|
||||||
|
returnPolicyModel: { findById: jest.fn() },
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../fulfillmentPolicies.js', () => ({
|
||||||
|
ensureFulfillmentPolicySynced: jest.fn(),
|
||||||
|
syncFulfillmentPolicy: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../paymentPolicies.js', () => ({
|
||||||
|
ensurePaymentPolicySynced: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../returnPolicies.js', () => ({
|
||||||
|
ensureReturnPolicySynced: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule('../shared.js', () => ({
|
||||||
|
makeRequest: jest.fn(),
|
||||||
|
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
||||||
|
getEbayMarketplaceId: () => 'EBAY_GB',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { ensureFulfillmentPolicySynced, syncFulfillmentPolicy } = await import('../fulfillmentPolicies.js');
|
||||||
|
const { ensurePaymentPolicySynced } = await import('../paymentPolicies.js');
|
||||||
|
const { ensureReturnPolicySynced } = await import('../returnPolicies.js');
|
||||||
|
const { syncListingPolicies } = await import('../listingPolicies.js');
|
||||||
|
|
||||||
|
describe('syncListingPolicies', () => {
|
||||||
|
const marketplace = {
|
||||||
|
_id: 'mp1',
|
||||||
|
defaultPaymentPolicy: { _id: 'pay-doc', name: 'Immediate Pay' },
|
||||||
|
defaultReturnPolicy: { _id: 'ret-doc', name: '30 Day Returns' },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ensureFulfillmentPolicySynced.mockReset();
|
||||||
|
syncFulfillmentPolicy.mockReset();
|
||||||
|
ensurePaymentPolicySynced.mockReset();
|
||||||
|
ensureReturnPolicySynced.mockReset();
|
||||||
|
ensurePaymentPolicySynced.mockResolvedValue({ paymentPolicyId: 'pay-1' });
|
||||||
|
ensureReturnPolicySynced.mockResolvedValue({ returnPolicyId: 'ret-1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses synced policy IDs from listing policies and marketplace defaults', async () => {
|
||||||
|
ensureFulfillmentPolicySynced.mockResolvedValue({ fulfillmentPolicyId: 'ful-1' });
|
||||||
|
const listing = {
|
||||||
|
fulfillmentPolicy: { _id: 'ful-doc', name: 'Standard Shipping' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(syncListingPolicies(marketplace, listing)).resolves.toEqual({
|
||||||
|
fulfillmentPolicyId: 'ful-1',
|
||||||
|
paymentPolicyId: 'pay-1',
|
||||||
|
returnPolicyId: 'ret-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ensureFulfillmentPolicySynced).toHaveBeenCalledWith(
|
||||||
|
marketplace,
|
||||||
|
listing.fulfillmentPolicy
|
||||||
|
);
|
||||||
|
expect(syncFulfillmentPolicy).not.toHaveBeenCalled();
|
||||||
|
expect(ensurePaymentPolicySynced).toHaveBeenCalledWith(
|
||||||
|
marketplace,
|
||||||
|
marketplace.defaultPaymentPolicy
|
||||||
|
);
|
||||||
|
expect(ensureReturnPolicySynced).toHaveBeenCalledWith(
|
||||||
|
marketplace,
|
||||||
|
marketplace.defaultReturnPolicy
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to courier-service auto-create when no fulfillment policy is set', async () => {
|
||||||
|
syncFulfillmentPolicy.mockResolvedValue({ fulfillmentPolicyId: 'auto-ful' });
|
||||||
|
const listing = { courierServices: [{ _id: 'cs-1' }] };
|
||||||
|
|
||||||
|
await expect(syncListingPolicies(marketplace, listing)).resolves.toEqual({
|
||||||
|
fulfillmentPolicyId: 'auto-ful',
|
||||||
|
paymentPolicyId: 'pay-1',
|
||||||
|
returnPolicyId: 'ret-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ensureFulfillmentPolicySynced).not.toHaveBeenCalled();
|
||||||
|
expect(syncFulfillmentPolicy).toHaveBeenCalledWith(marketplace, listing);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,17 +1,14 @@
|
|||||||
import { describe, expect, it, jest } from '@jest/globals';
|
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../../database/schemas/inventory/stocklocation.schema.js', () => ({
|
jest.unstable_mockModule('../../../../database/schemas/inventory/stocklocation.schema.js', () => ({
|
||||||
stockLocationModel: { findById: jest.fn() },
|
stockLocationModel: { findById: jest.fn() },
|
||||||
}));
|
}));
|
||||||
jest.unstable_mockModule('../../../../database/schemas/inventory/productstock.schema.js', () => ({
|
|
||||||
productStockModel: { find: jest.fn() },
|
|
||||||
}));
|
|
||||||
jest.unstable_mockModule('../countryCodes.js', () => ({
|
jest.unstable_mockModule('../countryCodes.js', () => ({
|
||||||
FARMCONTROL_GB_SUBDIVISION_STATE: {},
|
FARMCONTROL_GB_SUBDIVISION_STATE: {},
|
||||||
resolveEbayCountry: jest.fn(),
|
resolveEbayCountry: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.unstable_mockModule('../categories.js', () => ({ syncProductCategory: jest.fn() }));
|
jest.unstable_mockModule('../categories.js', () => ({ syncProductCategory: jest.fn() }));
|
||||||
jest.unstable_mockModule('../fulfillmentPolicies.js', () => ({ syncFulfillmentPolicy: jest.fn() }));
|
jest.unstable_mockModule('../listingPolicies.js', () => ({ syncListingPolicies: jest.fn() }));
|
||||||
jest.unstable_mockModule('../shared.js', () => ({
|
jest.unstable_mockModule('../shared.js', () => ({
|
||||||
makeRequest: jest.fn(),
|
makeRequest: jest.fn(),
|
||||||
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
||||||
@ -21,36 +18,31 @@ jest.unstable_mockModule('../../ids.js', () => ({
|
|||||||
marketplaceActor: (marketplace) => ({ ...marketplace, _objectType: 'marketplace' }),
|
marketplaceActor: (marketplace) => ({ ...marketplace, _objectType: 'marketplace' }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { productStockModel } =
|
const { resolveEbayCountry } = await import('../countryCodes.js');
|
||||||
await import('../../../../database/schemas/inventory/productstock.schema.js');
|
const { syncProductCategory } = await import('../categories.js');
|
||||||
|
const { syncListingPolicies } = await import('../listingPolicies.js');
|
||||||
const { makeRequest } = await import('../shared.js');
|
const { makeRequest } = await import('../shared.js');
|
||||||
const {
|
const {
|
||||||
|
buildVarientEntry,
|
||||||
fromEbayCondition,
|
fromEbayCondition,
|
||||||
|
publishOfferForSku,
|
||||||
|
withdrawOfferForSku,
|
||||||
resolveListingDescription,
|
resolveListingDescription,
|
||||||
resolveVarientQuantity,
|
resolveVarientQuantity,
|
||||||
toEbayCondition,
|
toEbayCondition,
|
||||||
upsertInventoryItem,
|
upsertInventoryItem,
|
||||||
|
inventoryItemPutBody,
|
||||||
} = await import('../listingVarients.js');
|
} = await import('../listingVarients.js');
|
||||||
|
|
||||||
describe('eBay varient quantity', () => {
|
describe('eBay varient quantity', () => {
|
||||||
it('sums productStock.currentQuantity for the listing stock location', async () => {
|
it('uses listing varient stockQuantity', () => {
|
||||||
productStockModel.find.mockReturnValue({
|
expect(resolveVarientQuantity({ stockQuantity: 8, productSku: 'sku-1' })).toBe(8);
|
||||||
lean: async () => [{ currentQuantity: 3 }, { currentQuantity: 5 }],
|
expect(resolveVarientQuantity({ stockQuantity: '3' })).toBe(3);
|
||||||
});
|
|
||||||
const quantity = await resolveVarientQuantity(
|
|
||||||
{ productSku: 'sku-1' },
|
|
||||||
{ stockLocation: 'loc-1' }
|
|
||||||
);
|
|
||||||
expect(quantity).toBe(8);
|
|
||||||
expect(productStockModel.find).toHaveBeenCalledWith({
|
|
||||||
productSku: 'sku-1',
|
|
||||||
stockLocation: 'loc-1',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to varient.inventory when no product SKU is linked', async () => {
|
it('treats missing stockQuantity as 0', () => {
|
||||||
const quantity = await resolveVarientQuantity({ inventory: 4 }, {});
|
expect(resolveVarientQuantity({ inventory: 4 })).toBe(0);
|
||||||
expect(quantity).toBe(4);
|
expect(resolveVarientQuantity({})).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,6 +62,24 @@ describe('eBay listing description', () => {
|
|||||||
'SKU copy'
|
'SKU copy'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sends markdown listing copy as plain text without HTML entities', async () => {
|
||||||
|
makeRequest.mockResolvedValue({});
|
||||||
|
await upsertInventoryItem(
|
||||||
|
{ config: { accessToken: 'token' } },
|
||||||
|
{ _reference: 'SKU-1' },
|
||||||
|
{ title: 'Widget', description: '# Test description. :)\n\n ' }
|
||||||
|
);
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
body: expect.objectContaining({
|
||||||
|
product: expect.objectContaining({
|
||||||
|
description: 'Test description. :)',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('eBay listing condition', () => {
|
describe('eBay listing condition', () => {
|
||||||
@ -108,4 +118,386 @@ describe('upsertInventoryItem description', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sends product.aspects when the varient has aspects', async () => {
|
||||||
|
makeRequest.mockResolvedValue({});
|
||||||
|
await upsertInventoryItem(
|
||||||
|
{ config: { accessToken: 'token' } },
|
||||||
|
{
|
||||||
|
_reference: 'SKU-1',
|
||||||
|
aspects: [
|
||||||
|
{ name: 'Color', value: 'Red' },
|
||||||
|
{ name: 'Size', value: 'Large' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ title: 'Widget', condition: 'new' }
|
||||||
|
);
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
body: expect.objectContaining({
|
||||||
|
product: expect.objectContaining({
|
||||||
|
aspects: { Color: ['Red'], Size: ['Large'] },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends stockQuantity as eBay inventory quantity', async () => {
|
||||||
|
makeRequest.mockResolvedValue({});
|
||||||
|
await upsertInventoryItem(
|
||||||
|
{ config: { accessToken: 'token' } },
|
||||||
|
{ _reference: 'SKU-1', stockQuantity: 7 },
|
||||||
|
{ title: 'Widget', condition: 'new' }
|
||||||
|
);
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
body: expect.objectContaining({
|
||||||
|
availability: {
|
||||||
|
shipToLocationAvailability: { quantity: 7 },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges GET inventory fields into the replace payload', async () => {
|
||||||
|
makeRequest.mockImplementation(async ({ method = 'GET' }) => {
|
||||||
|
if (method === 'GET') {
|
||||||
|
return {
|
||||||
|
product: {
|
||||||
|
title: 'Printed Plastic Wrench',
|
||||||
|
imageUrls: ['https://i.ebayimg.com/image.jpg'],
|
||||||
|
brand: 'FarmControl',
|
||||||
|
},
|
||||||
|
condition: 'NEW',
|
||||||
|
packageWeightAndSize: { weight: { value: 1, unit: 'KILOGRAM' } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
await upsertInventoryItem(
|
||||||
|
{ config: { accessToken: 'token' } },
|
||||||
|
{
|
||||||
|
_reference: 'S1R8WDRIAHQY',
|
||||||
|
stockQuantity: 4,
|
||||||
|
aspects: [{ name: 'Color', value: 'Grey' }],
|
||||||
|
},
|
||||||
|
{ title: 'Printed Plastic Wrench', description: 'Test description. :)', condition: 'new' }
|
||||||
|
);
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'PUT',
|
||||||
|
path: '/sell/inventory/v1/inventory_item/S1R8WDRIAHQY',
|
||||||
|
body: expect.objectContaining({
|
||||||
|
product: expect.objectContaining({
|
||||||
|
description: 'Test description. :)',
|
||||||
|
imageUrls: ['https://i.ebayimg.com/image.jpg'],
|
||||||
|
brand: 'FarmControl',
|
||||||
|
aspects: { Color: ['Grey'] },
|
||||||
|
}),
|
||||||
|
packageWeightAndSize: { weight: { value: 1, unit: 'KILOGRAM' } },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('inventoryItemPutBody', () => {
|
||||||
|
it('keeps existing product images and omits GET-only fields', () => {
|
||||||
|
expect(
|
||||||
|
inventoryItemPutBody(
|
||||||
|
{
|
||||||
|
sku: 'S1R8WDRIAHQY',
|
||||||
|
locale: 'en_GB',
|
||||||
|
inventoryItemGroupKeys: ['9VODNHNI7TFD'],
|
||||||
|
condition: 'NEW',
|
||||||
|
product: {
|
||||||
|
title: 'Printed Plastic Wrench',
|
||||||
|
description: 'Old',
|
||||||
|
imageUrls: ['https://i.ebayimg.com/image.jpg'],
|
||||||
|
brand: 'FarmControl',
|
||||||
|
},
|
||||||
|
availability: {
|
||||||
|
shipToLocationAvailability: {
|
||||||
|
quantity: 2,
|
||||||
|
allocationByFormat: { fixedPrice: 2 },
|
||||||
|
availabilityDistributions: [{ merchantLocationKey: 'fc-1', quantity: 2 }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
packageWeightAndSize: { weight: { value: 1, unit: 'KILOGRAM' } },
|
||||||
|
},
|
||||||
|
{ title: 'Printed Plastic Wrench', description: 'Test description. :)', condition: 'new' },
|
||||||
|
'S1R8WDRIAHQY',
|
||||||
|
{
|
||||||
|
_reference: 'S1R8WDRIAHQY',
|
||||||
|
aspects: [
|
||||||
|
{ name: 'Color', value: 'Grey' },
|
||||||
|
{ name: 'Size', value: 'Large' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
4
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
condition: 'NEW',
|
||||||
|
product: {
|
||||||
|
title: 'Printed Plastic Wrench',
|
||||||
|
description: 'Test description. :)',
|
||||||
|
imageUrls: ['https://i.ebayimg.com/image.jpg'],
|
||||||
|
brand: 'FarmControl',
|
||||||
|
aspects: { Color: ['Grey'], Size: ['Large'] },
|
||||||
|
},
|
||||||
|
availability: {
|
||||||
|
shipToLocationAvailability: {
|
||||||
|
quantity: 4,
|
||||||
|
availabilityDistributions: [{ merchantLocationKey: 'fc-1', quantity: 2 }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
packageWeightAndSize: { weight: { value: 1, unit: 'KILOGRAM' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers varient imageUrls over listing and existing product images', () => {
|
||||||
|
expect(
|
||||||
|
inventoryItemPutBody(
|
||||||
|
{
|
||||||
|
product: {
|
||||||
|
title: 'Printed Plastic Wrench',
|
||||||
|
imageUrls: ['https://i.ebayimg.com/old.jpg'],
|
||||||
|
},
|
||||||
|
condition: 'NEW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Printed Plastic Wrench',
|
||||||
|
description: 'Test description. :)',
|
||||||
|
condition: 'new',
|
||||||
|
imageUrls: ['https://example.com/listing.jpg'],
|
||||||
|
},
|
||||||
|
'SKU-1',
|
||||||
|
{
|
||||||
|
_reference: 'SKU-1',
|
||||||
|
imageUrls: ['https://example.com/varient.jpg'],
|
||||||
|
},
|
||||||
|
1
|
||||||
|
).product.imageUrls
|
||||||
|
).toEqual(['https://example.com/varient.jpg']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildVarientEntry', () => {
|
||||||
|
it('maps eBay product.aspects onto listing varient aspects', () => {
|
||||||
|
expect(
|
||||||
|
buildVarientEntry(
|
||||||
|
{
|
||||||
|
sku: 'SKU-RED',
|
||||||
|
product: { aspects: { Color: ['Red'], Size: ['Large'] } },
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
_reference: 'SKU-RED',
|
||||||
|
externalReference: 'SKU-RED',
|
||||||
|
aspects: [
|
||||||
|
{ name: 'Color', value: 'Red' },
|
||||||
|
{ name: 'Size', value: 'Large' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('publishOfferForSku', () => {
|
||||||
|
const marketplace = {
|
||||||
|
config: {
|
||||||
|
accessToken: 'token',
|
||||||
|
marketplaceId: 'EBAY_GB',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const listing = {
|
||||||
|
_reference: 'LST-1',
|
||||||
|
title: 'Widget',
|
||||||
|
description: 'A widget',
|
||||||
|
condition: 'new',
|
||||||
|
price: 9.99,
|
||||||
|
currency: 'GBP',
|
||||||
|
stockLocation: {
|
||||||
|
_id: 'loc-1',
|
||||||
|
name: 'Warehouse',
|
||||||
|
address: { country: 'GB', city: 'London', postcode: 'SW1A 1AA' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const varient = {
|
||||||
|
_reference: 'UD41EKPNU3QI',
|
||||||
|
price: 9.99,
|
||||||
|
currency: 'GBP',
|
||||||
|
stockQuantity: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
makeRequest.mockReset();
|
||||||
|
resolveEbayCountry.mockReturnValue({
|
||||||
|
ebayCountryCode: 'GB',
|
||||||
|
farmControlCountryCode: 'GB',
|
||||||
|
});
|
||||||
|
syncProductCategory.mockResolvedValue({ categoryId: '12345' });
|
||||||
|
syncListingPolicies.mockResolvedValue({
|
||||||
|
fulfillmentPolicyId: 'ful-1',
|
||||||
|
paymentPolicyId: 'pay-1',
|
||||||
|
returnPolicyId: 'ret-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockEbayApi({ offers = [], inventoryItem = null } = {}) {
|
||||||
|
makeRequest.mockImplementation(async ({ method = 'GET', path }) => {
|
||||||
|
if (path.startsWith('/sell/inventory/v1/location/')) {
|
||||||
|
return method === 'GET' ? null : {};
|
||||||
|
}
|
||||||
|
if (path.startsWith('/sell/inventory/v1/inventory_item/')) {
|
||||||
|
return method === 'GET' ? inventoryItem : {};
|
||||||
|
}
|
||||||
|
if (path === '/sell/inventory/v1/offer') {
|
||||||
|
return method === 'POST' ? { offerId: 'offer-created' } : { offers };
|
||||||
|
}
|
||||||
|
if (path.endsWith('/publish')) {
|
||||||
|
return { listingId: '123456' };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('creates inventory and an offer when none exist, then publishes', async () => {
|
||||||
|
mockEbayApi();
|
||||||
|
const result = await publishOfferForSku(marketplace, 'UD41EKPNU3QI', listing, varient);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
offerId: 'offer-created',
|
||||||
|
listingId: '123456',
|
||||||
|
externalReference: '123456',
|
||||||
|
url: 'https://www.ebay.com/itm/123456',
|
||||||
|
});
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'PUT',
|
||||||
|
path: '/sell/inventory/v1/inventory_item/UD41EKPNU3QI',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/inventory/v1/offer',
|
||||||
|
body: expect.objectContaining({
|
||||||
|
sku: 'UD41EKPNU3QI',
|
||||||
|
categoryId: '12345',
|
||||||
|
availableQuantity: 5,
|
||||||
|
listingPolicies: {
|
||||||
|
fulfillmentPolicyId: 'ful-1',
|
||||||
|
paymentPolicyId: 'pay-1',
|
||||||
|
returnPolicyId: 'ret-1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/inventory/v1/offer/offer-created/publish',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('publishes an existing offer without creating a new one', async () => {
|
||||||
|
mockEbayApi({
|
||||||
|
offers: [{ offerId: 'offer-existing', listingPolicies: {} }],
|
||||||
|
inventoryItem: { product: { title: 'Widget' } },
|
||||||
|
});
|
||||||
|
const result = await publishOfferForSku(marketplace, 'UD41EKPNU3QI', listing, varient);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
offerId: 'offer-existing',
|
||||||
|
listingId: '123456',
|
||||||
|
externalReference: '123456',
|
||||||
|
url: 'https://www.ebay.com/itm/123456',
|
||||||
|
});
|
||||||
|
expect(makeRequest).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ method: 'POST', path: '/sell/inventory/v1/offer' })
|
||||||
|
);
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/inventory/v1/offer/offer-existing/publish',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not publish again when the existing offer is already live', async () => {
|
||||||
|
mockEbayApi({
|
||||||
|
offers: [
|
||||||
|
{
|
||||||
|
offerId: 'offer-existing',
|
||||||
|
status: 'PUBLISHED',
|
||||||
|
listingId: '110590242406',
|
||||||
|
listingPolicies: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inventoryItem: { product: { title: 'Widget' } },
|
||||||
|
});
|
||||||
|
const result = await publishOfferForSku(
|
||||||
|
marketplace,
|
||||||
|
'UD41EKPNU3QI',
|
||||||
|
{ ...listing, externalReference: '110590242406' },
|
||||||
|
varient
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
offerId: 'offer-existing',
|
||||||
|
listingId: '110590242406',
|
||||||
|
externalReference: '110590242406',
|
||||||
|
url: 'https://www.ebay.com/itm/110590242406',
|
||||||
|
});
|
||||||
|
expect(makeRequest).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/inventory/v1/offer/offer-existing/publish',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a sandbox item URL when the marketplace is in sandbox', async () => {
|
||||||
|
mockEbayApi();
|
||||||
|
const result = await publishOfferForSku(
|
||||||
|
{ ...marketplace, config: { ...marketplace.config, sandbox: true } },
|
||||||
|
'UD41EKPNU3QI',
|
||||||
|
listing,
|
||||||
|
varient
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.url).toBe('https://sandbox.ebay.com/itm/123456');
|
||||||
|
expect(result.externalReference).toBe('123456');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('withdraws the offer matching the stored eBay item id', async () => {
|
||||||
|
mockEbayApi({
|
||||||
|
offers: [
|
||||||
|
{ offerId: 'offer-other', listingId: '999' },
|
||||||
|
{ offerId: 'offer-existing', listing: { listingId: '110590242406' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const result = await withdrawOfferForSku(marketplace, 'UD41EKPNU3QI', {
|
||||||
|
externalReference: '110590242406',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ offerId: 'offer-existing', listingId: '110590242406' });
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/inventory/v1/offer/offer-existing/withdraw',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(makeRequest).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
path: '/sell/inventory/v1/offer/offer-other/withdraw',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,9 +31,38 @@ jest.unstable_mockModule('../shared.js', () => ({
|
|||||||
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
||||||
}));
|
}));
|
||||||
jest.unstable_mockModule('../categories.js', () => ({ syncProductCategory: jest.fn() }));
|
jest.unstable_mockModule('../categories.js', () => ({ syncProductCategory: jest.fn() }));
|
||||||
jest.unstable_mockModule('../fulfillmentPolicies.js', () => ({ syncFulfillmentPolicy: jest.fn() }));
|
jest.unstable_mockModule('../listingPolicies.js', () => ({ syncListingPolicies: jest.fn() }));
|
||||||
|
jest.unstable_mockModule('../images.js', () => ({
|
||||||
|
attachImageUrlsToListingAndVarients: jest.fn(async (_marketplace, listing, varients = []) => ({
|
||||||
|
listing,
|
||||||
|
varients,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
const { mapProductToListing } = await import('../listings.js');
|
const { mapProductToListing, buildInventoryItemGroupBody, syncListingImages } = await import(
|
||||||
|
'../listings.js'
|
||||||
|
);
|
||||||
|
const { getEbayItemUrl, parseEbayItemId } = await import('../itemUrl.js');
|
||||||
|
const { makeRequest } = await import('../shared.js');
|
||||||
|
const { attachImageUrlsToListingAndVarients } = await import('../images.js');
|
||||||
|
const { upsertInventoryItem, syncOfferAndMaybePublish } = await import('../listingVarients.js');
|
||||||
|
|
||||||
|
describe('eBay item URLs', () => {
|
||||||
|
it('parses an item id from a sandbox or production URL', () => {
|
||||||
|
expect(parseEbayItemId('https://sandbox.ebay.com/itm/110590242406')).toBe('110590242406');
|
||||||
|
expect(parseEbayItemId('https://www.ebay.com/itm/110590242406')).toBe('110590242406');
|
||||||
|
expect(parseEbayItemId('110590242406')).toBe('110590242406');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds sandbox and production item URLs', () => {
|
||||||
|
expect(getEbayItemUrl({ config: { sandbox: true } }, '110590242406')).toBe(
|
||||||
|
'https://sandbox.ebay.com/itm/110590242406'
|
||||||
|
);
|
||||||
|
expect(getEbayItemUrl({ config: { sandbox: false } }, '110590242406')).toBe(
|
||||||
|
'https://www.ebay.com/itm/110590242406'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('eBay listing mappers', () => {
|
describe('eBay listing mappers', () => {
|
||||||
it('maps grouped inventory to externalReference instead of overwriting _reference', () => {
|
it('maps grouped inventory to externalReference instead of overwriting _reference', () => {
|
||||||
@ -56,6 +85,7 @@ describe('eBay listing mappers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mapped.externalReference).toBe('123456');
|
expect(mapped.externalReference).toBe('123456');
|
||||||
|
expect(mapped.url).toBe('https://www.ebay.com/itm/123456');
|
||||||
expect(mapped.title).toBe('Widget');
|
expect(mapped.title).toBe('Widget');
|
||||||
expect(mapped.description).toBe('A widget');
|
expect(mapped.description).toBe('A widget');
|
||||||
expect(mapped.varients[0].externalReference).toBe('SKU-RED');
|
expect(mapped.varients[0].externalReference).toBe('SKU-RED');
|
||||||
@ -67,10 +97,129 @@ describe('eBay listing mappers', () => {
|
|||||||
sku: 'SKU-BLUE',
|
sku: 'SKU-BLUE',
|
||||||
condition: 'LIKE_NEW',
|
condition: 'LIKE_NEW',
|
||||||
product: { title: 'Blue widget', description: 'Blue' },
|
product: { title: 'Blue widget', description: 'Blue' },
|
||||||
_offers: [{ listingId: '999', status: 'PUBLISHED', pricingSummary: { price: { value: '4.00', currency: 'GBP' } } }],
|
_offers: [
|
||||||
|
{
|
||||||
|
listingId: '999',
|
||||||
|
status: 'PUBLISHED',
|
||||||
|
pricingSummary: { price: { value: '4.00', currency: 'GBP' } },
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
expect(mapped.externalReference).toBe('999');
|
expect(mapped.externalReference).toBe('999');
|
||||||
|
expect(mapped.url).toBe('https://www.ebay.com/itm/999');
|
||||||
expect(mapped.varients[0].externalReference).toBe('SKU-BLUE');
|
expect(mapped.varients[0].externalReference).toBe('SKU-BLUE');
|
||||||
expect(mapped.condition).toBe('likeNew');
|
expect(mapped.condition).toBe('likeNew');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the sandbox item URL when the marketplace is in sandbox', () => {
|
||||||
|
const mapped = mapProductToListing(
|
||||||
|
{
|
||||||
|
sku: 'SKU-BLUE',
|
||||||
|
product: { title: 'Blue widget' },
|
||||||
|
_offers: [{ listingId: '110590242406', status: 'PUBLISHED' }],
|
||||||
|
},
|
||||||
|
{ config: { sandbox: true } }
|
||||||
|
);
|
||||||
|
expect(mapped.externalReference).toBe('110590242406');
|
||||||
|
expect(mapped.url).toBe('https://sandbox.ebay.com/itm/110590242406');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('eBay inventory item group body', () => {
|
||||||
|
const listing = {
|
||||||
|
_reference: 'LST-1',
|
||||||
|
title: 'Widget',
|
||||||
|
description: 'A widget',
|
||||||
|
};
|
||||||
|
const red = {
|
||||||
|
_reference: 'SKU-RED',
|
||||||
|
aspects: [{ name: 'Color', value: 'Red' }],
|
||||||
|
};
|
||||||
|
const blue = {
|
||||||
|
_reference: 'SKU-BLUE',
|
||||||
|
aspects: [{ name: 'Color', value: 'Blue' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('includes variesBy.specifications and omits identical aspects from variesBy', () => {
|
||||||
|
expect(
|
||||||
|
buildInventoryItemGroupBody(listing, [
|
||||||
|
{
|
||||||
|
...red,
|
||||||
|
aspects: [...red.aspects, { name: 'Brand', value: 'Acme' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...blue,
|
||||||
|
aspects: [...blue.aspects, { name: 'Brand', value: 'Acme' }],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).toEqual({
|
||||||
|
title: 'Widget',
|
||||||
|
variantSKUs: ['SKU-RED', 'SKU-BLUE'],
|
||||||
|
description: 'A widget',
|
||||||
|
variesBy: { specifications: [{ name: 'Color', values: ['Red', 'Blue'] }] },
|
||||||
|
aspects: { Brand: ['Acme'] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts markdown listing descriptions to eBay-safe HTML', () => {
|
||||||
|
expect(
|
||||||
|
buildInventoryItemGroupBody({ ...listing, description: '# Test description. :)\n\n ' }, [
|
||||||
|
red,
|
||||||
|
blue,
|
||||||
|
]).description
|
||||||
|
).toBe('<p><strong>Test description. :)</strong></p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets aspectsImageVariesBy when the listing has images', () => {
|
||||||
|
const body = buildInventoryItemGroupBody(
|
||||||
|
{ ...listing, imageUrls: ['https://example.com/red.jpg'] },
|
||||||
|
[red, blue]
|
||||||
|
);
|
||||||
|
expect(body.imageUrls).toEqual(['https://example.com/red.jpg']);
|
||||||
|
expect(body.variesBy.aspectsImageVariesBy).toEqual(['Color']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws before makeRequest when grouped varients are missing aspects', () => {
|
||||||
|
expect(() => buildInventoryItemGroupBody(listing, [red, { _reference: 'SKU-BLUE' }])).toThrow(
|
||||||
|
/SKU-BLUE.*at least one aspect/
|
||||||
|
);
|
||||||
|
expect(makeRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('syncListingImages', () => {
|
||||||
|
it('uploads listing images and upserts inventory without publishing offers', async () => {
|
||||||
|
const listing = { _reference: 'LST-1', title: 'Widget' };
|
||||||
|
const varients = [{ _reference: 'SKU-RED' }, { _reference: 'SKU-BLUE' }];
|
||||||
|
const listingWithImages = { ...listing, imageUrls: ['https://i.ebayimg.com/1.jpg'] };
|
||||||
|
const varientsWithImages = [
|
||||||
|
{ _reference: 'SKU-RED', imageUrls: ['https://i.ebayimg.com/1.jpg'] },
|
||||||
|
{ _reference: 'SKU-BLUE', imageUrls: ['https://i.ebayimg.com/2.jpg'] },
|
||||||
|
];
|
||||||
|
attachImageUrlsToListingAndVarients.mockResolvedValueOnce({
|
||||||
|
listing: listingWithImages,
|
||||||
|
varients: varientsWithImages,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await syncListingImages({ name: 'eBay UK' }, listing, varients);
|
||||||
|
|
||||||
|
expect(attachImageUrlsToListingAndVarients).toHaveBeenCalledWith(
|
||||||
|
{ name: 'eBay UK' },
|
||||||
|
listing,
|
||||||
|
varients
|
||||||
|
);
|
||||||
|
expect(upsertInventoryItem).toHaveBeenCalledTimes(2);
|
||||||
|
expect(upsertInventoryItem).toHaveBeenCalledWith(
|
||||||
|
{ name: 'eBay UK' },
|
||||||
|
varientsWithImages[0],
|
||||||
|
listingWithImages
|
||||||
|
);
|
||||||
|
expect(upsertInventoryItem).toHaveBeenCalledWith(
|
||||||
|
{ name: 'eBay UK' },
|
||||||
|
varientsWithImages[1],
|
||||||
|
listingWithImages
|
||||||
|
);
|
||||||
|
expect(syncOfferAndMaybePublish).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({ listing: listingWithImages, varients: varientsWithImages });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { describe, expect, it } from '@jest/globals';
|
||||||
|
import {
|
||||||
|
buildGroupVariesBy,
|
||||||
|
fromEbayProductAspects,
|
||||||
|
toEbayProductAspects,
|
||||||
|
} from '../variationAspects.js';
|
||||||
|
|
||||||
|
describe('toEbayProductAspects', () => {
|
||||||
|
it('converts name/value pairs into eBay product.aspects', () => {
|
||||||
|
expect(
|
||||||
|
toEbayProductAspects([
|
||||||
|
{ name: 'Color', value: 'Red' },
|
||||||
|
{ name: 'Size', value: 'Large' },
|
||||||
|
])
|
||||||
|
).toEqual({ Color: ['Red'], Size: ['Large'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined when there are no usable aspects', () => {
|
||||||
|
expect(toEbayProductAspects([])).toBeUndefined();
|
||||||
|
expect(toEbayProductAspects([{ name: ' ', value: 'Red' }])).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fromEbayProductAspects', () => {
|
||||||
|
it('converts eBay product.aspects into name/value pairs', () => {
|
||||||
|
expect(fromEbayProductAspects({ Color: ['Red'], Size: ['Large'] })).toEqual([
|
||||||
|
{ name: 'Color', value: 'Red' },
|
||||||
|
{ name: 'Size', value: 'Large' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty array for missing aspects', () => {
|
||||||
|
expect(fromEbayProductAspects(undefined)).toEqual([]);
|
||||||
|
expect(fromEbayProductAspects(null)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildGroupVariesBy', () => {
|
||||||
|
it('puts differing values in variesBy.specifications and shared values in aspects', () => {
|
||||||
|
expect(
|
||||||
|
buildGroupVariesBy([
|
||||||
|
{
|
||||||
|
_reference: 'SKU-RED-S',
|
||||||
|
aspects: [
|
||||||
|
{ name: 'Color', value: 'Red' },
|
||||||
|
{ name: 'Brand', value: 'Acme' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_reference: 'SKU-BLUE-S',
|
||||||
|
aspects: [
|
||||||
|
{ name: 'Color', value: 'Blue' },
|
||||||
|
{ name: 'Brand', value: 'Acme' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).toEqual({
|
||||||
|
variesBy: { specifications: [{ name: 'Color', values: ['Red', 'Blue'] }] },
|
||||||
|
aspects: { Brand: ['Acme'] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when a varient has no aspects', () => {
|
||||||
|
expect(() =>
|
||||||
|
buildGroupVariesBy([
|
||||||
|
{ _reference: 'SKU-RED', aspects: [{ name: 'Color', value: 'Red' }] },
|
||||||
|
{ _reference: 'SKU-BLUE' },
|
||||||
|
])
|
||||||
|
).toThrow(/SKU-BLUE.*at least one aspect/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when varients use different aspect names', () => {
|
||||||
|
expect(() =>
|
||||||
|
buildGroupVariesBy([
|
||||||
|
{ _reference: 'SKU-RED', aspects: [{ name: 'Color', value: 'Red' }] },
|
||||||
|
{ _reference: 'SKU-LARGE', aspects: [{ name: 'Size', value: 'Large' }] },
|
||||||
|
])
|
||||||
|
).toThrow(/missing aspect/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when varients do not differ by any aspect', () => {
|
||||||
|
expect(() =>
|
||||||
|
buildGroupVariesBy([
|
||||||
|
{ _reference: 'SKU-1', aspects: [{ name: 'Color', value: 'Red' }] },
|
||||||
|
{ _reference: 'SKU-2', aspects: [{ name: 'Color', value: 'Red' }] },
|
||||||
|
])
|
||||||
|
).toThrow(/differ by at least one aspect/);
|
||||||
|
});
|
||||||
|
});
|
||||||
279
src/integrations/marketplaces/ebay/accountPolicies.js
Normal file
279
src/integrations/marketplaces/ebay/accountPolicies.js
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
import { makeRequest, logger, getEbayMarketplaceId } from './shared.js';
|
||||||
|
|
||||||
|
const SELLING_POLICY_PROGRAM = 'SELLING_POLICY_MANAGEMENT';
|
||||||
|
export const POLICY_CATEGORY_TYPE = 'ALL_EXCLUDING_MOTORS_VEHICLES';
|
||||||
|
|
||||||
|
export function idOf(value) {
|
||||||
|
if (value == null) return '';
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
if (value._id != null) return String(value._id);
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPopulatedDocument(value) {
|
||||||
|
return (
|
||||||
|
value &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!(value.constructor?.name === 'ObjectId') &&
|
||||||
|
(value.name != null || value._reference != null || Array.isArray(value.marketplaces))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMarketplaceMapping(doc, marketplace) {
|
||||||
|
const marketplaceId = idOf(marketplace);
|
||||||
|
if (!marketplaceId) return (doc?.marketplaces || [])[0] || null;
|
||||||
|
return (
|
||||||
|
(doc?.marketplaces || []).find((entry) => idOf(entry.marketplace) === marketplaceId) || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mappingExternalReference(doc, marketplace) {
|
||||||
|
return getMarketplaceMapping(doc, marketplace)?.externalReference || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function persistMarketplaceMapping(
|
||||||
|
model,
|
||||||
|
doc,
|
||||||
|
marketplace,
|
||||||
|
{ externalReference, stateType, message } = {}
|
||||||
|
) {
|
||||||
|
if (!model || !doc) return null;
|
||||||
|
const marketplaceId = idOf(marketplace);
|
||||||
|
const docId = idOf(doc);
|
||||||
|
if (!marketplaceId || !docId) return null;
|
||||||
|
|
||||||
|
const current = (await model.findById(docId).lean()) || doc;
|
||||||
|
const mappings = [...(current?.marketplaces || [])];
|
||||||
|
const index = mappings.findIndex((entry) => idOf(entry.marketplace) === marketplaceId);
|
||||||
|
const previous = index >= 0 ? mappings[index] : {};
|
||||||
|
const next = {
|
||||||
|
...(previous._id ? { _id: previous._id } : {}),
|
||||||
|
marketplace: previous.marketplace || marketplaceId,
|
||||||
|
externalReference:
|
||||||
|
externalReference !== undefined ? externalReference : previous.externalReference,
|
||||||
|
state: {
|
||||||
|
type: stateType || previous.state?.type || 'pending',
|
||||||
|
...(message != null && message !== ''
|
||||||
|
? { message }
|
||||||
|
: stateType === 'failed'
|
||||||
|
? { message: previous.state?.message }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (index >= 0) mappings[index] = next;
|
||||||
|
else mappings.push(next);
|
||||||
|
|
||||||
|
await model.updateOne({ _id: docId }, { $set: { marketplaces: mappings } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { deleteObjectCache } = await import('../../../database/database.js');
|
||||||
|
const { distributeUpdate } = await import('../../../utils.js');
|
||||||
|
await deleteObjectCache({ model, id: docId });
|
||||||
|
let broadcastMappings = mappings;
|
||||||
|
const query = model.findById(docId);
|
||||||
|
if (typeof query?.populate === 'function') {
|
||||||
|
const populated = await query.populate('marketplaces.marketplace').lean();
|
||||||
|
if (populated?.marketplaces) broadcastMappings = populated.marketplaces;
|
||||||
|
}
|
||||||
|
await distributeUpdate({ marketplaces: broadcastMappings }, docId, model.modelName);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(`Could not broadcast marketplace mapping update for ${docId}: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOptedInPrograms(marketplace) {
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/sell/account/v1/program/get_opted_in_programs',
|
||||||
|
});
|
||||||
|
return result?.programs || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSellingPolicyManagement(programs) {
|
||||||
|
return programs.some((program) => program?.programType === SELLING_POLICY_PROGRAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureSellingPolicyManagement(marketplace) {
|
||||||
|
if (hasSellingPolicyManagement(await fetchOptedInPrograms(marketplace))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Opting eBay seller account into Selling Policy Management');
|
||||||
|
await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/account/v1/program/opt_in',
|
||||||
|
body: { programType: SELLING_POLICY_PROGRAM },
|
||||||
|
acceptableStatuses: [409],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasSellingPolicyManagement(await fetchOptedInPrograms(marketplace))) {
|
||||||
|
throw new Error(
|
||||||
|
'eBay Selling Policy Management enrollment was requested but is not active yet. eBay can take up to 24 hours to process enrollment; retry publishing later.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDefaultAccountPolicy(existingPolicy, allPolicies = [], idField) {
|
||||||
|
const existingId = existingPolicy?.[idField];
|
||||||
|
if (!existingId) return false;
|
||||||
|
|
||||||
|
const existingIdString = String(existingId);
|
||||||
|
const sources = [existingPolicy, ...allPolicies];
|
||||||
|
if (
|
||||||
|
sources.some(
|
||||||
|
(policy) =>
|
||||||
|
String(policy?.[idField]) === existingIdString &&
|
||||||
|
(policy.categoryTypes || []).some((type) => type?.default === true)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueIds = [
|
||||||
|
...new Set(
|
||||||
|
allPolicies.filter((policy) => policy?.[idField]).map((policy) => String(policy[idField]))
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return uniqueIds.length === 1 && uniqueIds[0] === existingIdString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCategoryTypes(existingPolicy, allPolicies = [], idField) {
|
||||||
|
const categoryType = { name: POLICY_CATEGORY_TYPE };
|
||||||
|
if (idField && isDefaultAccountPolicy(existingPolicy, allPolicies, idField)) {
|
||||||
|
categoryType.default = true;
|
||||||
|
}
|
||||||
|
return [categoryType];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDefaultStatusError(err) {
|
||||||
|
return /changing the default status/i.test(err?.message || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnchangedPolicyError(err) {
|
||||||
|
return /same as in the system/i.test(err?.message || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAccountPolicy(marketplace, path, policy) {
|
||||||
|
try {
|
||||||
|
await makeRequest({ marketplace, method: 'PUT', path, body: policy });
|
||||||
|
} catch (err) {
|
||||||
|
if (isUnchangedPolicyError(err)) {
|
||||||
|
logger.debug(`eBay account policy ${path} is already up to date`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (policy.categoryTypes?.[0]?.default === true || !isDefaultStatusError(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
logger.debug(`Retrying account policy ${path} with categoryTypes.default=true`);
|
||||||
|
try {
|
||||||
|
await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'PUT',
|
||||||
|
path,
|
||||||
|
body: {
|
||||||
|
...policy,
|
||||||
|
categoryTypes: [{ name: POLICY_CATEGORY_TYPE, default: true }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (retryErr) {
|
||||||
|
if (isUnchangedPolicyError(retryErr)) {
|
||||||
|
logger.debug(`eBay account policy ${path} is already up to date`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw retryErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadPolicyDocument(model, value) {
|
||||||
|
if (!value) return null;
|
||||||
|
if (isPopulatedDocument(value)) return value;
|
||||||
|
const id = idOf(value);
|
||||||
|
if (!id) return null;
|
||||||
|
return model.findById(id).populate(['marketplaces.marketplace']).lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveListingPolicy(listing, marketplace, listingField, defaultField) {
|
||||||
|
return (
|
||||||
|
listing?.[listingField] ||
|
||||||
|
listing?.marketplace?.[defaultField] ||
|
||||||
|
marketplace?.[defaultField] ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findLocalPolicyByExternalReference(model, marketplace, externalReference) {
|
||||||
|
if (!externalReference) return null;
|
||||||
|
return model
|
||||||
|
.findOne({
|
||||||
|
marketplaces: {
|
||||||
|
$elemMatch: {
|
||||||
|
marketplace: idOf(marketplace),
|
||||||
|
externalReference: String(externalReference),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findLocalPolicyByName(model, marketplace, name) {
|
||||||
|
if (!name) return null;
|
||||||
|
return model
|
||||||
|
.findOne({
|
||||||
|
name,
|
||||||
|
'marketplaces.marketplace': idOf(marketplace),
|
||||||
|
})
|
||||||
|
.lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertLocalPolicyFromRemote({
|
||||||
|
model,
|
||||||
|
marketplace,
|
||||||
|
remoteId,
|
||||||
|
name,
|
||||||
|
fields,
|
||||||
|
user,
|
||||||
|
}) {
|
||||||
|
let local =
|
||||||
|
(await findLocalPolicyByExternalReference(model, marketplace, remoteId)) ||
|
||||||
|
(await findLocalPolicyByName(model, marketplace, name));
|
||||||
|
|
||||||
|
const mapping = {
|
||||||
|
marketplace: idOf(marketplace),
|
||||||
|
externalReference: String(remoteId),
|
||||||
|
state: { type: 'ready' },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (local) {
|
||||||
|
const mappings = [...(local.marketplaces || [])];
|
||||||
|
const index = mappings.findIndex((entry) => idOf(entry.marketplace) === idOf(marketplace));
|
||||||
|
if (index >= 0) mappings[index] = { ...mappings[index], ...mapping };
|
||||||
|
else mappings.push(mapping);
|
||||||
|
await model.updateOne(
|
||||||
|
{ _id: local._id },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
...fields,
|
||||||
|
name,
|
||||||
|
marketplaces: mappings,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return model.findById(local._id).lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await model.create({
|
||||||
|
...fields,
|
||||||
|
name,
|
||||||
|
marketplaces: [mapping],
|
||||||
|
});
|
||||||
|
return created.toObject ? created.toObject() : created;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getEbayMarketplaceId };
|
||||||
17
src/integrations/marketplaces/ebay/accountPolicySync.js
Normal file
17
src/integrations/marketplaces/ebay/accountPolicySync.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { logger } from './shared.js';
|
||||||
|
import { syncFulfillmentPoliciesFromEbay } from './fulfillmentPolicies.js';
|
||||||
|
import { syncPaymentPoliciesFromEbay } from './paymentPolicies.js';
|
||||||
|
import { syncReturnPoliciesFromEbay } from './returnPolicies.js';
|
||||||
|
import { syncTaxRatesFromEbay, syncTaxRatesToEbay } from './salesTax.js';
|
||||||
|
|
||||||
|
export async function syncAccountPolicies(marketplace) {
|
||||||
|
const fulfillment = await syncFulfillmentPoliciesFromEbay(marketplace);
|
||||||
|
const payment = await syncPaymentPoliciesFromEbay(marketplace);
|
||||||
|
const returns = await syncReturnPoliciesFromEbay(marketplace);
|
||||||
|
const taxInbound = await syncTaxRatesFromEbay(marketplace);
|
||||||
|
const taxOutbound = await syncTaxRatesToEbay(marketplace);
|
||||||
|
logger.info(
|
||||||
|
`Synced account policies for marketplace "${marketplace.name}": fulfillment=${fulfillment.count}, payment=${payment.count}, return=${returns.count}, taxIn=${taxInbound.count}, taxOut=${taxOutbound.synced}`
|
||||||
|
);
|
||||||
|
return { fulfillment, payment, returns, taxInbound, taxOutbound };
|
||||||
|
}
|
||||||
@ -1,15 +1,20 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { productModel } from '../../../database/schemas/management/product.schema.js';
|
import { productModel } from '../../../database/schemas/management/product.schema.js';
|
||||||
import { productCategoryModel } from '../../../database/schemas/management/productcategory.schema.js';
|
import { productCategoryModel } from '../../../database/schemas/management/productcategory.schema.js';
|
||||||
import { makeRequest, logger } from './shared.js';
|
import { logger } from './shared.js';
|
||||||
|
|
||||||
const categoryTreeIds = new Map();
|
const PRODUCT_CATEGORY_POPULATE = ['marketplaces.marketplace'];
|
||||||
const categoryMatches = new Map();
|
|
||||||
|
|
||||||
function isPopulated(value) {
|
function isPopulated(value) {
|
||||||
return value && typeof value === 'object' && !(value instanceof mongoose.Types.ObjectId);
|
return value && typeof value === 'object' && !(value instanceof mongoose.Types.ObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function idOf(value) {
|
||||||
|
if (value == null) return '';
|
||||||
|
if (typeof value === 'object') return String(value._id || '');
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveProduct(listing, varients) {
|
async function resolveProduct(listing, varients) {
|
||||||
const productRef = listing?.product || varients?.find((varient) => varient?.product)?.product;
|
const productRef = listing?.product || varients?.find((varient) => varient?.product)?.product;
|
||||||
if (!productRef) return null;
|
if (!productRef) return null;
|
||||||
@ -19,7 +24,10 @@ async function resolveProduct(listing, varients) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const productId = productRef._id || productRef;
|
const productId = productRef._id || productRef;
|
||||||
return productModel.findById(productId).populate('productCategory').lean();
|
return productModel
|
||||||
|
.findById(productId)
|
||||||
|
.populate({ path: 'productCategory', populate: { path: 'marketplaces.marketplace' } })
|
||||||
|
.lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveProductCategory(listing, varients) {
|
async function resolveProductCategory(listing, varients) {
|
||||||
@ -27,49 +35,34 @@ async function resolveProductCategory(listing, varients) {
|
|||||||
const categoryRef = product?.productCategory;
|
const categoryRef = product?.productCategory;
|
||||||
if (!categoryRef) return null;
|
if (!categoryRef) return null;
|
||||||
|
|
||||||
if (isPopulated(categoryRef) && categoryRef.name) {
|
if (isPopulated(categoryRef) && Array.isArray(categoryRef.marketplaces)) {
|
||||||
return categoryRef;
|
return categoryRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryId = categoryRef._id || categoryRef;
|
const categoryId = categoryRef._id || categoryRef;
|
||||||
return productCategoryModel.findById(categoryId).lean();
|
return productCategoryModel.findById(categoryId).populate(PRODUCT_CATEGORY_POPULATE).lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDefaultCategoryTreeId(marketplace) {
|
export function getProductCategoryMarketplaceMappings(productCategory) {
|
||||||
const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
|
if (Array.isArray(productCategory?.marketplaces) && productCategory.marketplaces.length) {
|
||||||
const cacheKey = `${marketplace.config?.sandbox ? 'sandbox' : 'production'}:${marketplaceId}`;
|
return productCategory.marketplaces;
|
||||||
if (categoryTreeIds.has(cacheKey)) {
|
}
|
||||||
return categoryTreeIds.get(cacheKey);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await makeRequest({
|
export function getProductCategoryExternalReference(productCategory, marketplace) {
|
||||||
marketplace,
|
const marketplaceId = idOf(marketplace);
|
||||||
path: '/commerce/taxonomy/v1/get_default_category_tree_id',
|
const mappings = getProductCategoryMarketplaceMappings(productCategory);
|
||||||
params: { marketplace_id: marketplaceId },
|
if (!marketplaceId) {
|
||||||
});
|
return mappings[0]?.externalReference || '';
|
||||||
|
|
||||||
if (!result?.categoryTreeId) {
|
|
||||||
throw new Error(`eBay did not return a category tree for marketplace "${marketplaceId}"`);
|
|
||||||
}
|
}
|
||||||
|
const mapping = mappings.find((entry) => idOf(entry?.marketplace) === marketplaceId);
|
||||||
categoryTreeIds.set(cacheKey, result.categoryTreeId);
|
return mapping?.externalReference || '';
|
||||||
return result.categoryTreeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectCategorySuggestion(suggestions, categoryName) {
|
|
||||||
const normalizedName = categoryName.trim().toLocaleLowerCase();
|
|
||||||
return (
|
|
||||||
suggestions.find(
|
|
||||||
(suggestion) =>
|
|
||||||
suggestion?.category?.categoryName?.trim().toLocaleLowerCase() === normalizedName
|
|
||||||
) || suggestions[0]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a FarmControl product category to the closest category in eBay's
|
* Resolves a FarmControl product category to the eBay category ID stored on
|
||||||
* marketplace taxonomy. eBay owns its category tree, so categories are matched
|
* the product category's marketplace mapping.
|
||||||
* rather than created.
|
|
||||||
*/
|
*/
|
||||||
export async function syncProductCategory(marketplace, listing, varients = []) {
|
export async function syncProductCategory(marketplace, listing, varients = []) {
|
||||||
if (listing?.categoryId) {
|
if (listing?.categoryId) {
|
||||||
@ -77,45 +70,22 @@ export async function syncProductCategory(marketplace, listing, varients = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const productCategory = await resolveProductCategory(listing, varients);
|
const productCategory = await resolveProductCategory(listing, varients);
|
||||||
if (!productCategory?.name) {
|
if (!productCategory) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`No product category found for listing "${listing?._reference}"; skipping eBay category sync`
|
`No product category found for listing "${listing?._reference}"; skipping eBay category sync`
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryTreeId = await fetchDefaultCategoryTreeId(marketplace);
|
const categoryId = getProductCategoryExternalReference(productCategory, marketplace);
|
||||||
const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
|
if (!categoryId) {
|
||||||
const cacheKey = `${categoryTreeId}:${productCategory.name.trim().toLocaleLowerCase()}`;
|
|
||||||
if (categoryMatches.has(cacheKey)) {
|
|
||||||
return categoryMatches.get(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
path: `/commerce/taxonomy/v1/category_tree/${encodeURIComponent(
|
|
||||||
categoryTreeId
|
|
||||||
)}/get_category_suggestions`,
|
|
||||||
params: { q: productCategory.name },
|
|
||||||
});
|
|
||||||
const suggestion = selectCategorySuggestion(
|
|
||||||
result?.categorySuggestions || [],
|
|
||||||
productCategory.name
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!suggestion?.category?.categoryId) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No eBay category found for product category "${productCategory.name}" on ${marketplaceId}`
|
`Product category "${productCategory.name}" requires an eBay category ID before it can be used on an eBay listing.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = {
|
|
||||||
categoryId: String(suggestion.category.categoryId),
|
|
||||||
categoryName: suggestion.category.categoryName,
|
|
||||||
};
|
|
||||||
categoryMatches.set(cacheKey, category);
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Matched product category "${productCategory.name}" to eBay category "${category.categoryName}" (${category.categoryId})`
|
`Using product category "${productCategory.name}" marketplace mapping ${categoryId} for listing "${listing?._reference}"`
|
||||||
);
|
);
|
||||||
return category;
|
return { categoryId: String(categoryId) };
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/integrations/marketplaces/ebay/categoryTree.js
Normal file
65
src/integrations/marketplaces/ebay/categoryTree.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
|
||||||
|
function mapCategoryNode(node) {
|
||||||
|
const categoryId = node?.category?.categoryId;
|
||||||
|
if (categoryId == null || categoryId === '') return null;
|
||||||
|
|
||||||
|
const children = (node.childCategoryTreeNodes || []).map(mapCategoryNode).filter(Boolean);
|
||||||
|
const isLeaf = node.leafCategoryTreeNode === true || children.length === 0;
|
||||||
|
const categoryName = node.category.categoryName || String(categoryId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${categoryName} (${categoryId})`,
|
||||||
|
value: String(categoryId),
|
||||||
|
selectable: isLeaf,
|
||||||
|
...(children.length ? { children } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapCategoryTreeToReferences(rootCategoryNode) {
|
||||||
|
const mapped = mapCategoryNode(rootCategoryNode);
|
||||||
|
if (!mapped) return [];
|
||||||
|
if (mapped.value === '0' && mapped.children?.length) {
|
||||||
|
return mapped.children;
|
||||||
|
}
|
||||||
|
return [mapped];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countCategoryReferences(nodes) {
|
||||||
|
let count = 0;
|
||||||
|
for (const node of nodes || []) {
|
||||||
|
count += 1;
|
||||||
|
if (node?.children?.length) {
|
||||||
|
count += countCategoryReferences(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEbayCategoryReferences(marketplace) {
|
||||||
|
const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
|
||||||
|
const treeIdResult = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/commerce/taxonomy/v1/get_default_category_tree_id',
|
||||||
|
params: { marketplace_id: marketplaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoryTreeId = treeIdResult?.categoryTreeId;
|
||||||
|
if (!categoryTreeId) {
|
||||||
|
throw new Error(`eBay did not return a category tree for marketplace "${marketplaceId}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: `/commerce/taxonomy/v1/category_tree/${encodeURIComponent(categoryTreeId)}`,
|
||||||
|
extraHeaders: { 'Accept-Encoding': 'gzip' },
|
||||||
|
logResponse: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoryReferences = mapCategoryTreeToReferences(tree?.rootCategoryNode);
|
||||||
|
const categoryCount = countCategoryReferences(categoryReferences);
|
||||||
|
logger.info(
|
||||||
|
`Fetched ${categoryCount} eBay categor${categoryCount === 1 ? 'y' : 'ies'} for marketplace "${marketplace.name}" (tree ${categoryTreeId})`
|
||||||
|
);
|
||||||
|
return categoryReferences;
|
||||||
|
}
|
||||||
142
src/integrations/marketplaces/ebay/description.js
Normal file
142
src/integrations/marketplaces/ebay/description.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
const NAMED_ENTITIES = {
|
||||||
|
nbsp: ' ',
|
||||||
|
amp: '&',
|
||||||
|
lt: '<',
|
||||||
|
gt: '>',
|
||||||
|
quot: '"',
|
||||||
|
apos: "'",
|
||||||
|
mdash: '—',
|
||||||
|
ndash: '–',
|
||||||
|
hellip: '…',
|
||||||
|
copy: '©',
|
||||||
|
reg: '®',
|
||||||
|
trade: '™',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRODUCT_DESCRIPTION_MAX_LENGTH = 4000;
|
||||||
|
const LISTING_DESCRIPTION_MAX_LENGTH = 500000;
|
||||||
|
|
||||||
|
function decodeHtmlEntities(text) {
|
||||||
|
return String(text || '').replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]+);/gi, (match, entity) => {
|
||||||
|
if (entity[0] === '#') {
|
||||||
|
const code =
|
||||||
|
entity[1] === 'x' || entity[1] === 'X'
|
||||||
|
? parseInt(entity.slice(2), 16)
|
||||||
|
: parseInt(entity.slice(1), 10);
|
||||||
|
if (!Number.isFinite(code) || code <= 0) return '';
|
||||||
|
if (code === 160) return ' ';
|
||||||
|
try {
|
||||||
|
return String.fromCodePoint(code);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mapped = NAMED_ENTITIES[entity.toLowerCase()];
|
||||||
|
return mapped !== undefined ? mapped : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSource(text) {
|
||||||
|
return decodeHtmlEntities(text)
|
||||||
|
.replace(/\u00A0/g, ' ')
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.replace(/[ \t]+\n/g, '\n')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeMarkdown(text) {
|
||||||
|
return /(?:^|\n)#{1,6}\s|(?:^|\n)[-*+]\s|(?:^|\n)\d+\.\s|\*\*|__|\[.+\]\(https?:/m.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function inlineMarkdown(text) {
|
||||||
|
return escapeHtml(text)
|
||||||
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
||||||
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, '<a href="$2">$1</a>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncate(text, maxLength) {
|
||||||
|
if (!maxLength || text.length <= maxLength) return text;
|
||||||
|
return text.slice(0, maxLength).trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripMarkdown(text) {
|
||||||
|
return text
|
||||||
|
.replace(/^#{1,6}\s+/gm, '')
|
||||||
|
.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||||
|
.replace(/__(.+?)__/g, '$1')
|
||||||
|
.replace(/\*(.+?)\*/g, '$1')
|
||||||
|
.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, '$1')
|
||||||
|
.replace(/^[-*+]\s+/gm, '')
|
||||||
|
.replace(/^\d+\.\s+/gm, '')
|
||||||
|
.replace(/[ \t]+/g, ' ')
|
||||||
|
.replace(/ *\n */g, '\n')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toEbayPlainDescription(text, { maxLength = PRODUCT_DESCRIPTION_MAX_LENGTH } = {}) {
|
||||||
|
return truncate(stripMarkdown(normalizeSource(text)), maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toEbayHtmlDescription(text, { maxLength = LISTING_DESCRIPTION_MAX_LENGTH } = {}) {
|
||||||
|
const source = normalizeSource(text);
|
||||||
|
if (!source) return '';
|
||||||
|
if (!looksLikeMarkdown(source)) {
|
||||||
|
return truncate(source, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = [];
|
||||||
|
let listType = null;
|
||||||
|
let listItems = [];
|
||||||
|
|
||||||
|
const flushList = () => {
|
||||||
|
if (!listType) return;
|
||||||
|
blocks.push(`<${listType}>${listItems.map((item) => `<li>${item}</li>`).join('')}</${listType}>`);
|
||||||
|
listType = null;
|
||||||
|
listItems = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const line of source.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
flushList();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heading = trimmed.match(/^(#{1,6})\s+(.*)$/);
|
||||||
|
if (heading) {
|
||||||
|
flushList();
|
||||||
|
blocks.push(`<p><strong>${inlineMarkdown(heading[2])}</strong></p>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unordered = trimmed.match(/^[-*+]\s+(.*)$/);
|
||||||
|
if (unordered) {
|
||||||
|
if (listType && listType !== 'ul') flushList();
|
||||||
|
listType = 'ul';
|
||||||
|
listItems.push(inlineMarkdown(unordered[1]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ordered = trimmed.match(/^\d+\.\s+(.*)$/);
|
||||||
|
if (ordered) {
|
||||||
|
if (listType && listType !== 'ol') flushList();
|
||||||
|
listType = 'ol';
|
||||||
|
listItems.push(inlineMarkdown(ordered[1]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushList();
|
||||||
|
blocks.push(`<p>${inlineMarkdown(trimmed)}</p>`);
|
||||||
|
}
|
||||||
|
flushList();
|
||||||
|
|
||||||
|
return truncate(blocks.join(''), maxLength);
|
||||||
|
}
|
||||||
@ -1,42 +1,21 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { courierServiceModel } from '../../../database/schemas/management/courierservice.schema.js';
|
import { courierServiceModel } from '../../../database/schemas/management/courierservice.schema.js';
|
||||||
|
import { fulfillmentPolicyModel } from '../../../database/schemas/sales/fulfillmentpolicy.schema.js';
|
||||||
import { makeRequest, logger } from './shared.js';
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
import {
|
||||||
|
POLICY_CATEGORY_TYPE,
|
||||||
|
buildCategoryTypes as buildSharedCategoryTypes,
|
||||||
|
ensureSellingPolicyManagement,
|
||||||
|
getEbayMarketplaceId,
|
||||||
|
idOf,
|
||||||
|
isDefaultAccountPolicy,
|
||||||
|
mappingExternalReference,
|
||||||
|
persistMarketplaceMapping,
|
||||||
|
updateAccountPolicy,
|
||||||
|
upsertLocalPolicyFromRemote,
|
||||||
|
} from './accountPolicies.js';
|
||||||
|
|
||||||
const SELLING_POLICY_PROGRAM = 'SELLING_POLICY_MANAGEMENT';
|
const FULFILLMENT_CATEGORY_TYPE = POLICY_CATEGORY_TYPE;
|
||||||
const FULFILLMENT_CATEGORY_TYPE = 'ALL_EXCLUDING_MOTORS_VEHICLES';
|
|
||||||
|
|
||||||
async function fetchOptedInPrograms(marketplace) {
|
|
||||||
const result = await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
path: '/sell/account/v1/program/get_opted_in_programs',
|
|
||||||
});
|
|
||||||
return result?.programs || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSellingPolicyManagement(programs) {
|
|
||||||
return programs.some((program) => program?.programType === SELLING_POLICY_PROGRAM);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureSellingPolicyManagement(marketplace) {
|
|
||||||
if (hasSellingPolicyManagement(await fetchOptedInPrograms(marketplace))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Opting eBay seller account into Selling Policy Management');
|
|
||||||
await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'POST',
|
|
||||||
path: '/sell/account/v1/program/opt_in',
|
|
||||||
body: { programType: SELLING_POLICY_PROGRAM },
|
|
||||||
acceptableStatuses: [409],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasSellingPolicyManagement(await fetchOptedInPrograms(marketplace))) {
|
|
||||||
throw new Error(
|
|
||||||
'eBay Selling Policy Management enrollment was requested but is not active yet. eBay can take up to 24 hours to process enrollment; retry publishing later.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPopulatedCourierService(service) {
|
function isPopulatedCourierService(service) {
|
||||||
return (
|
return (
|
||||||
@ -66,12 +45,6 @@ async function resolveCourierServices(listing) {
|
|||||||
return serviceIds.map((id) => servicesById.get(String(id))).filter(Boolean);
|
return serviceIds.map((id) => servicesById.get(String(id))).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function idOf(value) {
|
|
||||||
if (value == null) return '';
|
|
||||||
if (typeof value === 'object') return String(value._id || '');
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCourierServiceMarketplaceMappings(service) {
|
export function getCourierServiceMarketplaceMappings(service) {
|
||||||
if (Array.isArray(service?.marketplaces) && service.marketplaces.length) {
|
if (Array.isArray(service?.marketplaces) && service.marketplaces.length) {
|
||||||
return service.marketplaces;
|
return service.marketplaces;
|
||||||
@ -160,43 +133,11 @@ function buildShippingOption(optionType, services, defaultCurrency, marketplace)
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isDefaultFulfillmentPolicy(existingPolicy, allPolicies = []) {
|
export function isDefaultFulfillmentPolicy(existingPolicy, allPolicies = []) {
|
||||||
if (!existingPolicy?.fulfillmentPolicyId) {
|
return isDefaultAccountPolicy(existingPolicy, allPolicies, 'fulfillmentPolicyId');
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingId = String(existingPolicy.fulfillmentPolicyId);
|
|
||||||
const sources = [existingPolicy, ...allPolicies];
|
|
||||||
if (
|
|
||||||
sources.some(
|
|
||||||
(policy) =>
|
|
||||||
String(policy?.fulfillmentPolicyId) === existingId &&
|
|
||||||
(policy.categoryTypes || []).some((type) => type?.default === true)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueIds = [
|
|
||||||
...new Set(
|
|
||||||
allPolicies
|
|
||||||
.filter((policy) => policy?.fulfillmentPolicyId)
|
|
||||||
.map((policy) => String(policy.fulfillmentPolicyId))
|
|
||||||
),
|
|
||||||
];
|
|
||||||
return uniqueIds.length === 1 && uniqueIds[0] === existingId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCategoryTypes(existingPolicy, allPolicies = []) {
|
export function buildCategoryTypes(existingPolicy, allPolicies = []) {
|
||||||
const categoryType = { name: FULFILLMENT_CATEGORY_TYPE };
|
return buildSharedCategoryTypes(existingPolicy, allPolicies, 'fulfillmentPolicyId');
|
||||||
|
|
||||||
// Never send default: false. eBay's GET can report false for the account-default
|
|
||||||
// policy, and updateFulfillmentPolicy then rejects it as changing default status
|
|
||||||
// (20403). Keep the default flag only when this policy is (or must remain) default.
|
|
||||||
if (isDefaultFulfillmentPolicy(existingPolicy, allPolicies)) {
|
|
||||||
categoryType.default = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [categoryType];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildFulfillmentPolicy(
|
export function buildFulfillmentPolicy(
|
||||||
@ -204,9 +145,10 @@ export function buildFulfillmentPolicy(
|
|||||||
marketplace,
|
marketplace,
|
||||||
services,
|
services,
|
||||||
existingPolicy,
|
existingPolicy,
|
||||||
allPolicies = []
|
allPolicies = [],
|
||||||
|
overrides = {}
|
||||||
) {
|
) {
|
||||||
const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
|
const marketplaceId = getEbayMarketplaceId(marketplace);
|
||||||
const defaultCurrency = marketplace.config?.currency || listing.currency || 'GBP';
|
const defaultCurrency = marketplace.config?.currency || listing.currency || 'GBP';
|
||||||
const domesticServices = services.filter((service) => !service.international);
|
const domesticServices = services.filter((service) => !service.international);
|
||||||
const internationalServices = services.filter((service) => service.international);
|
const internationalServices = services.filter((service) => service.international);
|
||||||
@ -214,24 +156,92 @@ export function buildFulfillmentPolicy(
|
|||||||
buildShippingOption('DOMESTIC', domesticServices, defaultCurrency, marketplace),
|
buildShippingOption('DOMESTIC', domesticServices, defaultCurrency, marketplace),
|
||||||
buildShippingOption('INTERNATIONAL', internationalServices, defaultCurrency, marketplace),
|
buildShippingOption('INTERNATIONAL', internationalServices, defaultCurrency, marketplace),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const deliveryTime = Math.max(0, ...services.map((service) => Number(service.deliveryTime ?? 1)));
|
const deliveryTime =
|
||||||
|
overrides.handlingTime != null
|
||||||
|
? Number(overrides.handlingTime)
|
||||||
|
: Math.max(0, ...services.map((service) => Number(service.deliveryTime ?? 1)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: `FarmControl ${listing._reference}`.slice(0, 64),
|
name: String(overrides.name || `FarmControl ${listing._reference || ''}`).slice(0, 64),
|
||||||
description: `Managed by FarmControl for listing ${listing._reference}`.slice(0, 250),
|
description: String(
|
||||||
|
overrides.description || `Managed by FarmControl for listing ${listing._reference || ''}`
|
||||||
|
).slice(0, 250),
|
||||||
marketplaceId,
|
marketplaceId,
|
||||||
categoryTypes: buildCategoryTypes(existingPolicy, allPolicies),
|
categoryTypes: buildCategoryTypes(existingPolicy, allPolicies),
|
||||||
handlingTime: { value: deliveryTime, unit: 'DAY' },
|
handlingTime: { value: Number.isFinite(deliveryTime) ? deliveryTime : 1, unit: 'DAY' },
|
||||||
localPickup: false,
|
localPickup: overrides.localPickup === true,
|
||||||
globalShipping: false,
|
globalShipping: overrides.globalShipping === true,
|
||||||
freightShipping: false,
|
freightShipping: overrides.freightShipping === true,
|
||||||
pickupDropOff: false,
|
pickupDropOff: overrides.pickupDropOff === true,
|
||||||
shippingOptions,
|
shippingOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moneyKey(amount) {
|
||||||
|
if (amount == null || amount.value == null || amount.value === '') {
|
||||||
|
return '0:';
|
||||||
|
}
|
||||||
|
return `${Number(amount.value)}:${amount.currency || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shippingServiceFingerprint(service = {}) {
|
||||||
|
return [
|
||||||
|
service.sortOrder || 1,
|
||||||
|
service.shippingServiceCode || '',
|
||||||
|
service.freeShipping ? '1' : '0',
|
||||||
|
service.buyerResponsibleForShipping ? '1' : '0',
|
||||||
|
moneyKey(service.shippingCost),
|
||||||
|
moneyKey(service.additionalShippingCost),
|
||||||
|
].join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shippingOptionFingerprint(option = {}) {
|
||||||
|
const services = [...(option.shippingServices || [])]
|
||||||
|
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
|
||||||
|
.map(shippingServiceFingerprint);
|
||||||
|
return `${option.optionType || ''}|${option.costType || ''}|${services.join(',')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fulfillmentPolicyFingerprint(policy) {
|
||||||
|
const options = [...(policy?.shippingOptions || [])]
|
||||||
|
.sort((a, b) => String(a?.optionType || '').localeCompare(String(b?.optionType || '')))
|
||||||
|
.map(shippingOptionFingerprint);
|
||||||
|
const handling = policy?.handlingTime || {};
|
||||||
|
return [
|
||||||
|
policy?.marketplaceId || '',
|
||||||
|
Number(handling.value) || 0,
|
||||||
|
handling.unit || 'DAY',
|
||||||
|
policy?.localPickup ? 1 : 0,
|
||||||
|
policy?.globalShipping ? 1 : 0,
|
||||||
|
policy?.freightShipping ? 1 : 0,
|
||||||
|
policy?.pickupDropOff ? 1 : 0,
|
||||||
|
options.join(';'),
|
||||||
|
].join('~');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMatchingFulfillmentPolicy(desiredPolicy, allPolicies = []) {
|
||||||
|
const desiredFingerprint = fulfillmentPolicyFingerprint(desiredPolicy);
|
||||||
|
return (
|
||||||
|
allPolicies.find(
|
||||||
|
(policy) =>
|
||||||
|
policy?.fulfillmentPolicyId && fulfillmentPolicyFingerprint(policy) === desiredFingerprint
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function duplicateFulfillmentPolicyId(err) {
|
||||||
|
const params = [];
|
||||||
|
for (const ebayError of err?.ebayErrors || []) {
|
||||||
|
if (Array.isArray(ebayError?.parameters)) params.push(...ebayError.parameters);
|
||||||
|
}
|
||||||
|
const match = params.find((param) =>
|
||||||
|
/^(DuplicateProfileId|Shipping Profile Id)$/i.test(param?.name || '')
|
||||||
|
);
|
||||||
|
return match?.value ? String(match.value) : '';
|
||||||
|
}
|
||||||
|
|
||||||
function getMarketplaceId(marketplace) {
|
function getMarketplaceId(marketplace) {
|
||||||
return marketplace.config?.marketplaceId || 'EBAY_GB';
|
return getEbayMarketplaceId(marketplace);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFulfillmentPolicyByName(marketplace, name) {
|
async function fetchFulfillmentPolicyByName(marketplace, name) {
|
||||||
@ -256,33 +266,186 @@ async function fetchFulfillmentPolicies(marketplace) {
|
|||||||
return result?.fulfillmentPolicies || [];
|
return result?.fulfillmentPolicies || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDefaultStatusError(err) {
|
export { fetchFulfillmentPolicies };
|
||||||
return /changing the default status/i.test(err?.message || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateFulfillmentPolicy(marketplace, fulfillmentPolicyId, policy) {
|
async function updateFulfillmentPolicy(marketplace, fulfillmentPolicyId, policy) {
|
||||||
const path = `/sell/account/v1/fulfillment_policy/${encodeURIComponent(fulfillmentPolicyId)}`;
|
await updateAccountPolicy(
|
||||||
try {
|
marketplace,
|
||||||
await makeRequest({ marketplace, method: 'PUT', path, body: policy });
|
`/sell/account/v1/fulfillment_policy/${encodeURIComponent(fulfillmentPolicyId)}`,
|
||||||
return;
|
policy
|
||||||
} catch (err) {
|
);
|
||||||
if (policy.categoryTypes?.[0]?.default === true || !isDefaultStatusError(err)) {
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
async function resolvePolicyCourierServices(policy) {
|
||||||
`Retrying fulfillment policy ${fulfillmentPolicyId} with categoryTypes.default=true`
|
const serviceRefs = policy?.courierServices || [];
|
||||||
);
|
if (!serviceRefs.length) return [];
|
||||||
await makeRequest({
|
const populatedServices = serviceRefs.filter(isPopulatedCourierService);
|
||||||
marketplace,
|
if (populatedServices.length === serviceRefs.length) {
|
||||||
method: 'PUT',
|
return populatedServices;
|
||||||
path,
|
|
||||||
body: {
|
|
||||||
...policy,
|
|
||||||
categoryTypes: [{ name: FULFILLMENT_CATEGORY_TYPE, default: true }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
const serviceIds = serviceRefs.map((service) => service?._id || service).filter(Boolean);
|
||||||
|
const services = await courierServiceModel
|
||||||
|
.find({ _id: { $in: serviceIds } })
|
||||||
|
.populate(['courier', 'marketplaces.marketplace'])
|
||||||
|
.lean();
|
||||||
|
const servicesById = new Map(services.map((service) => [String(service._id), service]));
|
||||||
|
return serviceIds.map((id) => servicesById.get(String(id))).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureFulfillmentPolicySynced(marketplace, policy) {
|
||||||
|
if (!policy) {
|
||||||
|
throw new Error('A fulfillment policy is required before publishing to eBay.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureSellingPolicyManagement(marketplace);
|
||||||
|
await persistMarketplaceMapping(fulfillmentPolicyModel, policy, marketplace, {
|
||||||
|
stateType: 'syncing',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const services = validateCourierServices(
|
||||||
|
await resolvePolicyCourierServices(policy),
|
||||||
|
{ _reference: policy._reference || policy.name },
|
||||||
|
marketplace
|
||||||
|
);
|
||||||
|
const existingId = mappingExternalReference(policy, marketplace);
|
||||||
|
const [existingByName, allPolicies] = await Promise.all([
|
||||||
|
existingId ? null : fetchFulfillmentPolicyByName(marketplace, String(policy.name).slice(0, 64)),
|
||||||
|
fetchFulfillmentPolicies(marketplace),
|
||||||
|
]);
|
||||||
|
const existingPolicy =
|
||||||
|
(existingId &&
|
||||||
|
allPolicies.find((item) => String(item.fulfillmentPolicyId) === String(existingId))) ||
|
||||||
|
existingByName ||
|
||||||
|
null;
|
||||||
|
const payload = buildFulfillmentPolicy(
|
||||||
|
{ _reference: policy._reference, currency: marketplace.config?.currency || 'GBP' },
|
||||||
|
marketplace,
|
||||||
|
services,
|
||||||
|
existingPolicy,
|
||||||
|
allPolicies,
|
||||||
|
{
|
||||||
|
name: policy.name,
|
||||||
|
description: policy.description,
|
||||||
|
handlingTime: policy.handlingTime,
|
||||||
|
localPickup: policy.localPickup,
|
||||||
|
globalShipping: policy.globalShipping,
|
||||||
|
freightShipping: policy.freightShipping,
|
||||||
|
pickupDropOff: policy.pickupDropOff,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let fulfillmentPolicyId = existingId || existingPolicy?.fulfillmentPolicyId;
|
||||||
|
|
||||||
|
if (fulfillmentPolicyId) {
|
||||||
|
await updateFulfillmentPolicy(marketplace, fulfillmentPolicyId, payload);
|
||||||
|
} else {
|
||||||
|
const matchingPolicy = findMatchingFulfillmentPolicy(payload, allPolicies);
|
||||||
|
if (matchingPolicy?.fulfillmentPolicyId) {
|
||||||
|
fulfillmentPolicyId = matchingPolicy.fulfillmentPolicyId;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const created = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/account/v1/fulfillment_policy',
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
fulfillmentPolicyId = created?.fulfillmentPolicyId;
|
||||||
|
} catch (err) {
|
||||||
|
const duplicateId = duplicateFulfillmentPolicyId(err);
|
||||||
|
if (!duplicateId) throw err;
|
||||||
|
fulfillmentPolicyId = duplicateId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fulfillmentPolicyId) {
|
||||||
|
throw new Error(`eBay did not return an ID for fulfillment policy "${payload.name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await persistMarketplaceMapping(fulfillmentPolicyModel, policy, marketplace, {
|
||||||
|
externalReference: String(fulfillmentPolicyId),
|
||||||
|
stateType: 'ready',
|
||||||
|
});
|
||||||
|
logger.info(`Synced eBay fulfillment policy "${payload.name}" (${fulfillmentPolicyId})`);
|
||||||
|
return { fulfillmentPolicyId: String(fulfillmentPolicyId) };
|
||||||
|
} catch (err) {
|
||||||
|
await persistMarketplaceMapping(fulfillmentPolicyModel, policy, marketplace, {
|
||||||
|
stateType: 'failed',
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRemoteShippingCodes(remote) {
|
||||||
|
const codes = [];
|
||||||
|
for (const option of remote?.shippingOptions || []) {
|
||||||
|
for (const service of option.shippingServices || []) {
|
||||||
|
if (service.shippingServiceCode) codes.push(String(service.shippingServiceCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function matchCourierServicesByShippingCodes(marketplace, codes) {
|
||||||
|
if (!codes.length) return [];
|
||||||
|
const services = await courierServiceModel
|
||||||
|
.find({ 'marketplaces.marketplace': idOf(marketplace) })
|
||||||
|
.lean();
|
||||||
|
const matched = [];
|
||||||
|
for (const code of codes) {
|
||||||
|
const service = services.find((item) =>
|
||||||
|
(item.marketplaces || []).some(
|
||||||
|
(entry) =>
|
||||||
|
idOf(entry.marketplace) === idOf(marketplace) &&
|
||||||
|
String(entry.externalReference) === String(code)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (service && !matched.some((item) => String(item._id) === String(service._id))) {
|
||||||
|
matched.push(service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched.map((service) => service._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRemoteFulfillmentPolicy(remote, courierServiceIds) {
|
||||||
|
return {
|
||||||
|
description: remote.description,
|
||||||
|
handlingTime: Number(remote.handlingTime?.value) || 1,
|
||||||
|
localPickup: Boolean(remote.localPickup),
|
||||||
|
globalShipping: Boolean(remote.globalShipping),
|
||||||
|
freightShipping: Boolean(remote.freightShipping),
|
||||||
|
pickupDropOff: Boolean(remote.pickupDropOff),
|
||||||
|
courierServices: courierServiceIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncFulfillmentPoliciesFromEbay(marketplace) {
|
||||||
|
await ensureSellingPolicyManagement(marketplace);
|
||||||
|
const remotes = await fetchFulfillmentPolicies(marketplace);
|
||||||
|
const upserted = [];
|
||||||
|
for (const remote of remotes) {
|
||||||
|
const fulfillmentPolicyId = remote.fulfillmentPolicyId;
|
||||||
|
if (!fulfillmentPolicyId || !remote.name) continue;
|
||||||
|
const courierServiceIds = await matchCourierServicesByShippingCodes(
|
||||||
|
marketplace,
|
||||||
|
collectRemoteShippingCodes(remote)
|
||||||
|
);
|
||||||
|
upserted.push(
|
||||||
|
await upsertLocalPolicyFromRemote({
|
||||||
|
model: fulfillmentPolicyModel,
|
||||||
|
marketplace,
|
||||||
|
remoteId: fulfillmentPolicyId,
|
||||||
|
name: remote.name,
|
||||||
|
fields: mapRemoteFulfillmentPolicy(remote, courierServiceIds),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
`Imported ${upserted.length} eBay fulfillment polic${upserted.length === 1 ? 'y' : 'ies'}`
|
||||||
|
);
|
||||||
|
return { count: upserted.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncFulfillmentPolicy(marketplace, listing) {
|
export async function syncFulfillmentPolicy(marketplace, listing) {
|
||||||
@ -318,6 +481,15 @@ export async function syncFulfillmentPolicy(marketplace, listing) {
|
|||||||
return { fulfillmentPolicyId: String(existingPolicy.fulfillmentPolicyId) };
|
return { fulfillmentPolicyId: String(existingPolicy.fulfillmentPolicyId) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matchingPolicy = findMatchingFulfillmentPolicy(policy, allPolicies);
|
||||||
|
if (matchingPolicy?.fulfillmentPolicyId) {
|
||||||
|
logger.info(
|
||||||
|
`Reusing eBay fulfillment policy "${matchingPolicy.name}" (${matchingPolicy.fulfillmentPolicyId}) with matching shipping settings`
|
||||||
|
);
|
||||||
|
return { fulfillmentPolicyId: String(matchingPolicy.fulfillmentPolicyId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const result = await makeRequest({
|
const result = await makeRequest({
|
||||||
marketplace,
|
marketplace,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -330,4 +502,14 @@ export async function syncFulfillmentPolicy(marketplace, listing) {
|
|||||||
|
|
||||||
logger.info(`Created eBay fulfillment policy "${policy.name}" (${result.fulfillmentPolicyId})`);
|
logger.info(`Created eBay fulfillment policy "${policy.name}" (${result.fulfillmentPolicyId})`);
|
||||||
return { fulfillmentPolicyId: String(result.fulfillmentPolicyId) };
|
return { fulfillmentPolicyId: String(result.fulfillmentPolicyId) };
|
||||||
|
} catch (err) {
|
||||||
|
const duplicateId = duplicateFulfillmentPolicyId(err);
|
||||||
|
if (duplicateId) {
|
||||||
|
logger.info(
|
||||||
|
`Reusing existing eBay fulfillment policy ${duplicateId} after duplicate-policy response`
|
||||||
|
);
|
||||||
|
return { fulfillmentPolicyId: duplicateId };
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
151
src/integrations/marketplaces/ebay/images.js
Normal file
151
src/integrations/marketplaces/ebay/images.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { downloadFile, BUCKETS } from '../../../database/ceph.js';
|
||||||
|
import { getMediaApiBaseUrl, makeRequest, logger } from './shared.js';
|
||||||
|
|
||||||
|
function fileId(file) {
|
||||||
|
if (!file) return null;
|
||||||
|
if (typeof file === 'string') return file;
|
||||||
|
return file._id || file.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileExtension(file) {
|
||||||
|
if (!file || typeof file === 'string') return '';
|
||||||
|
return file.extension || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileContentType(file) {
|
||||||
|
if (!file || typeof file === 'string') return 'application/octet-stream';
|
||||||
|
return file.type || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFileName(name) {
|
||||||
|
const sanitized = String(name || 'image')
|
||||||
|
.replace(/[\r\n"]/g, '_')
|
||||||
|
.replace(/[^\w.\-]+/g, '_')
|
||||||
|
.slice(0, 120);
|
||||||
|
return sanitized || 'image';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileUploadName(file) {
|
||||||
|
if (file && typeof file !== 'string' && file.name) {
|
||||||
|
return sanitizeFileName(file.name);
|
||||||
|
}
|
||||||
|
const id = fileId(file) || 'image';
|
||||||
|
let ext = fileExtension(file) || '';
|
||||||
|
if (ext && !ext.startsWith('.')) ext = `.${ext}`;
|
||||||
|
return sanitizeFileName(`${id}${ext || '.jpg'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildImageUploadBody(file, bytes) {
|
||||||
|
const boundary = `----EbayFormBoundary${process.hrtime.bigint().toString(16)}`;
|
||||||
|
const filename = fileUploadName(file);
|
||||||
|
const type = fileContentType(file);
|
||||||
|
const header = Buffer.from(
|
||||||
|
`--${boundary}\r\n` +
|
||||||
|
`Content-Disposition: form-data; name="image"; filename="${filename}"\r\n` +
|
||||||
|
`Content-Type: ${type}\r\n` +
|
||||||
|
`\r\n`
|
||||||
|
);
|
||||||
|
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
||||||
|
const payload = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
||||||
|
return {
|
||||||
|
body: Buffer.concat([header, payload, footer]),
|
||||||
|
contentType: `multipart/form-data; boundary=${boundary}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function streamToBuffer(body) {
|
||||||
|
if (body == null) return Buffer.alloc(0);
|
||||||
|
if (Buffer.isBuffer(body)) return body;
|
||||||
|
if (body instanceof Uint8Array) return Buffer.from(body);
|
||||||
|
if (typeof body.transformToByteArray === 'function') {
|
||||||
|
return Buffer.from(await body.transformToByteArray());
|
||||||
|
}
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of body) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks.map((chunk) => (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getListingImageFiles(listing, varient) {
|
||||||
|
if (varient?.listingImages?.length) return varient.listingImages;
|
||||||
|
if (listing?.listingImages?.length) return listing.listingImages;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractImageUrl(data) {
|
||||||
|
if (!data) return null;
|
||||||
|
if (typeof data === 'string' && data.startsWith('http')) return data;
|
||||||
|
return data.imageUrl || data.image?.imageUrl || data.image?.url || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFileToEbay(marketplace, file, bytes) {
|
||||||
|
const { body, contentType } = buildImageUploadBody(file, bytes);
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/commerce/media/v1_beta/image/create_image_from_file',
|
||||||
|
baseUrl: getMediaApiBaseUrl(marketplace),
|
||||||
|
body,
|
||||||
|
rawBody: true,
|
||||||
|
contentType,
|
||||||
|
});
|
||||||
|
const imageUrl = extractImageUrl(result);
|
||||||
|
if (!imageUrl) {
|
||||||
|
throw new Error(
|
||||||
|
`eBay Media API did not return an image URL for file ${fileId(file) || 'unknown'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function filesToImageUrls(marketplace, files = []) {
|
||||||
|
const urls = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const id = fileId(file);
|
||||||
|
if (!id) continue;
|
||||||
|
const extension = fileExtension(file);
|
||||||
|
const cephKey = `files/${id}${extension}`;
|
||||||
|
try {
|
||||||
|
const body = await downloadFile(BUCKETS.FILES, cephKey);
|
||||||
|
const bytes = await streamToBuffer(body);
|
||||||
|
const imageUrl = await uploadFileToEbay(marketplace, file, bytes);
|
||||||
|
urls.push(imageUrl);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Failed to upload listing image ${id} to eBay: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveListingImageUrls(marketplace, listing, varient) {
|
||||||
|
const files = getListingImageFiles(listing, varient);
|
||||||
|
if (files.length) {
|
||||||
|
return filesToImageUrls(marketplace, files);
|
||||||
|
}
|
||||||
|
if (varient?.imageUrls?.length) return varient.imageUrls;
|
||||||
|
if (listing?.imageUrls?.length) return listing.imageUrls;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function attachImageUrlsToListingAndVarients(marketplace, listing, varients = []) {
|
||||||
|
const listingImageUrls = await resolveListingImageUrls(marketplace, listing);
|
||||||
|
const listingWithUrls = listingImageUrls.length
|
||||||
|
? { ...listing, imageUrls: listingImageUrls }
|
||||||
|
: listing;
|
||||||
|
|
||||||
|
const varientsWithUrls = [];
|
||||||
|
for (const varient of varients) {
|
||||||
|
if (varient?.listingImages?.length) {
|
||||||
|
const urls = await filesToImageUrls(marketplace, varient.listingImages);
|
||||||
|
varientsWithUrls.push(urls.length ? { ...varient, imageUrls: urls } : varient);
|
||||||
|
} else if (listingImageUrls.length) {
|
||||||
|
varientsWithUrls.push({ ...varient, imageUrls: listingImageUrls });
|
||||||
|
} else {
|
||||||
|
varientsWithUrls.push(varient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { listing: listingWithUrls, varients: varientsWithUrls };
|
||||||
|
}
|
||||||
@ -7,10 +7,30 @@ export {
|
|||||||
verifyWebhookSignature,
|
verifyWebhookSignature,
|
||||||
} from './auth.js';
|
} from './auth.js';
|
||||||
|
|
||||||
export { syncItems, mapProductToListing, createItem, updateItem, deleteItem } from './listings.js';
|
export {
|
||||||
|
syncItems,
|
||||||
|
mapProductToListing,
|
||||||
|
createItem,
|
||||||
|
updateItem,
|
||||||
|
deleteItem,
|
||||||
|
syncListingImages,
|
||||||
|
} from './listings.js';
|
||||||
|
|
||||||
export { syncProductCategory } from './categories.js';
|
export { syncProductCategory } from './categories.js';
|
||||||
export { syncFulfillmentPolicy } from './fulfillmentPolicies.js';
|
export {
|
||||||
|
syncFulfillmentPolicy,
|
||||||
|
syncFulfillmentPoliciesFromEbay,
|
||||||
|
ensureFulfillmentPolicySynced,
|
||||||
|
} from './fulfillmentPolicies.js';
|
||||||
|
export { syncListingPolicies } from './listingPolicies.js';
|
||||||
|
export { syncPaymentPoliciesFromEbay, ensurePaymentPolicySynced } from './paymentPolicies.js';
|
||||||
|
export { syncReturnPoliciesFromEbay, ensureReturnPolicySynced } from './returnPolicies.js';
|
||||||
|
export {
|
||||||
|
syncTaxRatesFromEbay,
|
||||||
|
syncTaxRatesToEbay,
|
||||||
|
syncTaxRates,
|
||||||
|
ensureTaxRateSynced,
|
||||||
|
} from './salesTax.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
publishOfferById,
|
publishOfferById,
|
||||||
@ -42,3 +62,7 @@ export {
|
|||||||
|
|
||||||
export { makeRequest as debugGet } from './shared.js';
|
export { makeRequest as debugGet } from './shared.js';
|
||||||
export { syncMarketplaceMetadata } from './shippingServices.js';
|
export { syncMarketplaceMetadata } from './shippingServices.js';
|
||||||
|
export { syncAccountPolicies } from './accountPolicySync.js';
|
||||||
|
export { syncFulfillmentPoliciesFromEbay as syncFulfillmentPolicies } from './fulfillmentPolicies.js';
|
||||||
|
export { syncPaymentPoliciesFromEbay as syncPaymentPolicies } from './paymentPolicies.js';
|
||||||
|
export { syncReturnPoliciesFromEbay as syncReturnPolicies } from './returnPolicies.js';
|
||||||
|
|||||||
15
src/integrations/marketplaces/ebay/itemUrl.js
Normal file
15
src/integrations/marketplaces/ebay/itemUrl.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export function parseEbayItemId(value) {
|
||||||
|
if (value == null) return '';
|
||||||
|
const text = String(value).trim();
|
||||||
|
if (!text) return '';
|
||||||
|
const fromUrl = text.match(/\/itm\/(\d+)/i);
|
||||||
|
if (fromUrl) return fromUrl[1];
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEbayItemUrl(marketplace, listingId) {
|
||||||
|
const id = parseEbayItemId(listingId);
|
||||||
|
if (!id) return '';
|
||||||
|
const host = marketplace?.config?.sandbox ? 'https://sandbox.ebay.com' : 'https://www.ebay.com';
|
||||||
|
return `${host}/itm/${id}`;
|
||||||
|
}
|
||||||
68
src/integrations/marketplaces/ebay/listingPolicies.js
Normal file
68
src/integrations/marketplaces/ebay/listingPolicies.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { fulfillmentPolicyModel } from '../../../database/schemas/sales/fulfillmentpolicy.schema.js';
|
||||||
|
import { paymentPolicyModel } from '../../../database/schemas/finance/paymentpolicy.schema.js';
|
||||||
|
import { returnPolicyModel } from '../../../database/schemas/sales/returnpolicy.schema.js';
|
||||||
|
import {
|
||||||
|
loadPolicyDocument,
|
||||||
|
resolveListingPolicy,
|
||||||
|
} from './accountPolicies.js';
|
||||||
|
import {
|
||||||
|
ensureFulfillmentPolicySynced,
|
||||||
|
syncFulfillmentPolicy,
|
||||||
|
} from './fulfillmentPolicies.js';
|
||||||
|
import { ensurePaymentPolicySynced } from './paymentPolicies.js';
|
||||||
|
import { ensureReturnPolicySynced } from './returnPolicies.js';
|
||||||
|
|
||||||
|
async function loadFulfillmentPolicy(value) {
|
||||||
|
const policy = await loadPolicyDocument(fulfillmentPolicyModel, value);
|
||||||
|
if (!policy) return null;
|
||||||
|
if (Array.isArray(policy.courierServices) && policy.courierServices.length) {
|
||||||
|
return fulfillmentPolicyModel
|
||||||
|
.findById(policy._id)
|
||||||
|
.populate(['courierServices', 'marketplaces.marketplace'])
|
||||||
|
.lean();
|
||||||
|
}
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncListingPolicies(marketplace, listing) {
|
||||||
|
const fulfillmentRef = resolveListingPolicy(
|
||||||
|
listing,
|
||||||
|
marketplace,
|
||||||
|
'fulfillmentPolicy',
|
||||||
|
'defaultFulfillmentPolicy'
|
||||||
|
);
|
||||||
|
const paymentRef = resolveListingPolicy(
|
||||||
|
listing,
|
||||||
|
marketplace,
|
||||||
|
'paymentPolicy',
|
||||||
|
'defaultPaymentPolicy'
|
||||||
|
);
|
||||||
|
const returnRef = resolveListingPolicy(
|
||||||
|
listing,
|
||||||
|
marketplace,
|
||||||
|
'returnPolicy',
|
||||||
|
'defaultReturnPolicy'
|
||||||
|
);
|
||||||
|
|
||||||
|
let fulfillmentPolicyId;
|
||||||
|
if (fulfillmentRef) {
|
||||||
|
const fulfillmentPolicy = await loadFulfillmentPolicy(fulfillmentRef);
|
||||||
|
({ fulfillmentPolicyId } = await ensureFulfillmentPolicySynced(
|
||||||
|
marketplace,
|
||||||
|
fulfillmentPolicy
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
({ fulfillmentPolicyId } = await syncFulfillmentPolicy(marketplace, listing));
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentPolicy = await loadPolicyDocument(paymentPolicyModel, paymentRef);
|
||||||
|
const returnPolicy = await loadPolicyDocument(returnPolicyModel, returnRef);
|
||||||
|
const payment = await ensurePaymentPolicySynced(marketplace, paymentPolicy);
|
||||||
|
const returned = await ensureReturnPolicySynced(marketplace, returnPolicy);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fulfillmentPolicyId,
|
||||||
|
paymentPolicyId: payment.paymentPolicyId,
|
||||||
|
returnPolicyId: returned.returnPolicyId,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,11 +1,13 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { stockLocationModel } from '../../../database/schemas/inventory/stocklocation.schema.js';
|
import { stockLocationModel } from '../../../database/schemas/inventory/stocklocation.schema.js';
|
||||||
import { productStockModel } from '../../../database/schemas/inventory/productstock.schema.js';
|
|
||||||
import { FARMCONTROL_GB_SUBDIVISION_STATE, resolveEbayCountry } from './countryCodes.js';
|
import { FARMCONTROL_GB_SUBDIVISION_STATE, resolveEbayCountry } from './countryCodes.js';
|
||||||
import { syncProductCategory } from './categories.js';
|
import { syncProductCategory } from './categories.js';
|
||||||
import { syncFulfillmentPolicy } from './fulfillmentPolicies.js';
|
import { syncListingPolicies } from './listingPolicies.js';
|
||||||
import { makeRequest, logger } from './shared.js';
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
import { getEbayItemUrl, parseEbayItemId } from './itemUrl.js';
|
||||||
import { marketplaceSku } from '../ids.js';
|
import { marketplaceSku } from '../ids.js';
|
||||||
|
import { fromEbayProductAspects, toEbayProductAspects } from './variationAspects.js';
|
||||||
|
import { toEbayHtmlDescription, toEbayPlainDescription } from './description.js';
|
||||||
|
|
||||||
const WAREHOUSE_ADDRESS_DEFAULTS = {
|
const WAREHOUSE_ADDRESS_DEFAULTS = {
|
||||||
GB: { city: 'London', stateOrProvince: 'England', postalCode: 'SW1A 1AA' },
|
GB: { city: 'London', stateOrProvince: 'England', postalCode: 'SW1A 1AA' },
|
||||||
@ -181,17 +183,20 @@ function applyMarketplaceOfferDefaults(
|
|||||||
offer,
|
offer,
|
||||||
marketplace,
|
marketplace,
|
||||||
merchantLocationKey,
|
merchantLocationKey,
|
||||||
fulfillmentPolicyId
|
listingPolicies = {}
|
||||||
) {
|
) {
|
||||||
offer.merchantLocationKey = merchantLocationKey;
|
offer.merchantLocationKey = merchantLocationKey;
|
||||||
const config = marketplace.config || {};
|
const policies = {};
|
||||||
const listingPolicies = {};
|
if (listingPolicies.fulfillmentPolicyId) {
|
||||||
if (fulfillmentPolicyId || config.fulfillmentPolicyId) {
|
policies.fulfillmentPolicyId = String(listingPolicies.fulfillmentPolicyId);
|
||||||
listingPolicies.fulfillmentPolicyId = fulfillmentPolicyId || config.fulfillmentPolicyId;
|
|
||||||
}
|
}
|
||||||
if (config.paymentPolicyId) listingPolicies.paymentPolicyId = config.paymentPolicyId;
|
if (listingPolicies.paymentPolicyId) {
|
||||||
if (config.returnPolicyId) listingPolicies.returnPolicyId = config.returnPolicyId;
|
policies.paymentPolicyId = String(listingPolicies.paymentPolicyId);
|
||||||
if (Object.keys(listingPolicies).length) offer.listingPolicies = listingPolicies;
|
}
|
||||||
|
if (listingPolicies.returnPolicyId) {
|
||||||
|
policies.returnPolicyId = String(listingPolicies.returnPolicyId);
|
||||||
|
}
|
||||||
|
if (Object.keys(policies).length) offer.listingPolicies = policies;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveListingDescription(listing, varient) {
|
export function resolveListingDescription(listing, varient) {
|
||||||
@ -211,32 +216,74 @@ export function resolveListingDescription(listing, varient) {
|
|||||||
return 'No description provided.';
|
return 'No description provided.';
|
||||||
}
|
}
|
||||||
|
|
||||||
function inventoryItemPutBody(existing, listing, sku, varient) {
|
export function resolveEbayProductDescription(listing, varient) {
|
||||||
const product = { ...(existing?.product || {}) };
|
return toEbayPlainDescription(resolveListingDescription(listing, varient));
|
||||||
product.title = product.title || listing?.title || sku;
|
}
|
||||||
product.description = resolveListingDescription(listing, varient);
|
|
||||||
const body = { product };
|
export function resolveEbayListingDescription(listing, varient) {
|
||||||
if (existing?.availability) body.availability = existing.availability;
|
return toEbayHtmlDescription(resolveListingDescription(listing, varient));
|
||||||
body.condition = toEbayCondition(listing?.condition || existing?.condition);
|
}
|
||||||
|
|
||||||
|
const PRODUCT_PUT_FIELDS = [
|
||||||
|
'aspects',
|
||||||
|
'brand',
|
||||||
|
'description',
|
||||||
|
'ean',
|
||||||
|
'epid',
|
||||||
|
'imageUrls',
|
||||||
|
'isbn',
|
||||||
|
'mpn',
|
||||||
|
'subtitle',
|
||||||
|
'title',
|
||||||
|
'upc',
|
||||||
|
'videoIds',
|
||||||
|
];
|
||||||
|
|
||||||
|
function pickDefined(source, keys) {
|
||||||
|
const result = {};
|
||||||
|
if (!source || typeof source !== 'object') return result;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (source[key] != null) result[key] = source[key];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inventoryItemPutBody(existing, listing, sku, varient, quantity) {
|
||||||
|
const product = pickDefined(existing?.product, PRODUCT_PUT_FIELDS);
|
||||||
|
product.title = listing?.title || product.title || sku;
|
||||||
|
product.description = resolveEbayProductDescription(listing, varient);
|
||||||
|
const aspects = toEbayProductAspects(varient?.aspects);
|
||||||
|
if (aspects) product.aspects = aspects;
|
||||||
|
const imageUrls = varient?.imageUrls?.length ? varient.imageUrls : listing?.imageUrls;
|
||||||
|
if (imageUrls?.length) product.imageUrls = imageUrls;
|
||||||
|
|
||||||
|
const shipTo = existing?.availability?.shipToLocationAvailability || {};
|
||||||
|
const availability = {
|
||||||
|
shipToLocationAvailability: { quantity },
|
||||||
|
};
|
||||||
|
if (Array.isArray(shipTo.availabilityDistributions) && shipTo.availabilityDistributions.length) {
|
||||||
|
availability.shipToLocationAvailability.availabilityDistributions =
|
||||||
|
shipTo.availabilityDistributions;
|
||||||
|
}
|
||||||
|
if (existing?.availability?.pickupAtLocationAvailability?.length) {
|
||||||
|
availability.pickupAtLocationAvailability = existing.availability.pickupAtLocationAvailability;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
product,
|
||||||
|
availability,
|
||||||
|
condition: toEbayCondition(listing?.condition || existing?.condition),
|
||||||
|
};
|
||||||
if (existing?.conditionDescription) body.conditionDescription = existing.conditionDescription;
|
if (existing?.conditionDescription) body.conditionDescription = existing.conditionDescription;
|
||||||
|
if (existing?.conditionDescriptors?.length) {
|
||||||
|
body.conditionDescriptors = existing.conditionDescriptors;
|
||||||
|
}
|
||||||
if (existing?.packageWeightAndSize) body.packageWeightAndSize = existing.packageWeightAndSize;
|
if (existing?.packageWeightAndSize) body.packageWeightAndSize = existing.packageWeightAndSize;
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureInventoryItemDescription(marketplace, sku, listing, varient) {
|
async function ensureInventoryItem(marketplace, sku, listing, varient) {
|
||||||
const existing = await makeRequest({
|
await upsertInventoryItem(marketplace, resolvePublishVarient(sku, varient), listing);
|
||||||
marketplace,
|
|
||||||
path: `/sell/inventory/v1/inventory_item/${encodeURIComponent(sku)}`,
|
|
||||||
acceptableStatuses: [404],
|
|
||||||
});
|
|
||||||
if (!existing) return;
|
|
||||||
|
|
||||||
await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'PUT',
|
|
||||||
path: `/sell/inventory/v1/inventory_item/${encodeURIComponent(sku)}`,
|
|
||||||
body: inventoryItemPutBody(existing, listing, sku, varient),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapVarientToInventoryItem(varient, listing, quantity = 0) {
|
function mapVarientToInventoryItem(varient, listing, quantity = 0) {
|
||||||
@ -244,29 +291,32 @@ function mapVarientToInventoryItem(varient, listing, quantity = 0) {
|
|||||||
condition: toEbayCondition(listing?.condition),
|
condition: toEbayCondition(listing?.condition),
|
||||||
product: {
|
product: {
|
||||||
title: listing.title || marketplaceSku(varient) || '',
|
title: listing.title || marketplaceSku(varient) || '',
|
||||||
description: resolveListingDescription(listing, varient),
|
description: resolveEbayProductDescription(listing, varient),
|
||||||
},
|
},
|
||||||
availability: {
|
availability: {
|
||||||
shipToLocationAvailability: { quantity },
|
shipToLocationAvailability: { quantity },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (listing.imageUrls?.length) item.product.imageUrls = listing.imageUrls;
|
const aspects = toEbayProductAspects(varient?.aspects);
|
||||||
|
if (aspects) item.product.aspects = aspects;
|
||||||
|
const imageUrls = varient?.imageUrls?.length ? varient.imageUrls : listing?.imageUrls;
|
||||||
|
if (imageUrls?.length) item.product.imageUrls = imageUrls;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapVarientToOffer(varient, listing, marketplace, merchantLocationKey) {
|
function mapVarientToOffer(varient, listing, marketplace, merchantLocationKey, quantity) {
|
||||||
const offer = {
|
const offer = {
|
||||||
sku: marketplaceSku(varient),
|
sku: marketplaceSku(varient),
|
||||||
marketplaceId: marketplace.config?.marketplaceId || 'EBAY_GB',
|
marketplaceId: marketplace.config?.marketplaceId || 'EBAY_GB',
|
||||||
format: 'FIXED_PRICE',
|
format: 'FIXED_PRICE',
|
||||||
listingDescription: resolveListingDescription(listing, varient),
|
listingDescription: resolveEbayListingDescription(listing, varient),
|
||||||
|
availableQuantity: quantity,
|
||||||
};
|
};
|
||||||
applyMarketplaceOfferDefaults(
|
applyMarketplaceOfferDefaults(offer, marketplace, merchantLocationKey, {
|
||||||
offer,
|
fulfillmentPolicyId: listing.fulfillmentPolicyId,
|
||||||
marketplace,
|
paymentPolicyId: listing.paymentPolicyId,
|
||||||
merchantLocationKey,
|
returnPolicyId: listing.returnPolicyId,
|
||||||
listing.fulfillmentPolicyId
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const price = varient.price ?? listing.price;
|
const price = varient.price ?? listing.price;
|
||||||
if (price != null) {
|
if (price != null) {
|
||||||
@ -281,24 +331,22 @@ function mapVarientToOffer(varient, listing, marketplace, merchantLocationKey) {
|
|||||||
return offer;
|
return offer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveVarientQuantity(varient, listing) {
|
export function resolveVarientQuantity(varient) {
|
||||||
const skuId = varient.productSku?._id || varient.productSku;
|
return Math.max(0, Number(varient?.stockQuantity) || 0);
|
||||||
if (!skuId) {
|
|
||||||
return Number(varient.inventory) || 0;
|
|
||||||
}
|
|
||||||
const locationId = listing?.stockLocation?._id || listing?.stockLocation;
|
|
||||||
const filter = { productSku: skuId };
|
|
||||||
if (locationId) {
|
|
||||||
filter.stockLocation = locationId;
|
|
||||||
}
|
|
||||||
const stocks = await productStockModel.find(filter).lean();
|
|
||||||
return stocks.reduce((sum, stock) => sum + (Number(stock.currentQuantity) || 0), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upsertInventoryItem(marketplace, varient, listing) {
|
export async function upsertInventoryItem(marketplace, varient, listing) {
|
||||||
const sku = marketplaceSku(varient);
|
const sku = marketplaceSku(varient);
|
||||||
const quantity = await resolveVarientQuantity(varient, listing);
|
if (!sku) throw new Error('SKU is required to upsert an eBay inventory item');
|
||||||
const inventoryItem = mapVarientToInventoryItem(varient, listing, quantity);
|
const quantity = resolveVarientQuantity(varient);
|
||||||
|
const existing = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: `/sell/inventory/v1/inventory_item/${encodeURIComponent(sku)}`,
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
});
|
||||||
|
const inventoryItem = existing
|
||||||
|
? inventoryItemPutBody(existing, listing, sku, varient, quantity)
|
||||||
|
: mapVarientToInventoryItem(varient, listing, quantity);
|
||||||
const result = await makeRequest({
|
const result = await makeRequest({
|
||||||
marketplace,
|
marketplace,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@ -309,6 +357,39 @@ export async function upsertInventoryItem(marketplace, varient, listing) {
|
|||||||
logger.debug('result', result);
|
logger.debug('result', result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveOfferListingId(offer) {
|
||||||
|
return parseEbayItemId(offer?.listingId || offer?.listing?.listingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storedEbayItemId(listing) {
|
||||||
|
return parseEbayItemId(listing?.externalReference) || parseEbayItemId(listing?.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOfferPublished(offer) {
|
||||||
|
if (!offer) return false;
|
||||||
|
if (offer.status === 'PUBLISHED') return true;
|
||||||
|
const listingStatus = offer.listing?.listingStatus;
|
||||||
|
return listingStatus === 'ACTIVE' || listingStatus === 'OUT_OF_STOCK';
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOfferForListing(offers = [], listing) {
|
||||||
|
const wantedId = storedEbayItemId(listing);
|
||||||
|
if (wantedId) {
|
||||||
|
const match = offers.find((offer) => resolveOfferListingId(offer) === wantedId);
|
||||||
|
if (match) return match;
|
||||||
|
}
|
||||||
|
return offers[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ebayListingPublishResult(marketplace, { offerId, listingId } = {}) {
|
||||||
|
const id = parseEbayItemId(listingId);
|
||||||
|
return {
|
||||||
|
...(offerId ? { offerId } : {}),
|
||||||
|
listingId: id || undefined,
|
||||||
|
...(id ? { externalReference: id, url: getEbayItemUrl(marketplace, id) } : { url: '' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchOffers(marketplace, sku) {
|
export async function fetchOffers(marketplace, sku) {
|
||||||
try {
|
try {
|
||||||
const data = await makeRequest({
|
const data = await makeRequest({
|
||||||
@ -327,13 +408,15 @@ export async function fetchOffers(marketplace, sku) {
|
|||||||
async function upsertOrCreateOffer(marketplace, varient, listing) {
|
async function upsertOrCreateOffer(marketplace, varient, listing) {
|
||||||
const merchantLocationKey = await ensureMerchantLocation(marketplace, listing);
|
const merchantLocationKey = await ensureMerchantLocation(marketplace, listing);
|
||||||
const offers = await fetchOffers(marketplace, marketplaceSku(varient));
|
const offers = await fetchOffers(marketplace, marketplaceSku(varient));
|
||||||
const existingOffer = offers[0];
|
const existingOffer = findOfferForListing(offers, listing);
|
||||||
const price = varient.price ?? listing.price;
|
const price = varient.price ?? listing.price;
|
||||||
|
const quantity = resolveVarientQuantity(varient);
|
||||||
|
|
||||||
if (existingOffer?.offerId) {
|
if (existingOffer?.offerId) {
|
||||||
const offerUpdate = {
|
const offerUpdate = {
|
||||||
merchantLocationKey,
|
merchantLocationKey,
|
||||||
listingDescription: resolveListingDescription(listing, varient),
|
listingDescription: resolveEbayListingDescription(listing, varient),
|
||||||
|
availableQuantity: quantity,
|
||||||
};
|
};
|
||||||
if (price != null) {
|
if (price != null) {
|
||||||
offerUpdate.pricingSummary = {
|
offerUpdate.pricingSummary = {
|
||||||
@ -344,10 +427,20 @@ async function upsertOrCreateOffer(marketplace, varient, listing) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (listing.categoryId) offerUpdate.categoryId = String(listing.categoryId);
|
if (listing.categoryId) offerUpdate.categoryId = String(listing.categoryId);
|
||||||
if (listing.fulfillmentPolicyId) {
|
if (
|
||||||
|
listing.fulfillmentPolicyId ||
|
||||||
|
listing.paymentPolicyId ||
|
||||||
|
listing.returnPolicyId
|
||||||
|
) {
|
||||||
offerUpdate.listingPolicies = {
|
offerUpdate.listingPolicies = {
|
||||||
...(existingOffer.listingPolicies || {}),
|
...(existingOffer.listingPolicies || {}),
|
||||||
fulfillmentPolicyId: String(listing.fulfillmentPolicyId),
|
...(listing.fulfillmentPolicyId
|
||||||
|
? { fulfillmentPolicyId: String(listing.fulfillmentPolicyId) }
|
||||||
|
: {}),
|
||||||
|
...(listing.paymentPolicyId
|
||||||
|
? { paymentPolicyId: String(listing.paymentPolicyId) }
|
||||||
|
: {}),
|
||||||
|
...(listing.returnPolicyId ? { returnPolicyId: String(listing.returnPolicyId) } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const body = { ...existingOffer, ...offerUpdate };
|
const body = { ...existingOffer, ...offerUpdate };
|
||||||
@ -361,7 +454,7 @@ async function upsertOrCreateOffer(marketplace, varient, listing) {
|
|||||||
return existingOffer;
|
return existingOffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const offerBody = mapVarientToOffer(varient, listing, marketplace, merchantLocationKey);
|
const offerBody = mapVarientToOffer(varient, listing, marketplace, merchantLocationKey, quantity);
|
||||||
const result = await makeRequest({
|
const result = await makeRequest({
|
||||||
marketplace,
|
marketplace,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -392,14 +485,15 @@ export async function withdrawOfferById(marketplace, offerId) {
|
|||||||
|
|
||||||
export async function syncOfferAndMaybePublish(marketplace, listing, varient) {
|
export async function syncOfferAndMaybePublish(marketplace, listing, varient) {
|
||||||
const offerResult = await upsertOrCreateOffer(marketplace, varient, listing);
|
const offerResult = await upsertOrCreateOffer(marketplace, varient, listing);
|
||||||
|
const existingListingId = resolveOfferListingId(offerResult) || storedEbayItemId(listing);
|
||||||
|
if (isOfferPublished(offerResult) && existingListingId) {
|
||||||
|
return ebayListingPublishResult(marketplace, { listingId: existingListingId });
|
||||||
|
}
|
||||||
if (offerResult?.offerId && listing.state?.type === 'active') {
|
if (offerResult?.offerId && listing.state?.type === 'active') {
|
||||||
try {
|
try {
|
||||||
const publishResult = await publishOfferById(marketplace, offerResult.offerId);
|
const publishResult = await publishOfferById(marketplace, offerResult.offerId);
|
||||||
if (publishResult?.listingId) {
|
if (publishResult?.listingId) {
|
||||||
return {
|
return ebayListingPublishResult(marketplace, { listingId: publishResult.listingId });
|
||||||
url: `https://www.ebay.com/itm/${publishResult.listingId}`,
|
|
||||||
listingId: publishResult.listingId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@ -407,10 +501,17 @@ export async function syncOfferAndMaybePublish(marketplace, listing, varient) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { url: '', listingId: offerResult?.listingId };
|
return ebayListingPublishResult(marketplace, { listingId: existingListingId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function publishOfferForSku(marketplace, sku, listing) {
|
function resolvePublishVarient(sku, varient) {
|
||||||
|
if (varient && (marketplaceSku(varient) === sku || varient._reference === sku)) {
|
||||||
|
return varient;
|
||||||
|
}
|
||||||
|
return { ...(varient || {}), _reference: sku, externalReference: sku };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishOfferForSku(marketplace, sku, listing, varient) {
|
||||||
if (!sku) throw new Error('SKU (_reference) is required to publish an offer');
|
if (!sku) throw new Error('SKU (_reference) is required to publish an offer');
|
||||||
if (!listing) {
|
if (!listing) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -418,51 +519,52 @@ export async function publishOfferForSku(marketplace, sku, listing) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const merchantLocationKey = await ensureMerchantLocation(marketplace, listing);
|
const resolvedVarient = resolvePublishVarient(sku, varient);
|
||||||
const category = await syncProductCategory(marketplace, listing);
|
const category = await syncProductCategory(marketplace, listing, [resolvedVarient]);
|
||||||
const fulfillmentPolicy = await syncFulfillmentPolicy(marketplace, listing);
|
const listingPolicies = await syncListingPolicies(marketplace, listing);
|
||||||
if (!category?.categoryId) {
|
if (!category?.categoryId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Listing "${listing._reference || sku}" must have a product with a product category before publishing on eBay.`
|
`Listing "${listing._reference || sku}" must have a product with a product category before publishing on eBay.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingOffer = (await fetchOffers(marketplace, sku))[0];
|
const listingWithContext = {
|
||||||
if (!existingOffer?.offerId) {
|
...listing,
|
||||||
throw new Error(
|
categoryId: category.categoryId,
|
||||||
`No eBay offer exists for SKU "${sku}". Create or sync the listing so an offer exists before publishing.`
|
...listingPolicies,
|
||||||
);
|
};
|
||||||
|
|
||||||
|
await ensureInventoryItem(marketplace, sku, listingWithContext, resolvedVarient);
|
||||||
|
|
||||||
|
const offerResult = await upsertOrCreateOffer(marketplace, resolvedVarient, listingWithContext);
|
||||||
|
const offers = offerResult?.offerId ? [offerResult] : await fetchOffers(marketplace, sku);
|
||||||
|
const matchedOffer = findOfferForListing(offers, listingWithContext) || offerResult;
|
||||||
|
const offerId = matchedOffer?.offerId || offers[0]?.offerId;
|
||||||
|
if (!offerId) {
|
||||||
|
throw new Error(`Failed to create an eBay offer for SKU "${sku}".`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const listingDescription = resolveListingDescription(listing, { _reference: sku });
|
const existingListingId = resolveOfferListingId(matchedOffer) || storedEbayItemId(listing);
|
||||||
await ensureInventoryItemDescription(marketplace, sku, listing, { _reference: sku });
|
if (isOfferPublished(matchedOffer) && existingListingId) {
|
||||||
|
return ebayListingPublishResult(marketplace, { offerId, listingId: existingListingId });
|
||||||
|
}
|
||||||
|
|
||||||
await makeRequest({
|
const publishResult = await publishOfferById(marketplace, offerId);
|
||||||
marketplace,
|
return ebayListingPublishResult(marketplace, {
|
||||||
method: 'PUT',
|
offerId,
|
||||||
path: `/sell/inventory/v1/offer/${existingOffer.offerId}`,
|
listingId: publishResult?.listingId || existingListingId,
|
||||||
body: {
|
|
||||||
...existingOffer,
|
|
||||||
merchantLocationKey,
|
|
||||||
categoryId: String(category.categoryId),
|
|
||||||
listingDescription,
|
|
||||||
listingPolicies: {
|
|
||||||
...(existingOffer.listingPolicies || {}),
|
|
||||||
fulfillmentPolicyId: fulfillmentPolicy.fulfillmentPolicyId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const publishResult = await publishOfferById(marketplace, existingOffer.offerId);
|
|
||||||
return { offerId: existingOffer.offerId, listingId: publishResult?.listingId };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function withdrawOfferForSku(marketplace, sku) {
|
export async function withdrawOfferForSku(marketplace, sku, listing) {
|
||||||
if (!sku) throw new Error('SKU (_reference) is required to withdraw an offer');
|
if (!sku) throw new Error('SKU (_reference) is required to withdraw an offer');
|
||||||
const existingOffer = (await fetchOffers(marketplace, sku))[0];
|
const existingOffer = findOfferForListing(await fetchOffers(marketplace, sku), listing);
|
||||||
if (!existingOffer?.offerId) throw new Error(`No eBay offer exists for SKU "${sku}".`);
|
if (!existingOffer?.offerId) throw new Error(`No eBay offer exists for SKU "${sku}".`);
|
||||||
await withdrawOfferById(marketplace, existingOffer.offerId);
|
await withdrawOfferById(marketplace, existingOffer.offerId);
|
||||||
return { offerId: existingOffer.offerId };
|
return {
|
||||||
|
offerId: existingOffer.offerId,
|
||||||
|
listingId: resolveOfferListingId(existingOffer) || storedEbayItemId(listing),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveOfferState(offers) {
|
export function resolveOfferState(offers) {
|
||||||
@ -479,6 +581,7 @@ export function resolveOfferState(offers) {
|
|||||||
|
|
||||||
export function buildVarientEntry(item, offers) {
|
export function buildVarientEntry(item, offers) {
|
||||||
const offer = offers?.[0];
|
const offer = offers?.[0];
|
||||||
|
const aspects = fromEbayProductAspects(item.product?.aspects);
|
||||||
return {
|
return {
|
||||||
_reference: item.sku,
|
_reference: item.sku,
|
||||||
externalReference: item.sku,
|
externalReference: item.sku,
|
||||||
@ -487,5 +590,6 @@ export function buildVarientEntry(item, offers) {
|
|||||||
: undefined,
|
: undefined,
|
||||||
currency: offer?.pricingSummary?.price?.currency || undefined,
|
currency: offer?.pricingSummary?.price?.currency || undefined,
|
||||||
state: { type: resolveOfferState(offers || []) },
|
state: { type: resolveOfferState(offers || []) },
|
||||||
|
...(aspects.length ? { aspects } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { syncProductCategory } from './categories.js';
|
import { syncProductCategory } from './categories.js';
|
||||||
import { syncFulfillmentPolicy } from './fulfillmentPolicies.js';
|
import { syncListingPolicies } from './listingPolicies.js';
|
||||||
import {
|
import {
|
||||||
buildVarientEntry,
|
buildVarientEntry,
|
||||||
fetchOffers,
|
fetchOffers,
|
||||||
@ -11,25 +11,49 @@ import {
|
|||||||
withdrawOfferById,
|
withdrawOfferById,
|
||||||
} from './listingVarients.js';
|
} from './listingVarients.js';
|
||||||
import { makeRequest, logger } from './shared.js';
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
import { getEbayItemUrl, parseEbayItemId } from './itemUrl.js';
|
||||||
import { marketplaceSku } from '../ids.js';
|
import { marketplaceSku } from '../ids.js';
|
||||||
|
import { buildGroupVariesBy } from './variationAspects.js';
|
||||||
|
import { toEbayHtmlDescription } from './description.js';
|
||||||
|
import { attachImageUrlsToListingAndVarients } from './images.js';
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createOrReplaceGroup(marketplace, listing, varients) {
|
export function buildInventoryItemGroupBody(listing, varients) {
|
||||||
const groupKey = listing._reference;
|
const groupKey = listing._reference;
|
||||||
const variantSKUs = varients.map((v) => marketplaceSku(v)).filter(Boolean);
|
const variantSKUs = varients.map((v) => marketplaceSku(v)).filter(Boolean);
|
||||||
|
const variation = buildGroupVariesBy(varients);
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
title: listing.title || groupKey,
|
title: listing.title || groupKey,
|
||||||
variantSKUs,
|
variantSKUs,
|
||||||
description: resolveListingDescription(listing),
|
description: toEbayHtmlDescription(resolveListingDescription(listing)),
|
||||||
|
variesBy: variation.variesBy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (variation.aspects) {
|
||||||
|
body.aspects = variation.aspects;
|
||||||
|
}
|
||||||
|
|
||||||
if (listing.imageUrls?.length) {
|
if (listing.imageUrls?.length) {
|
||||||
body.imageUrls = listing.imageUrls;
|
body.imageUrls = listing.imageUrls;
|
||||||
|
const firstSpecName = variation.variesBy?.specifications?.[0]?.name;
|
||||||
|
if (firstSpecName) {
|
||||||
|
body.variesBy = {
|
||||||
|
...body.variesBy,
|
||||||
|
aspectsImageVariesBy: [firstSpecName],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOrReplaceGroup(marketplace, listing, varients) {
|
||||||
|
const groupKey = listing._reference;
|
||||||
|
const body = buildInventoryItemGroupBody(listing, varients);
|
||||||
|
|
||||||
await makeRequest({
|
await makeRequest({
|
||||||
marketplace,
|
marketplace,
|
||||||
@ -105,36 +129,40 @@ async function syncListing(marketplace, listing, varients, actionLabel) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = await syncProductCategory(marketplace, listing, validVarients);
|
const { listing: listingWithImages, varients: varientsWithImages } =
|
||||||
const fulfillmentPolicy = await syncFulfillmentPolicy(marketplace, listing);
|
await attachImageUrlsToListingAndVarients(marketplace, listing, validVarients);
|
||||||
|
|
||||||
|
const category = await syncProductCategory(marketplace, listingWithImages, varientsWithImages);
|
||||||
|
const listingPolicies = await syncListingPolicies(marketplace, listingWithImages);
|
||||||
const listingWithContext = {
|
const listingWithContext = {
|
||||||
...listing,
|
...listingWithImages,
|
||||||
...(category ? { categoryId: category.categoryId } : {}),
|
...(category ? { categoryId: category.categoryId } : {}),
|
||||||
fulfillmentPolicyId: fulfillmentPolicy.fulfillmentPolicyId,
|
...listingPolicies,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (validVarients.length === 1) {
|
if (varientsWithImages.length === 1) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Syncing standalone eBay inventory item "${validVarients[0]._reference}" for listing "${ref}"`
|
`Syncing standalone eBay inventory item "${varientsWithImages[0]._reference}" for listing "${ref}"`
|
||||||
);
|
);
|
||||||
const published = await syncSingleVarientListing(
|
const published = await syncSingleVarientListing(
|
||||||
marketplace,
|
marketplace,
|
||||||
listingWithContext,
|
listingWithContext,
|
||||||
validVarients[0]
|
varientsWithImages[0]
|
||||||
);
|
);
|
||||||
return listingSyncResult(published);
|
return listingSyncResult(published, marketplace);
|
||||||
}
|
}
|
||||||
|
|
||||||
const published = await syncGroupedListing(marketplace, listingWithContext, validVarients);
|
const published = await syncGroupedListing(marketplace, listingWithContext, varientsWithImages);
|
||||||
return listingSyncResult(published);
|
return listingSyncResult(published, marketplace);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listingSyncResult(published) {
|
function listingSyncResult(published, marketplace) {
|
||||||
if (!published) return { url: '' };
|
if (!published) return { url: '' };
|
||||||
if (typeof published === 'string') return { url: published };
|
if (typeof published === 'string') return { url: published };
|
||||||
|
const listingId = parseEbayItemId(published.listingId || published.externalReference);
|
||||||
return {
|
return {
|
||||||
url: published.url || '',
|
url: published.url || getEbayItemUrl(marketplace, listingId),
|
||||||
...(published.listingId ? { externalReference: published.listingId } : {}),
|
...(listingId ? { externalReference: listingId } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +174,20 @@ export async function updateItem(marketplace, listing, varients) {
|
|||||||
return syncListing(marketplace, listing, varients, 'update');
|
return syncListing(marketplace, listing, varients, 'update');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function syncListingImages(marketplace, listing, varients = []) {
|
||||||
|
const validVarients = (varients || []).filter((varient) => varient?._reference);
|
||||||
|
if (validVarients.length === 0) return null;
|
||||||
|
|
||||||
|
const { listing: listingWithImages, varients: varientsWithImages } =
|
||||||
|
await attachImageUrlsToListingAndVarients(marketplace, listing, validVarients);
|
||||||
|
|
||||||
|
for (const varient of varientsWithImages) {
|
||||||
|
await upsertInventoryItem(marketplace, varient, listingWithImages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { listing: listingWithImages, varients: varientsWithImages };
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteItem(marketplace, listing, varients = []) {
|
export async function deleteItem(marketplace, listing, varients = []) {
|
||||||
const skus = varients.map((v) => marketplaceSku(v)).filter(Boolean);
|
const skus = varients.map((v) => marketplaceSku(v)).filter(Boolean);
|
||||||
|
|
||||||
@ -296,17 +338,19 @@ export async function syncItems(marketplace) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapProductToListing(ebayItem) {
|
export function mapProductToListing(ebayItem, marketplace) {
|
||||||
if (ebayItem._type === 'group') {
|
if (ebayItem._type === 'group') {
|
||||||
const group = ebayItem._group;
|
const group = ebayItem._group;
|
||||||
const variants = ebayItem._variants || [];
|
const variants = ebayItem._variants || [];
|
||||||
const allOffers = variants.flatMap((v) => v._offers || []);
|
const allOffers = variants.flatMap((v) => v._offers || []);
|
||||||
|
|
||||||
const stateType = resolveOfferState(allOffers);
|
const stateType = resolveOfferState(allOffers);
|
||||||
const firstPublishedOffer = allOffers.find((o) => o?.listingId);
|
const firstPublishedOffer = allOffers.find((o) => o?.listingId || o?.listing?.listingId);
|
||||||
const url = firstPublishedOffer?.listingId
|
const publishedListingId = parseEbayItemId(
|
||||||
? `https://www.ebay.com/itm/${firstPublishedOffer.listingId}`
|
firstPublishedOffer?.listingId || firstPublishedOffer?.listing?.listingId
|
||||||
: '';
|
);
|
||||||
|
const listingId = publishedListingId || ebayItem._groupKey;
|
||||||
|
const url = getEbayItemUrl(marketplace, publishedListingId);
|
||||||
|
|
||||||
const firstOffer = allOffers[0];
|
const firstOffer = allOffers[0];
|
||||||
const price = firstOffer?.pricingSummary?.price?.value
|
const price = firstOffer?.pricingSummary?.price?.value
|
||||||
@ -317,7 +361,7 @@ export function mapProductToListing(ebayItem) {
|
|||||||
const varients = variants.map((v) => buildVarientEntry(v, v._offers || []));
|
const varients = variants.map((v) => buildVarientEntry(v, v._offers || []));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
externalReference: firstPublishedOffer?.listingId || ebayItem._groupKey,
|
externalReference: listingId,
|
||||||
title: group.title || ebayItem._groupKey,
|
title: group.title || ebayItem._groupKey,
|
||||||
description: group.description,
|
description: group.description,
|
||||||
condition: fromEbayCondition(variants[0]?.condition),
|
condition: fromEbayCondition(variants[0]?.condition),
|
||||||
@ -336,13 +380,14 @@ export function mapProductToListing(ebayItem) {
|
|||||||
const currency = offer?.pricingSummary?.price?.currency || undefined;
|
const currency = offer?.pricingSummary?.price?.currency || undefined;
|
||||||
|
|
||||||
const stateType = resolveOfferState(ebayItem._offers || []);
|
const stateType = resolveOfferState(ebayItem._offers || []);
|
||||||
|
const publishedListingId = parseEbayItemId(offer?.listingId || offer?.listing?.listingId);
|
||||||
const url = offer?.listingId ? `https://www.ebay.com/itm/${offer.listingId}` : '';
|
const listingId = publishedListingId || ebayItem.sku;
|
||||||
|
const url = getEbayItemUrl(marketplace, publishedListingId);
|
||||||
|
|
||||||
const varients = [buildVarientEntry(ebayItem, ebayItem._offers || [])];
|
const varients = [buildVarientEntry(ebayItem, ebayItem._offers || [])];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
externalReference: offer?.listingId || ebayItem.sku,
|
externalReference: listingId,
|
||||||
title: ebayItem.product?.title || ebayItem.sku,
|
title: ebayItem.product?.title || ebayItem.sku,
|
||||||
description: ebayItem.product?.description,
|
description: ebayItem.product?.description,
|
||||||
condition: fromEbayCondition(ebayItem.condition),
|
condition: fromEbayCondition(ebayItem.condition),
|
||||||
|
|||||||
133
src/integrations/marketplaces/ebay/paymentPolicies.js
Normal file
133
src/integrations/marketplaces/ebay/paymentPolicies.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { paymentPolicyModel } from '../../../database/schemas/finance/paymentpolicy.schema.js';
|
||||||
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
import {
|
||||||
|
buildCategoryTypes,
|
||||||
|
ensureSellingPolicyManagement,
|
||||||
|
getEbayMarketplaceId,
|
||||||
|
mappingExternalReference,
|
||||||
|
persistMarketplaceMapping,
|
||||||
|
updateAccountPolicy,
|
||||||
|
upsertLocalPolicyFromRemote,
|
||||||
|
} from './accountPolicies.js';
|
||||||
|
|
||||||
|
export function buildPaymentPolicy(policy, marketplace, existingPolicy, allPolicies = []) {
|
||||||
|
const payload = {
|
||||||
|
name: String(policy?.name || '').slice(0, 64),
|
||||||
|
marketplaceId: getEbayMarketplaceId(marketplace),
|
||||||
|
categoryTypes: buildCategoryTypes(existingPolicy, allPolicies, 'paymentPolicyId'),
|
||||||
|
immediatePay: policy?.immediatePay !== false,
|
||||||
|
};
|
||||||
|
if (policy?.description) payload.description = String(policy.description).slice(0, 250);
|
||||||
|
if (policy?.paymentInstructions) {
|
||||||
|
payload.paymentInstructions = String(policy.paymentInstructions).slice(0, 1000);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPaymentPolicyByName(marketplace, name) {
|
||||||
|
return makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/sell/account/v1/payment_policy/get_by_policy_name',
|
||||||
|
params: {
|
||||||
|
marketplace_id: getEbayMarketplaceId(marketplace),
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPaymentPolicies(marketplace) {
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/sell/account/v1/payment_policy',
|
||||||
|
params: { marketplace_id: getEbayMarketplaceId(marketplace) },
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
});
|
||||||
|
return result?.paymentPolicies || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensurePaymentPolicySynced(marketplace, policy) {
|
||||||
|
if (!policy) {
|
||||||
|
throw new Error('A payment policy is required before publishing to eBay.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureSellingPolicyManagement(marketplace);
|
||||||
|
await persistMarketplaceMapping(paymentPolicyModel, policy, marketplace, {
|
||||||
|
stateType: 'syncing',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const policyName = String(policy.name || '').slice(0, 64);
|
||||||
|
const existingId = mappingExternalReference(policy, marketplace);
|
||||||
|
const [existingByName, allPolicies] = await Promise.all([
|
||||||
|
existingId ? null : fetchPaymentPolicyByName(marketplace, policyName),
|
||||||
|
fetchPaymentPolicies(marketplace),
|
||||||
|
]);
|
||||||
|
const existingPolicy =
|
||||||
|
(existingId && allPolicies.find((item) => String(item.paymentPolicyId) === String(existingId))) ||
|
||||||
|
existingByName ||
|
||||||
|
null;
|
||||||
|
const payload = buildPaymentPolicy(policy, marketplace, existingPolicy, allPolicies);
|
||||||
|
let paymentPolicyId = existingId || existingPolicy?.paymentPolicyId;
|
||||||
|
|
||||||
|
if (paymentPolicyId) {
|
||||||
|
await updateAccountPolicy(
|
||||||
|
marketplace,
|
||||||
|
`/sell/account/v1/payment_policy/${encodeURIComponent(paymentPolicyId)}`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const created = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/account/v1/payment_policy',
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
paymentPolicyId = created?.paymentPolicyId;
|
||||||
|
if (!paymentPolicyId) {
|
||||||
|
throw new Error(`eBay did not return an ID for payment policy "${payload.name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await persistMarketplaceMapping(paymentPolicyModel, policy, marketplace, {
|
||||||
|
externalReference: String(paymentPolicyId),
|
||||||
|
stateType: 'ready',
|
||||||
|
});
|
||||||
|
logger.info(`Synced eBay payment policy "${payload.name}" (${paymentPolicyId})`);
|
||||||
|
return { paymentPolicyId: String(paymentPolicyId) };
|
||||||
|
} catch (err) {
|
||||||
|
await persistMarketplaceMapping(paymentPolicyModel, policy, marketplace, {
|
||||||
|
stateType: 'failed',
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRemotePaymentPolicy(remote) {
|
||||||
|
return {
|
||||||
|
description: remote.description,
|
||||||
|
immediatePay: remote.immediatePay !== false,
|
||||||
|
paymentInstructions: remote.paymentInstructions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncPaymentPoliciesFromEbay(marketplace) {
|
||||||
|
await ensureSellingPolicyManagement(marketplace);
|
||||||
|
const remotes = await fetchPaymentPolicies(marketplace);
|
||||||
|
const upserted = [];
|
||||||
|
for (const remote of remotes) {
|
||||||
|
const paymentPolicyId = remote.paymentPolicyId;
|
||||||
|
if (!paymentPolicyId || !remote.name) continue;
|
||||||
|
upserted.push(
|
||||||
|
await upsertLocalPolicyFromRemote({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
marketplace,
|
||||||
|
remoteId: paymentPolicyId,
|
||||||
|
name: remote.name,
|
||||||
|
fields: mapRemotePaymentPolicy(remote),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { count: upserted.length };
|
||||||
|
}
|
||||||
189
src/integrations/marketplaces/ebay/returnPolicies.js
Normal file
189
src/integrations/marketplaces/ebay/returnPolicies.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { returnPolicyModel } from '../../../database/schemas/sales/returnpolicy.schema.js';
|
||||||
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
import {
|
||||||
|
buildCategoryTypes,
|
||||||
|
ensureSellingPolicyManagement,
|
||||||
|
getEbayMarketplaceId,
|
||||||
|
mappingExternalReference,
|
||||||
|
persistMarketplaceMapping,
|
||||||
|
updateAccountPolicy,
|
||||||
|
upsertLocalPolicyFromRemote,
|
||||||
|
} from './accountPolicies.js';
|
||||||
|
|
||||||
|
const PAYER_TO_EBAY = { buyer: 'BUYER', seller: 'SELLER' };
|
||||||
|
const EBAY_TO_PAYER = { BUYER: 'buyer', SELLER: 'seller' };
|
||||||
|
const REFUND_TO_EBAY = { moneyBack: 'MONEY_BACK', merchandiseCredit: 'MERCHANDISE_CREDIT' };
|
||||||
|
const EBAY_TO_REFUND = { MONEY_BACK: 'moneyBack', MERCHANDISE_CREDIT: 'merchandiseCredit' };
|
||||||
|
const EBAY_RETURN_PERIOD_DAYS = [14, 30, 60];
|
||||||
|
|
||||||
|
export function snapReturnPeriodDays(days, fallback = 30) {
|
||||||
|
const value = Number(days);
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return fallback;
|
||||||
|
return EBAY_RETURN_PERIOD_DAYS.find((allowed) => allowed >= value) || EBAY_RETURN_PERIOD_DAYS.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function periodFromDays(days, fallback = 30) {
|
||||||
|
return { value: snapReturnPeriodDays(days, fallback), unit: 'DAY' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysFromPeriod(period) {
|
||||||
|
if (period == null) return undefined;
|
||||||
|
const value = Number(period.value ?? period);
|
||||||
|
return Number.isFinite(value) ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildReturnPolicy(policy, marketplace, existingPolicy, allPolicies = []) {
|
||||||
|
const returnsAccepted = policy?.returnsAccepted !== false;
|
||||||
|
const payload = {
|
||||||
|
name: String(policy?.name || '').slice(0, 64),
|
||||||
|
marketplaceId: getEbayMarketplaceId(marketplace),
|
||||||
|
categoryTypes: buildCategoryTypes(existingPolicy, allPolicies, 'returnPolicyId'),
|
||||||
|
returnsAccepted,
|
||||||
|
};
|
||||||
|
if (policy?.description) payload.description = String(policy.description).slice(0, 250);
|
||||||
|
if (returnsAccepted) {
|
||||||
|
payload.returnPeriod = periodFromDays(policy?.returnPeriodDays ?? 30);
|
||||||
|
payload.returnShippingCostPayer =
|
||||||
|
PAYER_TO_EBAY[policy?.returnShippingCostPayer] || 'BUYER';
|
||||||
|
payload.refundMethod = REFUND_TO_EBAY[policy?.refundMethod] || 'MONEY_BACK';
|
||||||
|
}
|
||||||
|
if (policy?.returnInstructions) {
|
||||||
|
payload.returnInstructions = String(policy.returnInstructions).slice(0, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (policy?.internationalReturnsAccepted === true) {
|
||||||
|
payload.internationalOverride = {
|
||||||
|
returnsAccepted: true,
|
||||||
|
returnPeriod: periodFromDays(
|
||||||
|
policy.internationalReturnPeriodDays ?? policy.returnPeriodDays ?? 30
|
||||||
|
),
|
||||||
|
returnShippingCostPayer:
|
||||||
|
PAYER_TO_EBAY[policy.internationalReturnShippingCostPayer] ||
|
||||||
|
PAYER_TO_EBAY[policy.returnShippingCostPayer] ||
|
||||||
|
'BUYER',
|
||||||
|
};
|
||||||
|
} else if (policy?.internationalReturnsAccepted === false) {
|
||||||
|
payload.internationalOverride = { returnsAccepted: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchReturnPolicyByName(marketplace, name) {
|
||||||
|
return makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/sell/account/v1/return_policy/get_by_policy_name',
|
||||||
|
params: {
|
||||||
|
marketplace_id: getEbayMarketplaceId(marketplace),
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchReturnPolicies(marketplace) {
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/sell/account/v1/return_policy',
|
||||||
|
params: { marketplace_id: getEbayMarketplaceId(marketplace) },
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
});
|
||||||
|
return result?.returnPolicies || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureReturnPolicySynced(marketplace, policy) {
|
||||||
|
if (!policy) {
|
||||||
|
throw new Error('A return policy is required before publishing to eBay.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureSellingPolicyManagement(marketplace);
|
||||||
|
await persistMarketplaceMapping(returnPolicyModel, policy, marketplace, {
|
||||||
|
stateType: 'syncing',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const policyName = String(policy.name || '').slice(0, 64);
|
||||||
|
const existingId = mappingExternalReference(policy, marketplace);
|
||||||
|
const [existingByName, allPolicies] = await Promise.all([
|
||||||
|
existingId ? null : fetchReturnPolicyByName(marketplace, policyName),
|
||||||
|
fetchReturnPolicies(marketplace),
|
||||||
|
]);
|
||||||
|
const existingPolicy =
|
||||||
|
(existingId && allPolicies.find((item) => String(item.returnPolicyId) === String(existingId))) ||
|
||||||
|
existingByName ||
|
||||||
|
null;
|
||||||
|
const payload = buildReturnPolicy(policy, marketplace, existingPolicy, allPolicies);
|
||||||
|
let returnPolicyId = existingId || existingPolicy?.returnPolicyId;
|
||||||
|
|
||||||
|
if (returnPolicyId) {
|
||||||
|
await updateAccountPolicy(
|
||||||
|
marketplace,
|
||||||
|
`/sell/account/v1/return_policy/${encodeURIComponent(returnPolicyId)}`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const created = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/account/v1/return_policy',
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
returnPolicyId = created?.returnPolicyId;
|
||||||
|
if (!returnPolicyId) {
|
||||||
|
throw new Error(`eBay did not return an ID for return policy "${payload.name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await persistMarketplaceMapping(returnPolicyModel, policy, marketplace, {
|
||||||
|
externalReference: String(returnPolicyId),
|
||||||
|
stateType: 'ready',
|
||||||
|
});
|
||||||
|
logger.info(`Synced eBay return policy "${payload.name}" (${returnPolicyId})`);
|
||||||
|
return { returnPolicyId: String(returnPolicyId) };
|
||||||
|
} catch (err) {
|
||||||
|
await persistMarketplaceMapping(returnPolicyModel, policy, marketplace, {
|
||||||
|
stateType: 'failed',
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRemoteReturnPolicy(remote) {
|
||||||
|
const international = remote.internationalOverride || {};
|
||||||
|
return {
|
||||||
|
description: remote.description,
|
||||||
|
returnsAccepted: remote.returnsAccepted !== false,
|
||||||
|
returnPeriodDays: daysFromPeriod(remote.returnPeriod),
|
||||||
|
returnShippingCostPayer: EBAY_TO_PAYER[remote.returnShippingCostPayer] || 'buyer',
|
||||||
|
refundMethod: EBAY_TO_REFUND[remote.refundMethod] || 'moneyBack',
|
||||||
|
restockingFeePercentage:
|
||||||
|
remote.restockingFeePercentage != null ? Number(remote.restockingFeePercentage) : undefined,
|
||||||
|
returnInstructions: remote.returnInstructions,
|
||||||
|
internationalReturnsAccepted:
|
||||||
|
international.returnsAccepted == null ? undefined : Boolean(international.returnsAccepted),
|
||||||
|
internationalReturnPeriodDays: daysFromPeriod(international.returnPeriod),
|
||||||
|
internationalReturnShippingCostPayer: EBAY_TO_PAYER[international.returnShippingCostPayer],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncReturnPoliciesFromEbay(marketplace) {
|
||||||
|
await ensureSellingPolicyManagement(marketplace);
|
||||||
|
const remotes = await fetchReturnPolicies(marketplace);
|
||||||
|
const upserted = [];
|
||||||
|
for (const remote of remotes) {
|
||||||
|
const returnPolicyId = remote.returnPolicyId;
|
||||||
|
if (!returnPolicyId || !remote.name) continue;
|
||||||
|
upserted.push(
|
||||||
|
await upsertLocalPolicyFromRemote({
|
||||||
|
model: returnPolicyModel,
|
||||||
|
marketplace,
|
||||||
|
remoteId: returnPolicyId,
|
||||||
|
name: remote.name,
|
||||||
|
fields: mapRemoteReturnPolicy(remote),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.info(`Imported ${upserted.length} eBay return polic${upserted.length === 1 ? 'y' : 'ies'}`);
|
||||||
|
return { count: upserted.length };
|
||||||
|
}
|
||||||
155
src/integrations/marketplaces/ebay/salesTax.js
Normal file
155
src/integrations/marketplaces/ebay/salesTax.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { taxRateModel } from '../../../database/schemas/management/taxrate.schema.js';
|
||||||
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
import {
|
||||||
|
idOf,
|
||||||
|
persistMarketplaceMapping,
|
||||||
|
upsertLocalPolicyFromRemote,
|
||||||
|
} from './accountPolicies.js';
|
||||||
|
|
||||||
|
export const US_TERRITORY_JURISDICTIONS = ['AS', 'GU', 'MP', 'PW', 'VI'];
|
||||||
|
export const EBAY_TAX_COUNTRIES = ['US', 'CA'];
|
||||||
|
|
||||||
|
export function taxExternalReference(country, jurisdiction) {
|
||||||
|
const countryCode = String(country || '').toUpperCase();
|
||||||
|
const jurisdictionId = String(jurisdiction || '').toUpperCase();
|
||||||
|
if (!countryCode || !jurisdictionId) return '';
|
||||||
|
return `${countryCode}:${jurisdictionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEbayTaxTableSupported({ country, jurisdiction } = {}) {
|
||||||
|
const countryCode = String(country || '').toUpperCase();
|
||||||
|
const jurisdictionId = String(jurisdiction || '').toUpperCase();
|
||||||
|
if (!countryCode || !jurisdictionId) return false;
|
||||||
|
if (countryCode === 'CA') return true;
|
||||||
|
if (countryCode === 'US') return US_TERRITORY_JURISDICTIONS.includes(jurisdictionId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSalesTaxEntry(taxRate) {
|
||||||
|
const countryCode = String(taxRate?.country || '').toUpperCase();
|
||||||
|
const jurisdictionId = String(taxRate?.jurisdiction || '').toUpperCase();
|
||||||
|
return {
|
||||||
|
countryCode,
|
||||||
|
jurisdictionId,
|
||||||
|
salesTaxPercentage: String(Number(taxRate?.rate) || 0),
|
||||||
|
shippingAndHandlingTaxed: taxRate?.shippingAndHandlingTaxed === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSalesTaxes(marketplace, countryCode) {
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/sell/account/v1/sales_tax',
|
||||||
|
params: { country_code: countryCode },
|
||||||
|
acceptableStatuses: [204, 404],
|
||||||
|
});
|
||||||
|
return result?.salesTaxes || result?.salesTaxJurisdictions || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureTaxRateSynced(marketplace, taxRate) {
|
||||||
|
if (!isEbayTaxTableSupported(taxRate)) {
|
||||||
|
await persistMarketplaceMapping(taxRateModel, taxRate, marketplace, {
|
||||||
|
stateType: 'failed',
|
||||||
|
message:
|
||||||
|
'eBay tax tables only apply to US territories (AS, GU, MP, PW, VI) and Canada.',
|
||||||
|
});
|
||||||
|
return { skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
await persistMarketplaceMapping(taxRateModel, taxRate, marketplace, {
|
||||||
|
stateType: 'syncing',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entry = buildSalesTaxEntry(taxRate);
|
||||||
|
await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'PUT',
|
||||||
|
path: `/sell/account/v1/sales_tax/${encodeURIComponent(entry.countryCode)}/${encodeURIComponent(entry.jurisdictionId)}`,
|
||||||
|
body: {
|
||||||
|
salesTaxPercentage: entry.salesTaxPercentage,
|
||||||
|
shippingAndHandlingTaxed: entry.shippingAndHandlingTaxed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const externalReference = taxExternalReference(entry.countryCode, entry.jurisdictionId);
|
||||||
|
await persistMarketplaceMapping(taxRateModel, taxRate, marketplace, {
|
||||||
|
externalReference,
|
||||||
|
stateType: 'ready',
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
`Synced eBay sales tax ${externalReference} for tax rate "${taxRate.name || taxRate._reference}"`
|
||||||
|
);
|
||||||
|
return { externalReference };
|
||||||
|
} catch (err) {
|
||||||
|
await persistMarketplaceMapping(taxRateModel, taxRate, marketplace, {
|
||||||
|
stateType: 'failed',
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncTaxRatesFromEbay(marketplace) {
|
||||||
|
const remotes = [];
|
||||||
|
for (const countryCode of EBAY_TAX_COUNTRIES) {
|
||||||
|
const entries = await fetchSalesTaxes(marketplace, countryCode);
|
||||||
|
for (const entry of entries) {
|
||||||
|
remotes.push({
|
||||||
|
...entry,
|
||||||
|
countryCode: entry.countryCode || countryCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserted = [];
|
||||||
|
for (const remote of remotes) {
|
||||||
|
const country = String(remote.countryCode || '').toUpperCase();
|
||||||
|
const jurisdiction = String(remote.jurisdictionId || '').toUpperCase();
|
||||||
|
if (!isEbayTaxTableSupported({ country, jurisdiction })) continue;
|
||||||
|
|
||||||
|
const name = `${country} ${jurisdiction} sales tax`;
|
||||||
|
const remoteId = taxExternalReference(country, jurisdiction);
|
||||||
|
upserted.push(
|
||||||
|
await upsertLocalPolicyFromRemote({
|
||||||
|
model: taxRateModel,
|
||||||
|
marketplace,
|
||||||
|
remoteId,
|
||||||
|
name,
|
||||||
|
fields: {
|
||||||
|
country,
|
||||||
|
jurisdiction,
|
||||||
|
rate: Number(remote.salesTaxPercentage) || 0,
|
||||||
|
rateType: 'percentage',
|
||||||
|
active: true,
|
||||||
|
shippingAndHandlingTaxed: remote.shippingAndHandlingTaxed === true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Imported ${upserted.length} eBay sales tax table ${upserted.length === 1 ? 'entry' : 'entries'}`);
|
||||||
|
return { count: upserted.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncTaxRatesToEbay(marketplace) {
|
||||||
|
const taxRates = await taxRateModel.find({
|
||||||
|
'marketplaces.marketplace': idOf(marketplace),
|
||||||
|
});
|
||||||
|
let synced = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
for (const taxRate of taxRates) {
|
||||||
|
if (!isEbayTaxTableSupported(taxRate)) {
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await ensureTaxRateSynced(marketplace, taxRate);
|
||||||
|
synced += 1;
|
||||||
|
}
|
||||||
|
return { synced, skipped };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncTaxRates(marketplace) {
|
||||||
|
const inbound = await syncTaxRatesFromEbay(marketplace);
|
||||||
|
const outbound = await syncTaxRatesToEbay(marketplace);
|
||||||
|
return { inbound, outbound };
|
||||||
|
}
|
||||||
@ -25,16 +25,22 @@ function formatDebugPayload(value, { maxLength = DEBUG_PAYLOAD_MAX_LENGTH } = {}
|
|||||||
return `${text.slice(0, maxLength)}... [truncated ${text.length - maxLength} chars]`;
|
return `${text.slice(0, maxLength)}... [truncated ${text.length - maxLength} chars]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEbayMarketplaceId(marketplace) {
|
||||||
|
return marketplace?.config?.marketplaceId || 'EBAY_GB';
|
||||||
|
}
|
||||||
|
|
||||||
function getMarketplaceDebugContext(marketplace) {
|
function getMarketplaceDebugContext(marketplace) {
|
||||||
return {
|
return {
|
||||||
marketplace: marketplace?.name,
|
marketplace: marketplace?.name,
|
||||||
marketplaceId: marketplace?.config?.marketplaceId,
|
marketplaceId: getEbayMarketplaceId(marketplace),
|
||||||
sandbox: marketplace?.config?.sandbox ?? false,
|
sandbox: marketplace?.config?.sandbox ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SANDBOX_API_URL = 'https://api.sandbox.ebay.com';
|
const SANDBOX_API_URL = 'https://api.sandbox.ebay.com';
|
||||||
const PRODUCTION_API_URL = 'https://api.ebay.com';
|
const PRODUCTION_API_URL = 'https://api.ebay.com';
|
||||||
|
const SANDBOX_MEDIA_API_URL = 'https://apim.sandbox.ebay.com';
|
||||||
|
const PRODUCTION_MEDIA_API_URL = 'https://apim.ebay.com';
|
||||||
const SANDBOX_AUTH_URL = 'https://auth.sandbox.ebay.com';
|
const SANDBOX_AUTH_URL = 'https://auth.sandbox.ebay.com';
|
||||||
const PRODUCTION_AUTH_URL = 'https://auth.ebay.com';
|
const PRODUCTION_AUTH_URL = 'https://auth.ebay.com';
|
||||||
const TOKEN_PATH = '/identity/v1/oauth2/token';
|
const TOKEN_PATH = '/identity/v1/oauth2/token';
|
||||||
@ -44,6 +50,7 @@ const DEFAULT_SCOPES = [
|
|||||||
'https://api.ebay.com/oauth/api_scope/sell.fulfillment',
|
'https://api.ebay.com/oauth/api_scope/sell.fulfillment',
|
||||||
'https://api.ebay.com/oauth/api_scope/sell.account',
|
'https://api.ebay.com/oauth/api_scope/sell.account',
|
||||||
'https://api.ebay.com/oauth/api_scope/commerce.notification.subscription',
|
'https://api.ebay.com/oauth/api_scope/commerce.notification.subscription',
|
||||||
|
'https://api.ebay.com/oauth/api_scope/commerce.media',
|
||||||
];
|
];
|
||||||
const MARKETPLACE_LANGUAGE_MAP = {
|
const MARKETPLACE_LANGUAGE_MAP = {
|
||||||
EBAY_US: 'en-US',
|
EBAY_US: 'en-US',
|
||||||
@ -62,6 +69,10 @@ export function getApiBaseUrl(marketplace) {
|
|||||||
return marketplace.config.sandbox ? SANDBOX_API_URL : PRODUCTION_API_URL;
|
return marketplace.config.sandbox ? SANDBOX_API_URL : PRODUCTION_API_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMediaApiBaseUrl(marketplace) {
|
||||||
|
return marketplace.config.sandbox ? SANDBOX_MEDIA_API_URL : PRODUCTION_MEDIA_API_URL;
|
||||||
|
}
|
||||||
|
|
||||||
export function getAuthorizeBaseUrl(marketplace) {
|
export function getAuthorizeBaseUrl(marketplace) {
|
||||||
return marketplace.config.sandbox ? SANDBOX_AUTH_URL : PRODUCTION_AUTH_URL;
|
return marketplace.config.sandbox ? SANDBOX_AUTH_URL : PRODUCTION_AUTH_URL;
|
||||||
}
|
}
|
||||||
@ -137,6 +148,11 @@ export async function makeRequest({
|
|||||||
params = {},
|
params = {},
|
||||||
body = null,
|
body = null,
|
||||||
acceptableStatuses = [],
|
acceptableStatuses = [],
|
||||||
|
extraHeaders = {},
|
||||||
|
logResponse = true,
|
||||||
|
rawBody = false,
|
||||||
|
contentType,
|
||||||
|
baseUrl,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { accessToken } = marketplace.config || {};
|
const { accessToken } = marketplace.config || {};
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
@ -150,35 +166,47 @@ export async function makeRequest({
|
|||||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||||
.join('&');
|
.join('&');
|
||||||
|
|
||||||
const url = queryString
|
const apiBase = baseUrl || getApiBaseUrl(marketplace);
|
||||||
? `${getApiBaseUrl(marketplace)}${path}?${queryString}`
|
const url = queryString ? `${apiBase}${path}?${queryString}` : `${apiBase}${path}`;
|
||||||
: `${getApiBaseUrl(marketplace)}${path}`;
|
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Accept-Language': getAcceptLanguage(marketplace),
|
'Accept-Language': getAcceptLanguage(marketplace),
|
||||||
|
...extraHeaders,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (marketplace.config.marketplaceId) {
|
headers['X-EBAY-C-MARKETPLACE-ID'] = getEbayMarketplaceId(marketplace);
|
||||||
headers['X-EBAY-C-MARKETPLACE-ID'] = marketplace.config.marketplaceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (body && method !== 'GET') {
|
const isRawBody = rawBody === true || Boolean(contentType);
|
||||||
|
if (body != null && method !== 'GET') {
|
||||||
|
if (isRawBody) {
|
||||||
|
if (contentType) {
|
||||||
|
fetchOptions.headers['Content-Type'] = contentType;
|
||||||
|
}
|
||||||
|
fetchOptions.body = body;
|
||||||
|
} else {
|
||||||
fetchOptions.headers['Content-Type'] = 'application/json';
|
fetchOptions.headers['Content-Type'] = 'application/json';
|
||||||
fetchOptions.headers['Content-Language'] = getAcceptLanguage(marketplace);
|
fetchOptions.headers['Content-Language'] = getAcceptLanguage(marketplace);
|
||||||
fetchOptions.body = JSON.stringify(body);
|
fetchOptions.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
const debugBody = isRawBody
|
||||||
|
? `[binary ${body?.length ?? 0} bytes]`
|
||||||
|
: body
|
||||||
|
? formatDebugPayload(body)
|
||||||
|
: undefined;
|
||||||
logger.debug(`eBay API ${method} ${path}`, {
|
logger.debug(`eBay API ${method} ${path}`, {
|
||||||
...getMarketplaceDebugContext(marketplace),
|
...getMarketplaceDebugContext(marketplace),
|
||||||
|
host: apiBase,
|
||||||
params: Object.keys(params).length ? params : undefined,
|
params: Object.keys(params).length ? params : undefined,
|
||||||
body: body ? formatDebugPayload(body) : undefined,
|
body: debugBody,
|
||||||
acceptableStatuses: acceptableStatuses.length ? acceptableStatuses : undefined,
|
acceptableStatuses: acceptableStatuses.length ? acceptableStatuses : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -233,7 +261,7 @@ export async function makeRequest({
|
|||||||
|
|
||||||
logger.debug(`eBay API ${method} ${path} -> ${response.status} (${durationMs}ms)`, {
|
logger.debug(`eBay API ${method} ${path} -> ${response.status} (${durationMs}ms)`, {
|
||||||
...getMarketplaceDebugContext(marketplace),
|
...getMarketplaceDebugContext(marketplace),
|
||||||
response: data ? formatDebugPayload(data) : undefined,
|
response: logResponse && data ? formatDebugPayload(data) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { logger, getMarketplaceDebugContext } from './shared.js';
|
import { logger, getMarketplaceDebugContext } from './shared.js';
|
||||||
|
import { fetchEbayCategoryReferences } from './categoryTree.js';
|
||||||
|
import { syncAccountPolicies } from './accountPolicySync.js';
|
||||||
|
|
||||||
const TRADING_COMPATIBILITY_LEVEL = '1399';
|
const TRADING_COMPATIBILITY_LEVEL = '1399';
|
||||||
const TRADING_SANDBOX_URL = 'https://api.sandbox.ebay.com/ws/api.dll';
|
const TRADING_SANDBOX_URL = 'https://api.sandbox.ebay.com/ws/api.dll';
|
||||||
@ -135,10 +137,13 @@ export async function fetchEbayShippingServices(marketplace) {
|
|||||||
|
|
||||||
export async function syncMarketplaceMetadata(marketplace) {
|
export async function syncMarketplaceMetadata(marketplace) {
|
||||||
const availableShippingServices = await fetchEbayShippingServices(marketplace);
|
const availableShippingServices = await fetchEbayShippingServices(marketplace);
|
||||||
|
const categoryReferences = await fetchEbayCategoryReferences(marketplace);
|
||||||
|
await syncAccountPolicies(marketplace);
|
||||||
return {
|
return {
|
||||||
eBay: {
|
eBay: {
|
||||||
...(marketplace.eBay && typeof marketplace.eBay === 'object' ? marketplace.eBay : {}),
|
...(marketplace.eBay && typeof marketplace.eBay === 'object' ? marketplace.eBay : {}),
|
||||||
availableShippingServices,
|
availableShippingServices,
|
||||||
|
categoryReferences,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
88
src/integrations/marketplaces/ebay/variationAspects.js
Normal file
88
src/integrations/marketplaces/ebay/variationAspects.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
export function toEbayProductAspects(aspects = []) {
|
||||||
|
const result = {};
|
||||||
|
for (const aspect of aspects) {
|
||||||
|
const name = typeof aspect?.name === 'string' ? aspect.name.trim() : '';
|
||||||
|
const value = aspect?.value == null ? '' : String(aspect.value).trim();
|
||||||
|
if (!name || !value) continue;
|
||||||
|
if (!result[name]) result[name] = [];
|
||||||
|
if (!result[name].includes(value)) result[name].push(value);
|
||||||
|
}
|
||||||
|
return Object.keys(result).length ? result : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromEbayProductAspects(ebayAspects) {
|
||||||
|
if (!ebayAspects || typeof ebayAspects !== 'object' || Array.isArray(ebayAspects)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result = [];
|
||||||
|
for (const [name, values] of Object.entries(ebayAspects)) {
|
||||||
|
const list = Array.isArray(values) ? values : [values];
|
||||||
|
for (const value of list) {
|
||||||
|
if (value == null || String(value).trim() === '') continue;
|
||||||
|
result.push({ name, value: String(value) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function skuLabel(varient) {
|
||||||
|
return varient?._reference || varient?.externalReference || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGroupVariesBy(varients = []) {
|
||||||
|
const perVarient = varients.map((varient) => {
|
||||||
|
const aspects = toEbayProductAspects(varient?.aspects) || {};
|
||||||
|
const names = Object.keys(aspects);
|
||||||
|
if (!names.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Listing varient "${skuLabel(varient)}" must have at least one aspect (e.g. Color, Size) before publishing a multi-variation eBay listing.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { varient, aspects };
|
||||||
|
});
|
||||||
|
|
||||||
|
const allNames = [];
|
||||||
|
for (const { aspects } of perVarient) {
|
||||||
|
for (const name of Object.keys(aspects)) {
|
||||||
|
if (!allNames.includes(name)) allNames.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { varient, aspects } of perVarient) {
|
||||||
|
for (const name of allNames) {
|
||||||
|
if (!aspects[name]?.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Listing varient "${skuLabel(varient)}" is missing aspect "${name}". All varients in a listing must use the same aspect names.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const specifications = [];
|
||||||
|
const sharedAspects = {};
|
||||||
|
|
||||||
|
for (const name of allNames) {
|
||||||
|
const uniqueValues = [];
|
||||||
|
for (const { aspects } of perVarient) {
|
||||||
|
for (const value of aspects[name]) {
|
||||||
|
if (!uniqueValues.includes(value)) uniqueValues.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uniqueValues.length >= 2) {
|
||||||
|
specifications.push({ name, values: uniqueValues });
|
||||||
|
} else {
|
||||||
|
sharedAspects[name] = uniqueValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!specifications.length) {
|
||||||
|
throw new Error(
|
||||||
|
'Multi-variation eBay listings require varients that differ by at least one aspect (e.g. Color or Size).'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
variesBy: { specifications },
|
||||||
|
...(Object.keys(sharedAspects).length ? { aspects: sharedAspects } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -3,7 +3,16 @@ import log4js from 'log4js';
|
|||||||
import { listingModel } from '../database/schemas/sales/listing.schema.js';
|
import { listingModel } from '../database/schemas/sales/listing.schema.js';
|
||||||
import { listingVarientModel } from '../database/schemas/sales/listingvarient.schema.js';
|
import { listingVarientModel } from '../database/schemas/sales/listingvarient.schema.js';
|
||||||
import { marketplaceModel } from '../database/schemas/sales/marketplace.schema.js';
|
import { marketplaceModel } from '../database/schemas/sales/marketplace.schema.js';
|
||||||
|
import { shipmentModel } from '../database/schemas/inventory/shipment.schema.js';
|
||||||
|
import { paymentPolicyModel } from '../database/schemas/finance/paymentpolicy.schema.js';
|
||||||
|
import { returnPolicyModel } from '../database/schemas/sales/returnpolicy.schema.js';
|
||||||
|
import { fulfillmentPolicyModel } from '../database/schemas/sales/fulfillmentpolicy.schema.js';
|
||||||
|
import { taxRateModel } from '../database/schemas/management/taxrate.schema.js';
|
||||||
|
import { persistMarketplaceMapping } from './marketplaces/ebay/accountPolicies.js';
|
||||||
import { editObject } from '../database/database.js';
|
import { editObject } from '../database/database.js';
|
||||||
|
import { dbConnect } from '../database/mongo.js';
|
||||||
|
import { redisServer } from '../database/redis.js';
|
||||||
|
import { natsServer } from '../database/nats.js';
|
||||||
import * as tiktokShop from './marketplaces/tiktokShop.js';
|
import * as tiktokShop from './marketplaces/tiktokShop.js';
|
||||||
import * as ebay from './marketplaces/ebay/index.js';
|
import * as ebay from './marketplaces/ebay/index.js';
|
||||||
import {
|
import {
|
||||||
@ -35,7 +44,7 @@ export function hasIntegration(provider) {
|
|||||||
return !!providers[provider];
|
return !!providers[provider];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function publishMarketplaceOfferForSku(marketplace, user, sku, listing) {
|
export async function publishMarketplaceOfferForSku(marketplace, user, sku, listing, varient) {
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
const provider = getProvider(authenticatedMarketplace);
|
const provider = getProvider(authenticatedMarketplace);
|
||||||
if (typeof provider.publishOfferForSku !== 'function') {
|
if (typeof provider.publishOfferForSku !== 'function') {
|
||||||
@ -43,10 +52,39 @@ export async function publishMarketplaceOfferForSku(marketplace, user, sku, list
|
|||||||
`Marketplace provider "${marketplace.provider}" does not support publishing offers`
|
`Marketplace provider "${marketplace.provider}" does not support publishing offers`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return provider.publishOfferForSku(authenticatedMarketplace, sku, listing);
|
return provider.publishOfferForSku(authenticatedMarketplace, sku, listing, varient);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function withdrawMarketplaceOfferForSku(marketplace, user, sku) {
|
export async function ensureMarketplaceListingInventory(marketplace, user, listing, varients = []) {
|
||||||
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
|
const provider = getProvider(authenticatedMarketplace);
|
||||||
|
if (
|
||||||
|
typeof provider.updateItem !== 'function' ||
|
||||||
|
typeof provider.publishOfferForSku !== 'function'
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fullListing = listing?._id ? await fetchFullListing(listing._id) : listing;
|
||||||
|
if (!fullListing) throw new Error('Listing not found');
|
||||||
|
const listingVarients =
|
||||||
|
varients.length > 0 ? varients : listing?._id ? await fetchListingVarients(listing._id) : [];
|
||||||
|
return provider.updateItem(authenticatedMarketplace, fullListing, listingVarients);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncMarketplaceListingImages(marketplace, user, listing, varients = []) {
|
||||||
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
|
const provider = getProvider(authenticatedMarketplace);
|
||||||
|
if (typeof provider.syncListingImages !== 'function') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fullListing = listing?._id ? await fetchFullListing(listing._id) : listing;
|
||||||
|
if (!fullListing) throw new Error('Listing not found');
|
||||||
|
const listingVarients =
|
||||||
|
varients.length > 0 ? varients : listing?._id ? await fetchListingVarients(listing._id) : [];
|
||||||
|
return provider.syncListingImages(authenticatedMarketplace, fullListing, listingVarients);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withdrawMarketplaceOfferForSku(marketplace, user, sku, listing) {
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
const provider = getProvider(authenticatedMarketplace);
|
const provider = getProvider(authenticatedMarketplace);
|
||||||
if (typeof provider.withdrawOfferForSku !== 'function') {
|
if (typeof provider.withdrawOfferForSku !== 'function') {
|
||||||
@ -54,7 +92,7 @@ export async function withdrawMarketplaceOfferForSku(marketplace, user, sku) {
|
|||||||
`Marketplace provider "${marketplace.provider}" does not support withdrawing offers`
|
`Marketplace provider "${marketplace.provider}" does not support withdrawing offers`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return provider.withdrawOfferForSku(authenticatedMarketplace, sku);
|
return provider.withdrawOfferForSku(authenticatedMarketplace, sku, listing);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistMarketplaceUpdate(marketplace, configUpdates, marketplaceUpdates, user) {
|
async function persistMarketplaceUpdate(marketplace, configUpdates, marketplaceUpdates, user) {
|
||||||
@ -207,7 +245,11 @@ export async function handleWebhook(marketplace, event, { rawBody, signature } =
|
|||||||
const actor = marketplaceActor(marketplace);
|
const actor = marketplaceActor(marketplace);
|
||||||
|
|
||||||
if (signature && (await canVerifyWebhookSignature(marketplace))) {
|
if (signature && (await canVerifyWebhookSignature(marketplace))) {
|
||||||
const valid = await verifyWebhookSignature(marketplace, rawBody || JSON.stringify(event), signature);
|
const valid = await verifyWebhookSignature(
|
||||||
|
marketplace,
|
||||||
|
rawBody || JSON.stringify(event),
|
||||||
|
signature
|
||||||
|
);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
const error = new Error('Invalid webhook signature');
|
const error = new Error('Invalid webhook signature');
|
||||||
error.status = 401;
|
error.status = 401;
|
||||||
@ -240,6 +282,102 @@ export async function syncMarketplaceMetadata(marketplace, user) {
|
|||||||
return persistMarketplaceUpdate(authenticatedMarketplace, {}, metadataUpdates, user);
|
return persistMarketplaceUpdate(authenticatedMarketplace, {}, metadataUpdates, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function syncMarketplacePolicySet(marketplace, user, methodName) {
|
||||||
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
|
const provider = getProvider(authenticatedMarketplace);
|
||||||
|
if (typeof provider[methodName] !== 'function') {
|
||||||
|
throw new Error(
|
||||||
|
`Marketplace provider "${marketplace.provider}" does not support ${methodName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return provider[methodName](authenticatedMarketplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncFulfillmentPolicies(marketplace, user) {
|
||||||
|
return syncMarketplacePolicySet(marketplace, user, 'syncFulfillmentPolicies');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncPaymentPolicies(marketplace, user) {
|
||||||
|
return syncMarketplacePolicySet(marketplace, user, 'syncPaymentPolicies');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncReturnPolicies(marketplace, user) {
|
||||||
|
return syncMarketplacePolicySet(marketplace, user, 'syncReturnPolicies');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncTaxRates(marketplace, user) {
|
||||||
|
return syncMarketplacePolicySet(marketplace, user, 'syncTaxRates');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncOutboundAccountPolicy(
|
||||||
|
marketplace,
|
||||||
|
user,
|
||||||
|
{ policyId, model, populate, methodName, label }
|
||||||
|
) {
|
||||||
|
const policy = await model.findById(policyId).populate(populate).lean();
|
||||||
|
if (!policy) throw new Error(`${label} not found`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
|
const provider = getProvider(authenticatedMarketplace);
|
||||||
|
if (typeof provider[methodName] !== 'function') {
|
||||||
|
throw new Error(
|
||||||
|
`Marketplace provider "${marketplace.provider}" does not support ${methodName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return provider[methodName](authenticatedMarketplace, policy);
|
||||||
|
} catch (err) {
|
||||||
|
await persistMarketplaceMapping(model, policy, marketplace, {
|
||||||
|
stateType: 'failed',
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncPaymentPolicyOutbound(marketplace, user, policyId) {
|
||||||
|
return syncOutboundAccountPolicy(marketplace, user, {
|
||||||
|
policyId,
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
populate: ['marketplaces.marketplace'],
|
||||||
|
methodName: 'ensurePaymentPolicySynced',
|
||||||
|
label: 'Payment policy',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncReturnPolicyOutbound(marketplace, user, policyId) {
|
||||||
|
return syncOutboundAccountPolicy(marketplace, user, {
|
||||||
|
policyId,
|
||||||
|
model: returnPolicyModel,
|
||||||
|
populate: ['marketplaces.marketplace'],
|
||||||
|
methodName: 'ensureReturnPolicySynced',
|
||||||
|
label: 'Return policy',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncFulfillmentPolicyOutbound(marketplace, user, policyId) {
|
||||||
|
return syncOutboundAccountPolicy(marketplace, user, {
|
||||||
|
policyId,
|
||||||
|
model: fulfillmentPolicyModel,
|
||||||
|
populate: [
|
||||||
|
{ path: 'courierServices', populate: ['marketplaces.marketplace'] },
|
||||||
|
'marketplaces.marketplace',
|
||||||
|
],
|
||||||
|
methodName: 'ensureFulfillmentPolicySynced',
|
||||||
|
label: 'Fulfillment policy',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncTaxRateOutbound(marketplace, user, policyId) {
|
||||||
|
return syncOutboundAccountPolicy(marketplace, user, {
|
||||||
|
policyId,
|
||||||
|
model: taxRateModel,
|
||||||
|
populate: ['marketplaces.marketplace'],
|
||||||
|
methodName: 'ensureTaxRateSynced',
|
||||||
|
label: 'Tax rate',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureWebhookSubscriptions(marketplace, user) {
|
export async function ensureWebhookSubscriptions(marketplace, user) {
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
const provider = getProvider(authenticatedMarketplace);
|
const provider = getProvider(authenticatedMarketplace);
|
||||||
@ -265,9 +403,19 @@ export async function debugMarketplaceGet(marketplace, user, path, params = {})
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setListingState(listingId, stateType, user, message) {
|
function roundProgress(progress) {
|
||||||
|
const clamped = Math.min(1, Math.max(0, Number(progress) || 0));
|
||||||
|
return Math.round(clamped * 1000) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setListingState(listingId, stateType, user, messageOrOptions) {
|
||||||
|
const options =
|
||||||
|
typeof messageOrOptions === 'string' ? { message: messageOrOptions } : messageOrOptions || {};
|
||||||
const state = { type: stateType };
|
const state = { type: stateType };
|
||||||
if (message) state.message = message;
|
if (options.message) state.message = options.message;
|
||||||
|
if (options.progress != null && Number.isFinite(Number(options.progress))) {
|
||||||
|
state.progress = roundProgress(options.progress);
|
||||||
|
}
|
||||||
return editObject({
|
return editObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
id: listingId,
|
id: listingId,
|
||||||
@ -308,15 +456,37 @@ async function recalculateMarketplaceState(marketplace, user) {
|
|||||||
async function fetchFullListing(listingId) {
|
async function fetchFullListing(listingId) {
|
||||||
return listingModel
|
return listingModel
|
||||||
.findById(listingId)
|
.findById(listingId)
|
||||||
.populate(['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'])
|
.populate([
|
||||||
|
'product',
|
||||||
|
'vendor',
|
||||||
|
'stockLocation',
|
||||||
|
'courierServices',
|
||||||
|
{
|
||||||
|
path: 'marketplace',
|
||||||
|
populate: ['defaultFulfillmentPolicy', 'defaultPaymentPolicy', 'defaultReturnPolicy'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'fulfillmentPolicy',
|
||||||
|
populate: ['courierServices', 'marketplaces.marketplace'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'paymentPolicy',
|
||||||
|
populate: ['marketplaces.marketplace'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'returnPolicy',
|
||||||
|
populate: ['marketplaces.marketplace'],
|
||||||
|
},
|
||||||
|
'listingImages',
|
||||||
|
])
|
||||||
.lean();
|
.lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchListingVarients(listingId) {
|
async function fetchListingVarients(listingId) {
|
||||||
return listingVarientModel.find({ listing: listingId }).lean();
|
return listingVarientModel.find({ listing: listingId }).populate('listingImages').lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createListing(marketplace, user, listingData) {
|
export async function createListing(marketplace, user, listingData) {
|
||||||
const provider = getProvider(marketplace);
|
const provider = getProvider(marketplace);
|
||||||
if (!provider.createItem) {
|
if (!provider.createItem) {
|
||||||
logger.debug(`Provider ${marketplace.provider} does not support createItem — skipping`);
|
logger.debug(`Provider ${marketplace.provider} does not support createItem — skipping`);
|
||||||
@ -324,12 +494,11 @@ export function createListing(marketplace, user, listingData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (listingData._id) {
|
if (listingData._id) {
|
||||||
setListingState(listingData._id, 'syncing', user).catch((err) =>
|
await setListingState(listingData._id, 'syncing', user).catch((err) =>
|
||||||
logger.warn(`Failed to set listing syncing state: ${err.message}`)
|
logger.warn(`Failed to set listing syncing state: ${err.message}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const work = async () => {
|
|
||||||
try {
|
try {
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
const fullListing = listingData._id ? await fetchFullListing(listingData._id) : listingData;
|
const fullListing = listingData._id ? await fetchFullListing(listingData._id) : listingData;
|
||||||
@ -337,9 +506,7 @@ export function createListing(marketplace, user, listingData) {
|
|||||||
|
|
||||||
const varients = listingData._id ? await fetchListingVarients(listingData._id) : [];
|
const varients = listingData._id ? await fetchListingVarients(listingData._id) : [];
|
||||||
|
|
||||||
logger.info(
|
logger.info(`Creating listing on marketplace "${marketplace.name}" (${marketplace.provider})`);
|
||||||
`Creating listing on marketplace "${marketplace.name}" (${marketplace.provider})`
|
|
||||||
);
|
|
||||||
const result = await provider.createItem(authenticatedMarketplace, fullListing, varients);
|
const result = await provider.createItem(authenticatedMarketplace, fullListing, varients);
|
||||||
|
|
||||||
if (listingData._id) {
|
if (listingData._id) {
|
||||||
@ -370,13 +537,11 @@ export function createListing(marketplace, user, listingData) {
|
|||||||
if (listingData._id) {
|
if (listingData._id) {
|
||||||
await setListingState(listingData._id, 'draft', user, err.message).catch(() => {});
|
await setListingState(listingData._id, 'draft', user, err.message).catch(() => {});
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
work();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateListing(marketplace, user, listingData) {
|
export async function updateListing(marketplace, user, listingData) {
|
||||||
const provider = getProvider(marketplace);
|
const provider = getProvider(marketplace);
|
||||||
if (!provider.updateItem) {
|
if (!provider.updateItem) {
|
||||||
logger.debug(`Provider ${marketplace.provider} does not support updateItem — skipping`);
|
logger.debug(`Provider ${marketplace.provider} does not support updateItem — skipping`);
|
||||||
@ -384,12 +549,11 @@ export function updateListing(marketplace, user, listingData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (listingData._id) {
|
if (listingData._id) {
|
||||||
setListingState(listingData._id, 'syncing', user).catch((err) =>
|
await setListingState(listingData._id, 'syncing', user).catch((err) =>
|
||||||
logger.warn(`Failed to set listing syncing state: ${err.message}`)
|
logger.warn(`Failed to set listing syncing state: ${err.message}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const work = async () => {
|
|
||||||
try {
|
try {
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
const fullListing = listingData._id ? await fetchFullListing(listingData._id) : listingData;
|
const fullListing = listingData._id ? await fetchFullListing(listingData._id) : listingData;
|
||||||
@ -397,9 +561,7 @@ export function updateListing(marketplace, user, listingData) {
|
|||||||
|
|
||||||
const varients = listingData._id ? await fetchListingVarients(listingData._id) : [];
|
const varients = listingData._id ? await fetchListingVarients(listingData._id) : [];
|
||||||
|
|
||||||
logger.info(
|
logger.info(`Updating listing on marketplace "${marketplace.name}" (${marketplace.provider})`);
|
||||||
`Updating listing on marketplace "${marketplace.name}" (${marketplace.provider})`
|
|
||||||
);
|
|
||||||
const result = await provider.updateItem(authenticatedMarketplace, fullListing, varients);
|
const result = await provider.updateItem(authenticatedMarketplace, fullListing, varients);
|
||||||
|
|
||||||
if (listingData._id) {
|
if (listingData._id) {
|
||||||
@ -435,46 +597,40 @@ export function updateListing(marketplace, user, listingData) {
|
|||||||
if (listingData._id) {
|
if (listingData._id) {
|
||||||
await setListingState(listingData._id, 'active', user, err.message).catch(() => {});
|
await setListingState(listingData._id, 'active', user, err.message).catch(() => {});
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
work();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteListing(marketplace, user, listingData) {
|
export async function deleteListing(marketplace, user, listingData) {
|
||||||
const provider = getProvider(marketplace);
|
const provider = getProvider(marketplace);
|
||||||
if (!provider.deleteItem) {
|
if (!provider.deleteItem) {
|
||||||
logger.debug(`Provider ${marketplace.provider} does not support deleteItem — skipping`);
|
logger.debug(`Provider ${marketplace.provider} does not support deleteItem — skipping`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const work = async () => {
|
|
||||||
try {
|
try {
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
logger.info(
|
logger.info(
|
||||||
`Deleting listing from marketplace "${marketplace.name}" (${marketplace.provider})`
|
`Deleting listing from marketplace "${marketplace.name}" (${marketplace.provider})`
|
||||||
);
|
);
|
||||||
const varients = listingData._id
|
const varients = listingData?._id
|
||||||
? await fetchListingVarients(listingData._id)
|
? await fetchListingVarients(listingData._id).catch(() => listingData.varients || [])
|
||||||
: [];
|
: listingData?.varients || [];
|
||||||
await provider.deleteItem(authenticatedMarketplace, listingData, varients);
|
await provider.deleteItem(authenticatedMarketplace, listingData, varients);
|
||||||
logger.info(`Background deleteListing complete for marketplace "${marketplace.name}"`);
|
logger.info(`Background deleteListing complete for marketplace "${marketplace.name}"`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Background deleteListing failed for marketplace "${marketplace.name}": ${err.message}`
|
`Background deleteListing failed for marketplace "${marketplace.name}": ${err.message}`
|
||||||
);
|
);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
work();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncItems(marketplace, user) {
|
export async function syncItems(marketplace, user) {
|
||||||
setMarketplaceState(marketplace._id, 'syncing', user).catch((err) =>
|
await setMarketplaceState(marketplace._id, 'syncing', user).catch((err) =>
|
||||||
logger.warn(`Failed to set marketplace syncing state: ${err.message}`)
|
logger.warn(`Failed to set marketplace syncing state: ${err.message}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
const work = async () => {
|
|
||||||
try {
|
try {
|
||||||
const provider = getProvider(marketplace);
|
const provider = getProvider(marketplace);
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
@ -574,12 +730,9 @@ export function syncItems(marketplace, user) {
|
|||||||
results.push({ _reference: listing._reference, action: 'synced', id: listing._id });
|
results.push({ _reference: listing._reference, action: 'synced', id: listing._id });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`Failed to sync listing ${listing._reference}: ${err.message}`);
|
logger.warn(`Failed to sync listing ${listing._reference}: ${err.message}`);
|
||||||
await setListingState(
|
await setListingState(listing._id, listing.state?.type || 'draft', user, err.message).catch(
|
||||||
listing._id,
|
() => {}
|
||||||
listing.state?.type || 'draft',
|
);
|
||||||
user,
|
|
||||||
err.message
|
|
||||||
).catch(() => {});
|
|
||||||
results.push({
|
results.push({
|
||||||
_reference: listing._reference,
|
_reference: listing._reference,
|
||||||
action: 'error',
|
action: 'error',
|
||||||
@ -598,18 +751,15 @@ export function syncItems(marketplace, user) {
|
|||||||
`Background syncItems failed for marketplace "${marketplace.name}": ${err.message}`
|
`Background syncItems failed for marketplace "${marketplace.name}": ${err.message}`
|
||||||
);
|
);
|
||||||
await recalculateMarketplaceState(marketplace, user);
|
await recalculateMarketplaceState(marketplace, user);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
work();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncOrders(marketplace, user, { startTime, endTime } = {}) {
|
export async function syncOrders(marketplace, user, { startTime, endTime } = {}) {
|
||||||
setMarketplaceState(marketplace._id, 'syncing', user).catch((err) =>
|
await setMarketplaceState(marketplace._id, 'syncing', user).catch((err) =>
|
||||||
logger.warn(`Failed to set marketplace syncing state: ${err.message}`)
|
logger.warn(`Failed to set marketplace syncing state: ${err.message}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
const work = async () => {
|
|
||||||
try {
|
try {
|
||||||
const provider = getProvider(marketplace);
|
const provider = getProvider(marketplace);
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
||||||
@ -657,10 +807,8 @@ export function syncOrders(marketplace, user, { startTime, endTime } = {}) {
|
|||||||
`Background syncOrders failed for marketplace "${marketplace.name}": ${err.message}`
|
`Background syncOrders failed for marketplace "${marketplace.name}": ${err.message}`
|
||||||
);
|
);
|
||||||
await recalculateMarketplaceState(marketplace, user);
|
await recalculateMarketplaceState(marketplace, user);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
work();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushMarketplaceShipmentFulfillment(marketplace, user, shipment) {
|
export async function pushMarketplaceShipmentFulfillment(marketplace, user, shipment) {
|
||||||
@ -674,4 +822,374 @@ export async function pushMarketplaceShipmentFulfillment(marketplace, user, ship
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userRef(userId) {
|
||||||
|
if (!userId) return null;
|
||||||
|
return { _id: userId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMarketplace(marketplaceId) {
|
||||||
|
if (!marketplaceId) throw new Error('Marketplace id is required');
|
||||||
|
const marketplace = await marketplaceModel.findById(marketplaceId);
|
||||||
|
if (!marketplace) throw new Error('Marketplace not found');
|
||||||
|
return marketplace;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishListingOffers({
|
||||||
|
listingId,
|
||||||
|
userId,
|
||||||
|
varientIds,
|
||||||
|
restoreStateType = 'draft',
|
||||||
|
varientRestoreStateType = 'draft',
|
||||||
|
}) {
|
||||||
|
const user = userRef(userId);
|
||||||
|
const listing = await fetchFullListing(listingId);
|
||||||
|
if (!listing) throw new Error('Listing not found');
|
||||||
|
|
||||||
|
const marketplace = listing.marketplace?._id
|
||||||
|
? listing.marketplace
|
||||||
|
: await loadMarketplace(listing.marketplace);
|
||||||
|
if (!marketplace) throw new Error('Listing has no marketplace');
|
||||||
|
|
||||||
|
const allVarients = await fetchListingVarients(listingId);
|
||||||
|
const selectedIds =
|
||||||
|
Array.isArray(varientIds) && varientIds.length > 0
|
||||||
|
? new Set(varientIds.map((id) => String(id)))
|
||||||
|
: null;
|
||||||
|
const toPublish = allVarients.filter((varient) => {
|
||||||
|
if (!varient._reference) return false;
|
||||||
|
if (selectedIds && !selectedIds.has(String(varient._id))) return false;
|
||||||
|
return varient.state?.type !== 'active';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toPublish.length === 0) {
|
||||||
|
throw new Error('No variants to publish (all are already active or missing SKU).');
|
||||||
|
}
|
||||||
|
|
||||||
|
await setListingState(listingId, 'publishing', user, { progress: 0.05 });
|
||||||
|
for (const varient of toPublish) {
|
||||||
|
await setListingVarientState(varient._id, 'publishing', user).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureMarketplaceListingInventory(marketplace, user, listing, allVarients);
|
||||||
|
await setListingState(listingId, 'publishing', user, { progress: 0.15 });
|
||||||
|
|
||||||
|
let publishedListingId = listing.externalReference;
|
||||||
|
let publishedUrl = listing.url;
|
||||||
|
for (let i = 0; i < toPublish.length; i += 1) {
|
||||||
|
const varient = toPublish[i];
|
||||||
|
const apiResult = await publishMarketplaceOfferForSku(
|
||||||
|
marketplace,
|
||||||
|
user,
|
||||||
|
marketplaceSku(varient),
|
||||||
|
listing,
|
||||||
|
varient
|
||||||
|
);
|
||||||
|
await editObject({
|
||||||
|
model: listingVarientModel,
|
||||||
|
id: varient._id,
|
||||||
|
updateData: {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
state: { type: 'active' },
|
||||||
|
lastSyncedAt: new Date(),
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
if (apiResult?.listingId) {
|
||||||
|
publishedListingId = apiResult.listingId;
|
||||||
|
listing.externalReference = apiResult.listingId;
|
||||||
|
} else if (apiResult?.externalReference) {
|
||||||
|
publishedListingId = apiResult.externalReference;
|
||||||
|
listing.externalReference = apiResult.externalReference;
|
||||||
|
}
|
||||||
|
if (apiResult?.url) {
|
||||||
|
publishedUrl = apiResult.url;
|
||||||
|
listing.url = apiResult.url;
|
||||||
|
}
|
||||||
|
await setListingState(listingId, 'publishing', user, {
|
||||||
|
progress: 0.15 + (0.8 * (i + 1)) / toPublish.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const listingUpdate = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
state: { type: 'active' },
|
||||||
|
lastSyncedAt: new Date(),
|
||||||
|
};
|
||||||
|
if (publishedListingId) listingUpdate.externalReference = publishedListingId;
|
||||||
|
if (publishedUrl) listingUpdate.url = publishedUrl;
|
||||||
|
await editObject({
|
||||||
|
model: listingModel,
|
||||||
|
id: listingId,
|
||||||
|
updateData: listingUpdate,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
logger.info(`Background publishListing complete for listing ${listingId}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Background publishListing failed for listing ${listingId}: ${err.message}`);
|
||||||
|
await setListingState(listingId, restoreStateType, user, err.message).catch(() => {});
|
||||||
|
for (const varient of toPublish) {
|
||||||
|
await setListingVarientState(varient._id, varientRestoreStateType, user, err.message).catch(
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unpublishListingOffers({
|
||||||
|
listingId,
|
||||||
|
userId,
|
||||||
|
varientIds,
|
||||||
|
restoreStateType = 'active',
|
||||||
|
varientRestoreStateType = 'active',
|
||||||
|
}) {
|
||||||
|
const user = userRef(userId);
|
||||||
|
const listing = await fetchFullListing(listingId);
|
||||||
|
if (!listing) throw new Error('Listing not found');
|
||||||
|
|
||||||
|
const marketplace = listing.marketplace?._id
|
||||||
|
? listing.marketplace
|
||||||
|
: await loadMarketplace(listing.marketplace);
|
||||||
|
if (!marketplace) throw new Error('Listing has no marketplace');
|
||||||
|
|
||||||
|
const allVarients = await fetchListingVarients(listingId);
|
||||||
|
const selectedIds =
|
||||||
|
Array.isArray(varientIds) && varientIds.length > 0
|
||||||
|
? new Set(varientIds.map((id) => String(id)))
|
||||||
|
: null;
|
||||||
|
const toUnpublish = allVarients.filter((varient) => {
|
||||||
|
if (!varient._reference) return false;
|
||||||
|
if (selectedIds && !selectedIds.has(String(varient._id))) return false;
|
||||||
|
return varient.state?.type === 'active' || varient.state?.type === 'unpublishing';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toUnpublish.length === 0) {
|
||||||
|
throw new Error('No active variants to unpublish.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await setListingState(listingId, 'unpublishing', user, { progress: 0.05 });
|
||||||
|
for (const varient of toUnpublish) {
|
||||||
|
await setListingVarientState(varient._id, 'unpublishing', user).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await syncMarketplaceListingImages(marketplace, user, listing, allVarients);
|
||||||
|
await setListingState(listingId, 'unpublishing', user, { progress: 0.15 });
|
||||||
|
|
||||||
|
for (let i = 0; i < toUnpublish.length; i += 1) {
|
||||||
|
const varient = toUnpublish[i];
|
||||||
|
await withdrawMarketplaceOfferForSku(marketplace, user, marketplaceSku(varient), listing);
|
||||||
|
await editObject({
|
||||||
|
model: listingVarientModel,
|
||||||
|
id: varient._id,
|
||||||
|
updateData: {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
state: { type: 'draft' },
|
||||||
|
lastSyncedAt: new Date(),
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
await setListingState(listingId, 'unpublishing', user, {
|
||||||
|
progress: 0.15 + (0.8 * (i + 1)) / toUnpublish.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingActive = await listingVarientModel.exists({
|
||||||
|
listing: listingId,
|
||||||
|
'state.type': 'active',
|
||||||
|
});
|
||||||
|
await editObject({
|
||||||
|
model: listingModel,
|
||||||
|
id: listingId,
|
||||||
|
updateData: {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
state: { type: remainingActive ? 'active' : 'draft' },
|
||||||
|
lastSyncedAt: new Date(),
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
logger.info(`Background unpublishListing complete for listing ${listingId}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Background unpublishListing failed for listing ${listingId}: ${err.message}`);
|
||||||
|
await setListingState(listingId, restoreStateType, user, err.message).catch(() => {});
|
||||||
|
for (const varient of toUnpublish) {
|
||||||
|
await setListingVarientState(varient._id, varientRestoreStateType, user, err.message).catch(
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runJob(action, payload = {}) {
|
||||||
|
const user = userRef(payload.userId);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'createListing': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return createListing(marketplace, user, { _id: payload.listingId });
|
||||||
|
}
|
||||||
|
case 'updateListing': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return updateListing(marketplace, user, { _id: payload.listingId });
|
||||||
|
}
|
||||||
|
case 'deleteListing': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return deleteListing(marketplace, user, payload.listing);
|
||||||
|
}
|
||||||
|
case 'publishListing':
|
||||||
|
return publishListingOffers(payload);
|
||||||
|
case 'unpublishListing':
|
||||||
|
return unpublishListingOffers(payload);
|
||||||
|
case 'syncItems': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncItems(marketplace, user);
|
||||||
|
}
|
||||||
|
case 'syncOrders': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncOrders(marketplace, user, {
|
||||||
|
startTime: payload.startTime,
|
||||||
|
endTime: payload.endTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'syncMarketplaceMetadata': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncMarketplaceMetadata(marketplace, user);
|
||||||
|
}
|
||||||
|
case 'syncFulfillmentPolicies': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncFulfillmentPolicies(marketplace, user);
|
||||||
|
}
|
||||||
|
case 'syncPaymentPolicies': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncPaymentPolicies(marketplace, user);
|
||||||
|
}
|
||||||
|
case 'syncReturnPolicies': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncReturnPolicies(marketplace, user);
|
||||||
|
}
|
||||||
|
case 'syncTaxRates': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncTaxRates(marketplace, user);
|
||||||
|
}
|
||||||
|
case 'syncPaymentPolicy': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncPaymentPolicyOutbound(marketplace, user, payload.policyId);
|
||||||
|
}
|
||||||
|
case 'syncReturnPolicy': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncReturnPolicyOutbound(marketplace, user, payload.policyId);
|
||||||
|
}
|
||||||
|
case 'syncFulfillmentPolicy': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncFulfillmentPolicyOutbound(marketplace, user, payload.policyId);
|
||||||
|
}
|
||||||
|
case 'syncTaxRate': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return syncTaxRateOutbound(marketplace, user, payload.policyId);
|
||||||
|
}
|
||||||
|
case 'ensureWebhookSubscriptions': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return ensureWebhookSubscriptions(marketplace, user);
|
||||||
|
}
|
||||||
|
case 'pushShipmentFulfillment': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
const shipment = payload.shipmentId
|
||||||
|
? await shipmentModel.findById(payload.shipmentId).populate('courierService').lean()
|
||||||
|
: payload.shipment;
|
||||||
|
if (!shipment) throw new Error('Shipment not found');
|
||||||
|
return pushMarketplaceShipmentFulfillment(marketplace, user, shipment);
|
||||||
|
}
|
||||||
|
case 'getAuthorizationUrl': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return getAuthorizationUrl(marketplace, { state: payload.state });
|
||||||
|
}
|
||||||
|
case 'exchangeAuthorizationCode': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return exchangeAuthorizationCode(marketplace, user, {
|
||||||
|
code: payload.code,
|
||||||
|
state: payload.state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'refreshMarketplaceAuth': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return refreshMarketplaceAuth(marketplace, user);
|
||||||
|
}
|
||||||
|
case 'handleWebhook': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return handleWebhook(marketplace, payload.event, {
|
||||||
|
rawBody: payload.rawBody,
|
||||||
|
signature: payload.signature,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'buildWebhookChallengeResponse': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return buildWebhookChallengeResponse(marketplace, payload.query);
|
||||||
|
}
|
||||||
|
case 'canAuthorize': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return canAuthorize(marketplace);
|
||||||
|
}
|
||||||
|
case 'canVerifyWebhookSignature': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return canVerifyWebhookSignature(marketplace);
|
||||||
|
}
|
||||||
|
case 'verifyWebhookSignature': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return verifyWebhookSignature(marketplace, payload.rawBody, payload.signature);
|
||||||
|
}
|
||||||
|
case 'debugMarketplaceGet': {
|
||||||
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
return debugMarketplaceGet(marketplace, user, payload.path, payload.params);
|
||||||
|
}
|
||||||
|
case 'hasIntegration':
|
||||||
|
return hasIntegration(payload.provider);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown marketplace worker action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startWorkerProcess() {
|
||||||
|
logger.info('Starting marketplace worker process...');
|
||||||
|
await dbConnect();
|
||||||
|
await redisServer.connect();
|
||||||
|
await natsServer.connect();
|
||||||
|
|
||||||
|
process.on('message', (message) => {
|
||||||
|
if (!message || message.type === 'ready') return;
|
||||||
|
const { id, action, payload, wait } = message;
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
const job = runJob(action, payload || {});
|
||||||
|
if (wait) {
|
||||||
|
job
|
||||||
|
.then((result) => {
|
||||||
|
process.send?.({ id, ok: true, result: result ?? null });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(`Marketplace job "${action}" failed: ${err.message}`);
|
||||||
|
process.send?.({ id, ok: false, error: err.message });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
job.catch((err) => {
|
||||||
|
logger.error(`Background marketplace job "${action}" failed: ${err.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.send?.({ type: 'ready' });
|
||||||
|
logger.info('Marketplace worker process ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.MARKETPLACE_WORKER === '1') {
|
||||||
|
startWorkerProcess().catch((err) => {
|
||||||
|
logger.error('Marketplace worker failed to start:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export { marketplaceSku, marketplaceActor };
|
export { marketplaceSku, marketplaceActor };
|
||||||
|
|||||||
@ -58,7 +58,7 @@ import {
|
|||||||
} from '../../services/finance/invoices.js';
|
} from '../../services/finance/invoices.js';
|
||||||
|
|
||||||
// list of invoices
|
// list of invoices
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('invoice', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters, true, invoiceModel);
|
const filter = await getFilter(req.query, listAllowedFilters, true, invoiceModel);
|
||||||
listInvoicesRouteHandler(
|
listInvoicesRouteHandler(
|
||||||
|
|||||||
120
src/routes/finance/paymentpolicies.js
Normal file
120
src/routes/finance/paymentpolicies.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import { checkPermissions } from '../../database/permissions.js';
|
||||||
|
import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||||
|
import {
|
||||||
|
listPaymentPoliciesRouteHandler,
|
||||||
|
getPaymentPolicyRouteHandler,
|
||||||
|
editPaymentPolicyRouteHandler,
|
||||||
|
newPaymentPolicyRouteHandler,
|
||||||
|
deletePaymentPolicyRouteHandler,
|
||||||
|
listPaymentPoliciesByPropertiesRouteHandler,
|
||||||
|
getPaymentPolicyStatsRouteHandler,
|
||||||
|
getPaymentPolicyHistoryRouteHandler,
|
||||||
|
searchPaymentPoliciesRouteHandler,
|
||||||
|
getPaymentPolicyPropertyValuesRouteHandler,
|
||||||
|
getPaymentPolicyNeighborsRouteHandler,
|
||||||
|
} from '../../services/finance/paymentpolicies.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const listAllowedFilters = [
|
||||||
|
'name',
|
||||||
|
'immediatePay',
|
||||||
|
'marketplaces.marketplace',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'_reference',
|
||||||
|
];
|
||||||
|
const listAllowedSorters = ['name', 'immediatePay', 'createdAt', '_id', 'updatedAt'];
|
||||||
|
const propertiesAllowedFilters = ['name', 'immediatePay', 'marketplaces.marketplace'];
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, checkPermissions('paymentPolicy', 'list'), async (req, res) => {
|
||||||
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
|
listPaymentPoliciesRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/properties',
|
||||||
|
checkPermissions('paymentPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
const properties = convertPropertiesString(req.query.properties);
|
||||||
|
const filter = await getFilter(req.query, propertiesAllowedFilters, false);
|
||||||
|
let masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
|
}
|
||||||
|
listPaymentPoliciesByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/values',
|
||||||
|
checkPermissions('paymentPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
getPaymentPolicyPropertyValuesRouteHandler(req, res, req.query.property);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/search',
|
||||||
|
checkPermissions('paymentPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
searchPaymentPoliciesRouteHandler(req, res, req.query.search);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, checkPermissions('paymentPolicy', 'new'), async (req, res) => {
|
||||||
|
newPaymentPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/stats', isAuthenticated, async (req, res) => {
|
||||||
|
getPaymentPolicyStatsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/history', isAuthenticated, async (req, res) => {
|
||||||
|
getPaymentPolicyHistoryRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/neighbors', isAuthenticated, async (req, res) => {
|
||||||
|
const { property, search, sortProperty, sortOrder, id } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
|
getPaymentPolicyNeighborsRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
getPaymentPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', isAuthenticated, checkPermissions('paymentPolicy', 'edit'), async (req, res) => {
|
||||||
|
editPaymentPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
deletePaymentPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -49,7 +49,7 @@ import {
|
|||||||
} from '../../services/finance/payments.js';
|
} from '../../services/finance/payments.js';
|
||||||
|
|
||||||
// list of payments
|
// list of payments
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('payment', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listPaymentsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listPaymentsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import {
|
|||||||
} from '../../services/finance/taxrecords.js';
|
} from '../../services/finance/taxrecords.js';
|
||||||
|
|
||||||
// list of tax records
|
// list of tax records
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('taxRecord', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listTaxRecordsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listTaxRecordsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -48,6 +48,9 @@ import salesOrderRoutes from './sales/salesorders.js';
|
|||||||
import marketplaceRoutes from './sales/marketplaces.js';
|
import marketplaceRoutes from './sales/marketplaces.js';
|
||||||
import listingRoutes from './sales/listings.js';
|
import listingRoutes from './sales/listings.js';
|
||||||
import listingVarientRoutes from './sales/listingvarients.js';
|
import listingVarientRoutes from './sales/listingvarients.js';
|
||||||
|
import fulfillmentPolicyRoutes from './sales/fulfillmentpolicies.js';
|
||||||
|
import returnPolicyRoutes from './sales/returnpolicies.js';
|
||||||
|
import paymentPolicyRoutes from './finance/paymentpolicies.js';
|
||||||
import noteRoutes from './misc/notes.js';
|
import noteRoutes from './misc/notes.js';
|
||||||
import userNotifierRoutes from './misc/usernotifiers.js';
|
import userNotifierRoutes from './misc/usernotifiers.js';
|
||||||
import notificationRoutes from './misc/notifications.js';
|
import notificationRoutes from './misc/notifications.js';
|
||||||
@ -112,6 +115,9 @@ export {
|
|||||||
marketplaceRoutes,
|
marketplaceRoutes,
|
||||||
listingRoutes,
|
listingRoutes,
|
||||||
listingVarientRoutes,
|
listingVarientRoutes,
|
||||||
|
fulfillmentPolicyRoutes,
|
||||||
|
returnPolicyRoutes,
|
||||||
|
paymentPolicyRoutes,
|
||||||
userNotifierRoutes,
|
userNotifierRoutes,
|
||||||
notificationRoutes,
|
notificationRoutes,
|
||||||
odataRoutes,
|
odataRoutes,
|
||||||
|
|||||||
@ -44,7 +44,7 @@ import {
|
|||||||
} from '../../services/inventory/filamentstocks.js';
|
} from '../../services/inventory/filamentstocks.js';
|
||||||
|
|
||||||
// list of filament stocks
|
// list of filament stocks
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('filamentStock', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listFilamentStocksRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listFilamentStocksRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -48,7 +48,7 @@ import {
|
|||||||
} from '../../services/inventory/orderitems.js';
|
} from '../../services/inventory/orderitems.js';
|
||||||
|
|
||||||
// list of order items
|
// list of order items
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('orderItem', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listOrderItemsRouteHandler(
|
listOrderItemsRouteHandler(
|
||||||
|
|||||||
@ -9,7 +9,6 @@ const listAllowedFilters = [
|
|||||||
'partSku',
|
'partSku',
|
||||||
'partSku._id',
|
'partSku._id',
|
||||||
'state',
|
'state',
|
||||||
'startingQuantity',
|
|
||||||
'currentQuantity',
|
'currentQuantity',
|
||||||
'stockLocation',
|
'stockLocation',
|
||||||
'stockLocation._id',
|
'stockLocation._id',
|
||||||
@ -17,7 +16,7 @@ const listAllowedFilters = [
|
|||||||
'updatedAt',
|
'updatedAt',
|
||||||
'_reference',
|
'_reference',
|
||||||
];
|
];
|
||||||
const listAllowedSorters = ['partSku', 'startingQuantity', 'currentQuantity', 'state', 'createdAt', 'updatedAt'];
|
const listAllowedSorters = ['partSku', 'currentQuantity', 'state', 'createdAt', 'updatedAt'];
|
||||||
const propertiesAllowedFilters = ['part', 'state.type'];
|
const propertiesAllowedFilters = ['part', 'state.type'];
|
||||||
import {
|
import {
|
||||||
listPartStocksRouteHandler,
|
listPartStocksRouteHandler,
|
||||||
@ -26,6 +25,7 @@ import {
|
|||||||
editMultiplePartStocksRouteHandler,
|
editMultiplePartStocksRouteHandler,
|
||||||
newPartStockRouteHandler,
|
newPartStockRouteHandler,
|
||||||
deletePartStockRouteHandler,
|
deletePartStockRouteHandler,
|
||||||
|
postPartStockRouteHandler,
|
||||||
listPartStocksByPropertiesRouteHandler,
|
listPartStocksByPropertiesRouteHandler,
|
||||||
getPartStockStatsRouteHandler,
|
getPartStockStatsRouteHandler,
|
||||||
getPartStockHistoryRouteHandler,
|
getPartStockHistoryRouteHandler,
|
||||||
@ -36,7 +36,7 @@ import {
|
|||||||
} from '../../services/inventory/partstocks.js';
|
} from '../../services/inventory/partstocks.js';
|
||||||
|
|
||||||
// list of part stocks
|
// list of part stocks
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('partStock', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listPartStocksRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listPartStocksRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
@ -99,4 +99,8 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
|
|||||||
deletePartStockRouteHandler(req, res);
|
deletePartStockRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/:id/post', isAuthenticated, checkPermissions('partStock', 'post'), async (req, res) => {
|
||||||
|
postPartStockRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -35,7 +35,7 @@ import {
|
|||||||
getProductStockNeighborsRouteHandler
|
getProductStockNeighborsRouteHandler
|
||||||
} from '../../services/inventory/productstocks.js';
|
} from '../../services/inventory/productstocks.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('productStock', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listProductStocksRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listProductStocksRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -62,7 +62,7 @@ import {
|
|||||||
} from '../../services/inventory/purchaseorders.js';
|
} from '../../services/inventory/purchaseorders.js';
|
||||||
|
|
||||||
// list of purchase orders
|
// list of purchase orders
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('purchaseOrder', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listPurchaseOrdersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listPurchaseOrdersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -53,7 +53,7 @@ import {
|
|||||||
} from '../../services/inventory/shipments.js';
|
} from '../../services/inventory/shipments.js';
|
||||||
|
|
||||||
// list of shipments
|
// list of shipments
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('shipment', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listShipmentsRouteHandler(
|
listShipmentsRouteHandler(
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const listAllowedFilters = [
|
|||||||
const listAllowedSorters = ['createdAt', 'updatedAt', 'state'];
|
const listAllowedSorters = ['createdAt', 'updatedAt', 'state'];
|
||||||
|
|
||||||
// List stock audits
|
// List stock audits
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('stockAudit', 'list'), async (req, res) => {
|
||||||
const { page, limit, property } = req.query;
|
const { page, limit, property } = req.query;
|
||||||
|
|
||||||
var filter = {};
|
var filter = {};
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import {
|
|||||||
} from '../../services/inventory/stockevents.js';
|
} from '../../services/inventory/stockevents.js';
|
||||||
|
|
||||||
// list of stock events
|
// list of stock events
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('stockEvent', 'list'), async (req, res) => {
|
||||||
const { page, limit, sortProperty, sortOrder } = req.query;
|
const { page, limit, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listStockEventsRouteHandler(req, res, page, limit, filter, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listStockEventsRouteHandler(req, res, page, limit, filter, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import {
|
|||||||
getStockLocationNeighborsRouteHandler
|
getStockLocationNeighborsRouteHandler
|
||||||
} from '../../services/inventory/stocklocations.js';
|
} from '../../services/inventory/stocklocations.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('stockLocation', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listStockLocationsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listStockLocationsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import {
|
|||||||
getStockTransferNeighborsRouteHandler
|
getStockTransferNeighborsRouteHandler
|
||||||
} from '../../services/inventory/stocktransfers.js';
|
} from '../../services/inventory/stocktransfers.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('stockTransfer', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listStockTransfersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listStockTransfersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import {
|
|||||||
getAppPasswordNeighborsRouteHandler
|
getAppPasswordNeighborsRouteHandler
|
||||||
} from '../../services/management/apppasswords.js';
|
} from '../../services/management/apppasswords.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('appPassword', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listAppPasswordsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listAppPasswordsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const listAllowedFilters = [
|
|||||||
];
|
];
|
||||||
const listAllowedSorters = ['createdAt', 'updatedAt'];
|
const listAllowedSorters = ['createdAt', 'updatedAt'];
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('auditLog', 'list'), async (req, res) => {
|
||||||
const { page, limit, sortProperty, sortOrder } = req.query;
|
const { page, limit, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listAuditLogsRouteHandler(
|
listAuditLogsRouteHandler(
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import {
|
|||||||
} from '../../services/management/courier.js';
|
} from '../../services/management/courier.js';
|
||||||
|
|
||||||
// list of couriers
|
// list of couriers
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('courier', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listCouriersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listCouriersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -60,7 +60,7 @@ import {
|
|||||||
} from '../../services/management/courierservice.js';
|
} from '../../services/management/courierservice.js';
|
||||||
|
|
||||||
// list of courier services
|
// list of courier services
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('courierService', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listCourierServicesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listCourierServicesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import {
|
|||||||
} from '../../services/management/documentjobs.js';
|
} from '../../services/management/documentjobs.js';
|
||||||
|
|
||||||
// list of document jobs
|
// list of document jobs
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('documentJob', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listDocumentJobsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listDocumentJobsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import {
|
|||||||
} from '../../services/management/documentprinters.js';
|
} from '../../services/management/documentprinters.js';
|
||||||
|
|
||||||
// list of document printers
|
// list of document printers
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('documentPrinter', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listDocumentPrintersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listDocumentPrintersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -48,7 +48,7 @@ import {
|
|||||||
} from '../../services/management/documentsizes.js';
|
} from '../../services/management/documentsizes.js';
|
||||||
|
|
||||||
// list of document sizes
|
// list of document sizes
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('documentSize', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listDocumentSizesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listDocumentSizesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -50,7 +50,7 @@ import {
|
|||||||
} from '../../services/management/documenttemplates.js';
|
} from '../../services/management/documenttemplates.js';
|
||||||
|
|
||||||
// list of document templates
|
// list of document templates
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('documentTemplate', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listDocumentTemplatesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listDocumentTemplatesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import {
|
|||||||
} from '../../services/management/filaments.js';
|
} from '../../services/management/filaments.js';
|
||||||
|
|
||||||
// list of filaments
|
// list of filaments
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('filament', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import {
|
|||||||
getFilamentSkuNeighborsRouteHandler,
|
getFilamentSkuNeighborsRouteHandler,
|
||||||
} from '../../services/management/filamentskus.js';
|
} from '../../services/management/filamentskus.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('filamentSku', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listFilamentSkusRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listFilamentSkusRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -36,7 +36,7 @@ import {
|
|||||||
} from '../../services/management/files.js';
|
} from '../../services/management/files.js';
|
||||||
|
|
||||||
// list of files
|
// list of files
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('file', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listFilesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listFilesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import {
|
|||||||
} from '../../services/management/hosts.js';
|
} from '../../services/management/hosts.js';
|
||||||
|
|
||||||
// list of hosts
|
// list of hosts
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('host', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listHostsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listHostsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
} from '../../services/management/materials.js';
|
} from '../../services/management/materials.js';
|
||||||
|
|
||||||
// list of materials
|
// list of materials
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('material', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import {
|
|||||||
} from '../../services/management/notetypes.js';
|
} from '../../services/management/notetypes.js';
|
||||||
|
|
||||||
// list of note types
|
// list of note types
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('noteType', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listNoteTypesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listNoteTypesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import {
|
|||||||
} from '../../services/management/parts.js';
|
} from '../../services/management/parts.js';
|
||||||
|
|
||||||
// list of parts
|
// list of parts
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('part', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listPartsRouteHandler(
|
listPartsRouteHandler(
|
||||||
|
|||||||
@ -44,7 +44,7 @@ import {
|
|||||||
getPartSkuNeighborsRouteHandler
|
getPartSkuNeighborsRouteHandler
|
||||||
} from '../../services/management/partskus.js';
|
} from '../../services/management/partskus.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('partSku', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listPartSkusRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listPartSkusRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
getPermissionSettingsNeighborsRouteHandler,
|
getPermissionSettingsNeighborsRouteHandler,
|
||||||
} from '../../services/management/permissionsetting.js';
|
} from '../../services/management/permissionsetting.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('permissionSetting', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listPermissionSettingsRouteHandler(
|
listPermissionSettingsRouteHandler(
|
||||||
|
|||||||
@ -19,11 +19,18 @@ import {
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const listAllowedFilters = ['_id', 'name', 'createdAt', 'updatedAt', '_reference'];
|
const listAllowedFilters = [
|
||||||
|
'_id',
|
||||||
|
'name',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'_reference',
|
||||||
|
'marketplaces.marketplace',
|
||||||
|
];
|
||||||
const listAllowedSorters = ['name', 'createdAt', 'updatedAt', '_id'];
|
const listAllowedSorters = ['name', 'createdAt', 'updatedAt', '_id'];
|
||||||
const propertiesAllowedFilters = ['name'];
|
const propertiesAllowedFilters = ['name', 'marketplaces.marketplace'];
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('productCategory', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listProductCategoriesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listProductCategoriesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -44,7 +44,7 @@ import {
|
|||||||
} from '../../services/management/products.js';
|
} from '../../services/management/products.js';
|
||||||
|
|
||||||
// list of products
|
// list of products
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('product', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listProductsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listProductsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -44,7 +44,7 @@ import {
|
|||||||
getProductSkuNeighborsRouteHandler
|
getProductSkuNeighborsRouteHandler
|
||||||
} from '../../services/management/productskus.js';
|
} from '../../services/management/productskus.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('productSku', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listProductSkusRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listProductSkusRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -11,6 +11,8 @@ const listAllowedFilters = [
|
|||||||
'rateType',
|
'rateType',
|
||||||
'active',
|
'active',
|
||||||
'country',
|
'country',
|
||||||
|
'jurisdiction',
|
||||||
|
'marketplaces.marketplace',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'_reference',
|
'_reference',
|
||||||
@ -21,11 +23,12 @@ const listAllowedSorters = [
|
|||||||
'rateType',
|
'rateType',
|
||||||
'active',
|
'active',
|
||||||
'country',
|
'country',
|
||||||
|
'jurisdiction',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'_id',
|
'_id',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
];
|
];
|
||||||
const propertiesAllowedFilters = ['rateType', 'country', 'active'];
|
const propertiesAllowedFilters = ['rateType', 'country', 'jurisdiction', 'active'];
|
||||||
import {
|
import {
|
||||||
listTaxRatesRouteHandler,
|
listTaxRatesRouteHandler,
|
||||||
getTaxRateRouteHandler,
|
getTaxRateRouteHandler,
|
||||||
@ -42,7 +45,7 @@ import {
|
|||||||
} from '../../services/management/taxrates.js';
|
} from '../../services/management/taxrates.js';
|
||||||
|
|
||||||
// list of tax rates
|
// list of tax rates
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('taxRate', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listTaxRatesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listTaxRatesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
getUserGroupNeighborsRouteHandler,
|
getUserGroupNeighborsRouteHandler,
|
||||||
} from '../../services/management/usergroups.js';
|
} from '../../services/management/usergroups.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('userGroup', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listUserGroupsRouteHandler(
|
listUserGroupsRouteHandler(
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import {
|
|||||||
} from '../../services/management/users.js';
|
} from '../../services/management/users.js';
|
||||||
|
|
||||||
// list of document templates
|
// list of document templates
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('user', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listUsersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listUsersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import {
|
|||||||
} from '../../services/management/vendors.js';
|
} from '../../services/management/vendors.js';
|
||||||
|
|
||||||
// list of vendors
|
// list of vendors
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('vendor', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listVendorsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listVendorsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const listAllowedFilters = ['_id', 'name', 'createdAt', 'updatedAt', '_reference
|
|||||||
const listAllowedSorters = ['name', 'createdAt', 'updatedAt'];
|
const listAllowedSorters = ['name', 'createdAt', 'updatedAt'];
|
||||||
const propertiesAllowedFilters = ['name'];
|
const propertiesAllowedFilters = ['name'];
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('filamentProfile', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listFilamentProfilesRouteHandler(
|
listFilamentProfilesRouteHandler(
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import {
|
|||||||
import { convertPropertiesString, getFilter, getSort } from '../../utils.js';
|
import { convertPropertiesString, getFilter, getSort } from '../../utils.js';
|
||||||
|
|
||||||
// list of gcodeFiles
|
// list of gcodeFiles
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('gcodeFile', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listGCodeFilesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listGCodeFilesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -42,7 +42,7 @@ import {
|
|||||||
import { convertPropertiesString, getFilter, getSort } from '../../utils.js';
|
import { convertPropertiesString, getFilter, getSort } from '../../utils.js';
|
||||||
|
|
||||||
// list of jobs
|
// list of jobs
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('job', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listJobsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listJobsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const listAllowedSorters = [
|
|||||||
];
|
];
|
||||||
const propertiesAllowedFilters = ['name'];
|
const propertiesAllowedFilters = ['name'];
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('printerProfile', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listPrinterProfilesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listPrinterProfilesRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const listAllowedFilters = [
|
|||||||
const listAllowedSorters = ['name', 'state', 'connectedAt', 'createdAt', 'updatedAt', 'host'];
|
const listAllowedSorters = ['name', 'state', 'connectedAt', 'createdAt', 'updatedAt', 'host'];
|
||||||
const propertiesAllowedFilters = ['tags'];
|
const propertiesAllowedFilters = ['tags'];
|
||||||
// list of printers
|
// list of printers
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('printer', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listPrintersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listPrintersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -40,7 +40,7 @@ import {
|
|||||||
import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||||
|
|
||||||
// list of sub jobs
|
// list of sub jobs
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('subJob', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listSubJobsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listSubJobsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import {
|
|||||||
} from '../../services/sales/clients.js';
|
} from '../../services/sales/clients.js';
|
||||||
|
|
||||||
// list of clients
|
// list of clients
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('client', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listClientsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listClientsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
141
src/routes/sales/fulfillmentpolicies.js
Normal file
141
src/routes/sales/fulfillmentpolicies.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import { checkPermissions } from '../../database/permissions.js';
|
||||||
|
import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||||
|
import {
|
||||||
|
listFulfillmentPoliciesRouteHandler,
|
||||||
|
getFulfillmentPolicyRouteHandler,
|
||||||
|
editFulfillmentPolicyRouteHandler,
|
||||||
|
newFulfillmentPolicyRouteHandler,
|
||||||
|
deleteFulfillmentPolicyRouteHandler,
|
||||||
|
listFulfillmentPoliciesByPropertiesRouteHandler,
|
||||||
|
getFulfillmentPolicyStatsRouteHandler,
|
||||||
|
getFulfillmentPolicyHistoryRouteHandler,
|
||||||
|
searchFulfillmentPoliciesRouteHandler,
|
||||||
|
getFulfillmentPolicyPropertyValuesRouteHandler,
|
||||||
|
getFulfillmentPolicyNeighborsRouteHandler,
|
||||||
|
} from '../../services/sales/fulfillmentpolicies.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const listAllowedFilters = [
|
||||||
|
'name',
|
||||||
|
'handlingTime',
|
||||||
|
'localPickup',
|
||||||
|
'globalShipping',
|
||||||
|
'freightShipping',
|
||||||
|
'pickupDropOff',
|
||||||
|
'courierServices',
|
||||||
|
'marketplaces.marketplace',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'_reference',
|
||||||
|
];
|
||||||
|
const listAllowedSorters = ['name', 'handlingTime', 'createdAt', '_id', 'updatedAt'];
|
||||||
|
const propertiesAllowedFilters = [
|
||||||
|
'name',
|
||||||
|
'handlingTime',
|
||||||
|
'localPickup',
|
||||||
|
'courierServices',
|
||||||
|
'marketplaces.marketplace',
|
||||||
|
];
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, checkPermissions('fulfillmentPolicy', 'list'), async (req, res) => {
|
||||||
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
|
listFulfillmentPoliciesRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/properties',
|
||||||
|
checkPermissions('fulfillmentPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
const properties = convertPropertiesString(req.query.properties);
|
||||||
|
const filter = await getFilter(req.query, propertiesAllowedFilters, false);
|
||||||
|
let masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
|
}
|
||||||
|
listFulfillmentPoliciesByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/values',
|
||||||
|
checkPermissions('fulfillmentPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
getFulfillmentPolicyPropertyValuesRouteHandler(req, res, req.query.property);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/search',
|
||||||
|
checkPermissions('fulfillmentPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
searchFulfillmentPoliciesRouteHandler(req, res, req.query.search);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('fulfillmentPolicy', 'new'),
|
||||||
|
async (req, res) => {
|
||||||
|
newFulfillmentPolicyRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get('/stats', isAuthenticated, async (req, res) => {
|
||||||
|
getFulfillmentPolicyStatsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/history', isAuthenticated, async (req, res) => {
|
||||||
|
getFulfillmentPolicyHistoryRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/neighbors', isAuthenticated, async (req, res) => {
|
||||||
|
const { property, search, sortProperty, sortOrder, id } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
|
getFulfillmentPolicyNeighborsRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
getFulfillmentPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/:id',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('fulfillmentPolicy', 'edit'),
|
||||||
|
async (req, res) => {
|
||||||
|
editFulfillmentPolicyRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
deleteFulfillmentPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -16,6 +16,9 @@ const listAllowedFilters = [
|
|||||||
'marketplace',
|
'marketplace',
|
||||||
'marketplace._id',
|
'marketplace._id',
|
||||||
'courierServices',
|
'courierServices',
|
||||||
|
'fulfillmentPolicy',
|
||||||
|
'paymentPolicy',
|
||||||
|
'returnPolicy',
|
||||||
'state',
|
'state',
|
||||||
'state.type',
|
'state.type',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@ -40,6 +43,9 @@ const propertiesAllowedFilters = [
|
|||||||
'stockQuantity',
|
'stockQuantity',
|
||||||
'marketplace',
|
'marketplace',
|
||||||
'courierServices',
|
'courierServices',
|
||||||
|
'fulfillmentPolicy',
|
||||||
|
'paymentPolicy',
|
||||||
|
'returnPolicy',
|
||||||
'state',
|
'state',
|
||||||
'state.type',
|
'state.type',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@ -62,7 +68,7 @@ import {
|
|||||||
getListingNeighborsRouteHandler
|
getListingNeighborsRouteHandler
|
||||||
} from '../../services/sales/listings.js';
|
} from '../../services/sales/listings.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('listing', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listListingsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listListingsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -48,7 +48,7 @@ import {
|
|||||||
getListingVarientNeighborsRouteHandler
|
getListingVarientNeighborsRouteHandler
|
||||||
} from '../../services/sales/listingvarients.js';
|
} from '../../services/sales/listingvarients.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('listingVarient', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listListingVarientsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listListingVarientsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
@ -50,6 +50,10 @@ import {
|
|||||||
syncMarketplaceRouteHandler,
|
syncMarketplaceRouteHandler,
|
||||||
syncMarketplaceItemsRouteHandler,
|
syncMarketplaceItemsRouteHandler,
|
||||||
syncMarketplaceOrdersRouteHandler,
|
syncMarketplaceOrdersRouteHandler,
|
||||||
|
syncMarketplaceFulfillmentPoliciesRouteHandler,
|
||||||
|
syncMarketplacePaymentPoliciesRouteHandler,
|
||||||
|
syncMarketplaceReturnPoliciesRouteHandler,
|
||||||
|
syncMarketplaceTaxRatesRouteHandler,
|
||||||
marketplaceWebhookRouteHandler,
|
marketplaceWebhookRouteHandler,
|
||||||
marketplaceWebhookChallengeRouteHandler,
|
marketplaceWebhookChallengeRouteHandler,
|
||||||
subscribeMarketplaceWebhooksRouteHandler,
|
subscribeMarketplaceWebhooksRouteHandler,
|
||||||
@ -59,7 +63,7 @@ import {
|
|||||||
getMarketplaceNeighborsRouteHandler,
|
getMarketplaceNeighborsRouteHandler,
|
||||||
} from '../../services/sales/marketplaces.js';
|
} from '../../services/sales/marketplaces.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('marketplace', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listMarketplacesRouteHandler(
|
listMarketplacesRouteHandler(
|
||||||
@ -164,6 +168,42 @@ router.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:id/sync/fulfillmentPolicies',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('marketplace', 'sync'),
|
||||||
|
async (req, res) => {
|
||||||
|
syncMarketplaceFulfillmentPoliciesRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:id/sync/paymentPolicies',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('marketplace', 'sync'),
|
||||||
|
async (req, res) => {
|
||||||
|
syncMarketplacePaymentPoliciesRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:id/sync/returnPolicies',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('marketplace', 'sync'),
|
||||||
|
async (req, res) => {
|
||||||
|
syncMarketplaceReturnPoliciesRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:id/sync/taxRates',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('marketplace', 'sync'),
|
||||||
|
async (req, res) => {
|
||||||
|
syncMarketplaceTaxRatesRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/:id/sync/items',
|
'/:id/sync/items',
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
|||||||
136
src/routes/sales/returnpolicies.js
Normal file
136
src/routes/sales/returnpolicies.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import { checkPermissions } from '../../database/permissions.js';
|
||||||
|
import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||||
|
import {
|
||||||
|
listReturnPoliciesRouteHandler,
|
||||||
|
getReturnPolicyRouteHandler,
|
||||||
|
editReturnPolicyRouteHandler,
|
||||||
|
newReturnPolicyRouteHandler,
|
||||||
|
deleteReturnPolicyRouteHandler,
|
||||||
|
listReturnPoliciesByPropertiesRouteHandler,
|
||||||
|
getReturnPolicyStatsRouteHandler,
|
||||||
|
getReturnPolicyHistoryRouteHandler,
|
||||||
|
searchReturnPoliciesRouteHandler,
|
||||||
|
getReturnPolicyPropertyValuesRouteHandler,
|
||||||
|
getReturnPolicyNeighborsRouteHandler,
|
||||||
|
} from '../../services/sales/returnpolicies.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const listAllowedFilters = [
|
||||||
|
'name',
|
||||||
|
'returnsAccepted',
|
||||||
|
'returnPeriodDays',
|
||||||
|
'returnShippingCostPayer',
|
||||||
|
'refundMethod',
|
||||||
|
'marketplaces.marketplace',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'_reference',
|
||||||
|
];
|
||||||
|
const listAllowedSorters = [
|
||||||
|
'name',
|
||||||
|
'returnsAccepted',
|
||||||
|
'returnPeriodDays',
|
||||||
|
'createdAt',
|
||||||
|
'_id',
|
||||||
|
'updatedAt',
|
||||||
|
];
|
||||||
|
const propertiesAllowedFilters = [
|
||||||
|
'name',
|
||||||
|
'returnsAccepted',
|
||||||
|
'returnShippingCostPayer',
|
||||||
|
'refundMethod',
|
||||||
|
'marketplaces.marketplace',
|
||||||
|
];
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, checkPermissions('returnPolicy', 'list'), async (req, res) => {
|
||||||
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
|
listReturnPoliciesRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/properties',
|
||||||
|
checkPermissions('returnPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
const properties = convertPropertiesString(req.query.properties);
|
||||||
|
const filter = await getFilter(req.query, propertiesAllowedFilters, false);
|
||||||
|
let masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
|
}
|
||||||
|
listReturnPoliciesByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/values',
|
||||||
|
checkPermissions('returnPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
getReturnPolicyPropertyValuesRouteHandler(req, res, req.query.property);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/search',
|
||||||
|
checkPermissions('returnPolicy', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
searchReturnPoliciesRouteHandler(req, res, req.query.search);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, checkPermissions('returnPolicy', 'new'), async (req, res) => {
|
||||||
|
newReturnPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/stats', isAuthenticated, async (req, res) => {
|
||||||
|
getReturnPolicyStatsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/history', isAuthenticated, async (req, res) => {
|
||||||
|
getReturnPolicyHistoryRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/neighbors', isAuthenticated, async (req, res) => {
|
||||||
|
const { property, search, sortProperty, sortOrder, id } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
|
getReturnPolicyNeighborsRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
getReturnPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', isAuthenticated, checkPermissions('returnPolicy', 'edit'), async (req, res) => {
|
||||||
|
editReturnPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
deleteReturnPolicyRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -35,7 +35,7 @@ import {
|
|||||||
} from '../../services/sales/salesorders.js';
|
} from '../../services/sales/salesorders.js';
|
||||||
|
|
||||||
// list of sales orders
|
// list of sales orders
|
||||||
router.get('/', isAuthenticated, async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('salesOrder', 'list'), async (req, res) => {
|
||||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
listSalesOrdersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
listSalesOrdersRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder);
|
||||||
|
|||||||
232
src/services/finance/paymentpolicies.js
Normal file
232
src/services/finance/paymentpolicies.js
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import config from '../../config.js';
|
||||||
|
import { paymentPolicyModel } from '../../database/schemas/finance/paymentpolicy.schema.js';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import {
|
||||||
|
deleteObject,
|
||||||
|
listObjects,
|
||||||
|
getObject,
|
||||||
|
editObject,
|
||||||
|
newObject,
|
||||||
|
listObjectsByProperties,
|
||||||
|
getModelStats,
|
||||||
|
getModelHistory,
|
||||||
|
searchObjects,
|
||||||
|
getPropertyValues,
|
||||||
|
getObjectNeighbors,
|
||||||
|
} from '../../database/database.js';
|
||||||
|
import { syncPaymentPolicy } from '../../integrations/marketplace.js';
|
||||||
|
import { startPolicyMarketplaceSync } from '../misc/syncPolicyMarketplaces.js';
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('PaymentPolicies');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export const PAYMENT_POLICY_POPULATE = ['marketplaces.marketplace'];
|
||||||
|
|
||||||
|
const policyFields = (body = {}) => ({
|
||||||
|
name: body.name,
|
||||||
|
description: body.description,
|
||||||
|
immediatePay: body.immediatePay,
|
||||||
|
paymentInstructions: body.paymentInstructions,
|
||||||
|
marketplaces: body.marketplaces,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listPaymentPoliciesRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page = 1,
|
||||||
|
limit = 25,
|
||||||
|
property = '',
|
||||||
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend'
|
||||||
|
) => {
|
||||||
|
const result = await listObjects({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
populate: PAYMENT_POLICY_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing payment policies.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of payment policies (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listPaymentPoliciesByPropertiesRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
properties = '',
|
||||||
|
filter = {},
|
||||||
|
masterFilter = {}
|
||||||
|
) => {
|
||||||
|
const result = await listObjectsByProperties({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
properties,
|
||||||
|
filter,
|
||||||
|
masterFilter,
|
||||||
|
populate: PAYMENT_POLICY_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing payment policies.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of payment policies. Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPaymentPolicyPropertyValuesRouteHandler = async (req, res, property) => {
|
||||||
|
const result = await getPropertyValues({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
property,
|
||||||
|
});
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchPaymentPoliciesRouteHandler = async (req, res, search) => {
|
||||||
|
const result = await searchObjects({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
search,
|
||||||
|
populate: PAYMENT_POLICY_POPULATE,
|
||||||
|
});
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPaymentPolicyRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
const result = await getObject({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
id,
|
||||||
|
populate: PAYMENT_POLICY_POPULATE,
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
logger.warn('Payment policy not found with supplied id.');
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.debug(`Retrieved payment policy with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editPaymentPolicyRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
const result = await editObject({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
id,
|
||||||
|
updateData: { updatedAt: new Date(), ...policyFields(req.body) },
|
||||||
|
user: req.user,
|
||||||
|
populate: PAYMENT_POLICY_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('Error editing payment policy:', result.error);
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Edited payment policy with ID: ${id}`);
|
||||||
|
const synced = await startPolicyMarketplaceSync({
|
||||||
|
policy: result,
|
||||||
|
user: req.user,
|
||||||
|
syncFn: syncPaymentPolicy,
|
||||||
|
logger,
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
populate: PAYMENT_POLICY_POPULATE,
|
||||||
|
});
|
||||||
|
res.send(synced || result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newPaymentPolicyRouteHandler = async (req, res) => {
|
||||||
|
const result = await newObject({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
newData: { updatedAt: new Date(), ...policyFields(req.body) },
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No payment policy created:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`New payment policy with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deletePaymentPolicyRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
const result = await deleteObject({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
id,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No payment policy deleted:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Deleted payment policy with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPaymentPolicyStatsRouteHandler = async (req, res) => {
|
||||||
|
const result = await getModelStats({ model: paymentPolicyModel });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching payment policy stats:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPaymentPolicyHistoryRouteHandler = async (req, res) => {
|
||||||
|
const from = req.query.from;
|
||||||
|
const to = req.query.to;
|
||||||
|
const result = await getModelHistory({ model: paymentPolicyModel, from, to });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching payment policy history:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPaymentPolicyNeighborsRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
property = '',
|
||||||
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend',
|
||||||
|
id
|
||||||
|
) => {
|
||||||
|
if (!id) {
|
||||||
|
return res.status(400).send({ error: 'Missing id parameter', code: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getObjectNeighbors({
|
||||||
|
model: paymentPolicyModel,
|
||||||
|
id,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching paymentPolicy neighbors.');
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user