Enhanced courier service and listing schemas by adding cost-related fields and integrating courier services into listings. Updated route handlers and service logic to accommodate new fields, improving data management and retrieval capabilities.
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit is contained in:
parent
561520be9a
commit
e192c04000
@ -11,6 +11,13 @@ const courierServiceSchema = new mongoose.Schema(
|
|||||||
tracked: { required: true, type: Boolean },
|
tracked: { required: true, type: Boolean },
|
||||||
deliveryTime: { required: true, type: Number },
|
deliveryTime: { required: true, type: Number },
|
||||||
website: { required: false, type: String },
|
website: { required: false, type: String },
|
||||||
|
cost: { required: true, type: Number, default: 0 },
|
||||||
|
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
|
costWithTax: { required: false, type: Number },
|
||||||
|
additionalCost: { required: false, type: Number },
|
||||||
|
additionalCostWithTax: { required: false, type: Number },
|
||||||
|
shippingCurrency: { required: true, type: String, default: 'GBP' },
|
||||||
|
international: { required: true, type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const listingSchema = new Schema(
|
|||||||
price: { type: Number, required: false },
|
price: { type: Number, required: false },
|
||||||
currency: { type: String, required: false },
|
currency: { type: String, required: false },
|
||||||
lastSyncedAt: { type: Date, required: false },
|
lastSyncedAt: { type: Date, required: false },
|
||||||
|
courierServices: [{ type: Schema.Types.ObjectId, ref: 'courierService', required: true }],
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
121
src/integrations/marketplaces/ebay/categories.js
Normal file
121
src/integrations/marketplaces/ebay/categories.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { productModel } from '../../../database/schemas/management/product.schema.js';
|
||||||
|
import { productCategoryModel } from '../../../database/schemas/management/productcategory.schema.js';
|
||||||
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
|
||||||
|
const categoryTreeIds = new Map();
|
||||||
|
const categoryMatches = new Map();
|
||||||
|
|
||||||
|
function isPopulated(value) {
|
||||||
|
return value && typeof value === 'object' && !(value instanceof mongoose.Types.ObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveProduct(listing, varients) {
|
||||||
|
const productRef = listing?.product || varients?.find((varient) => varient?.product)?.product;
|
||||||
|
if (!productRef) return null;
|
||||||
|
|
||||||
|
if (isPopulated(productRef) && productRef.productCategory) {
|
||||||
|
return productRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productId = productRef._id || productRef;
|
||||||
|
return productModel.findById(productId).populate('productCategory').lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveProductCategory(listing, varients) {
|
||||||
|
const product = await resolveProduct(listing, varients);
|
||||||
|
const categoryRef = product?.productCategory;
|
||||||
|
if (!categoryRef) return null;
|
||||||
|
|
||||||
|
if (isPopulated(categoryRef) && categoryRef.name) {
|
||||||
|
return categoryRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryId = categoryRef._id || categoryRef;
|
||||||
|
return productCategoryModel.findById(categoryId).lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDefaultCategoryTreeId(marketplace) {
|
||||||
|
const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
|
||||||
|
const cacheKey = `${marketplace.config?.sandbox ? 'sandbox' : 'production'}:${marketplaceId}`;
|
||||||
|
if (categoryTreeIds.has(cacheKey)) {
|
||||||
|
return categoryTreeIds.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/commerce/taxonomy/v1/get_default_category_tree_id',
|
||||||
|
params: { marketplace_id: marketplaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.categoryTreeId) {
|
||||||
|
throw new Error(`eBay did not return a category tree for marketplace "${marketplaceId}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryTreeIds.set(cacheKey, result.categoryTreeId);
|
||||||
|
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
|
||||||
|
* marketplace taxonomy. eBay owns its category tree, so categories are matched
|
||||||
|
* rather than created.
|
||||||
|
*/
|
||||||
|
export async function syncProductCategory(marketplace, listing, varients = []) {
|
||||||
|
if (listing?.categoryId) {
|
||||||
|
return { categoryId: String(listing.categoryId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const productCategory = await resolveProductCategory(listing, varients);
|
||||||
|
if (!productCategory?.name) {
|
||||||
|
logger.debug(
|
||||||
|
`No product category found for listing "${listing?._reference}"; skipping eBay category sync`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryTreeId = await fetchDefaultCategoryTreeId(marketplace);
|
||||||
|
const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
|
||||||
|
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(
|
||||||
|
`No eBay category found for product category "${productCategory.name}" on ${marketplaceId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = {
|
||||||
|
categoryId: String(suggestion.category.categoryId),
|
||||||
|
categoryName: suggestion.category.categoryName,
|
||||||
|
};
|
||||||
|
categoryMatches.set(cacheKey, category);
|
||||||
|
logger.info(
|
||||||
|
`Matched product category "${productCategory.name}" to eBay category "${category.categoryName}" (${category.categoryId})`
|
||||||
|
);
|
||||||
|
return category;
|
||||||
|
}
|
||||||
215
src/integrations/marketplaces/ebay/fulfillmentPolicies.js
Normal file
215
src/integrations/marketplaces/ebay/fulfillmentPolicies.js
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { courierServiceModel } from '../../../database/schemas/management/courierservice.schema.js';
|
||||||
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
|
||||||
|
const SELLING_POLICY_PROGRAM = 'SELLING_POLICY_MANAGEMENT';
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return (
|
||||||
|
service &&
|
||||||
|
typeof service === 'object' &&
|
||||||
|
!(service instanceof mongoose.Types.ObjectId) &&
|
||||||
|
service.name != null &&
|
||||||
|
service.courier &&
|
||||||
|
typeof service.courier === 'object' &&
|
||||||
|
!(service.courier instanceof mongoose.Types.ObjectId) &&
|
||||||
|
service.courier._reference
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCourierServices(listing) {
|
||||||
|
const serviceRefs = listing?.courierServices || [];
|
||||||
|
if (!serviceRefs.length) return [];
|
||||||
|
|
||||||
|
const populatedServices = serviceRefs.filter(isPopulatedCourierService);
|
||||||
|
if (populatedServices.length === serviceRefs.length) {
|
||||||
|
return populatedServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceIds = serviceRefs.map((service) => service?._id || service).filter(Boolean);
|
||||||
|
const services = await courierServiceModel
|
||||||
|
.find({ _id: { $in: serviceIds } })
|
||||||
|
.populate('courier')
|
||||||
|
.lean();
|
||||||
|
const servicesById = new Map(services.map((service) => [String(service._id), service]));
|
||||||
|
|
||||||
|
return serviceIds.map((id) => servicesById.get(String(id))).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCourierServices(services, listing) {
|
||||||
|
const activeServices = services.filter((service) => service.active !== false);
|
||||||
|
if (!activeServices.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Listing "${listing._reference}" must have at least one active courier service before syncing with eBay.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const service of activeServices) {
|
||||||
|
if (!service._reference) {
|
||||||
|
throw new Error(
|
||||||
|
`Courier service "${service.name}" requires a reference containing its eBay shipping service code.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!service.courier?._reference) {
|
||||||
|
throw new Error(
|
||||||
|
`The courier for service "${service.name}" requires a reference containing its eBay shipping carrier code.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const domesticCount = activeServices.filter((service) => !service.international).length;
|
||||||
|
const internationalCount = activeServices.length - domesticCount;
|
||||||
|
if (domesticCount > 4 || internationalCount > 5) {
|
||||||
|
throw new Error(
|
||||||
|
'eBay fulfillment policies support at most four domestic and five international courier services.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildShippingService(service, index, defaultCurrency) {
|
||||||
|
const cost = Number(service.costWithTax ?? service.cost ?? 0);
|
||||||
|
const additionalCost = Number(service.additionalCostWithTax ?? service.additionalCost ?? 0);
|
||||||
|
const shippingService = {
|
||||||
|
sortOrder: index + 1,
|
||||||
|
shippingCarrierCode: service.courier._reference,
|
||||||
|
shippingServiceCode: service._reference,
|
||||||
|
buyerResponsibleForShipping: false,
|
||||||
|
freeShipping: cost === 0 && additionalCost === 0,
|
||||||
|
shippingCost: {
|
||||||
|
value: String(cost),
|
||||||
|
currency: service.shippingCurrency || defaultCurrency,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (service.additionalCostWithTax != null || service.additionalCost != null) {
|
||||||
|
shippingService.additionalShippingCost = {
|
||||||
|
value: String(additionalCost),
|
||||||
|
currency: service.shippingCurrency || defaultCurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return shippingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildShippingOption(optionType, services, defaultCurrency) {
|
||||||
|
if (!services.length) return null;
|
||||||
|
return {
|
||||||
|
optionType,
|
||||||
|
costType: 'FLAT_RATE',
|
||||||
|
shippingServices: services.map((service, index) =>
|
||||||
|
buildShippingService(service, index, defaultCurrency)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFulfillmentPolicy(listing, marketplace, services) {
|
||||||
|
const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
|
||||||
|
const defaultCurrency = marketplace.config?.currency || listing.currency || 'GBP';
|
||||||
|
const domesticServices = services.filter((service) => !service.international);
|
||||||
|
const internationalServices = services.filter((service) => service.international);
|
||||||
|
const shippingOptions = [
|
||||||
|
buildShippingOption('DOMESTIC', domesticServices, defaultCurrency),
|
||||||
|
buildShippingOption('INTERNATIONAL', internationalServices, defaultCurrency),
|
||||||
|
].filter(Boolean);
|
||||||
|
const deliveryTime = Math.max(0, ...services.map((service) => Number(service.deliveryTime ?? 1)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `FarmControl ${listing._reference}`.slice(0, 64),
|
||||||
|
description: `Managed by FarmControl for listing ${listing._reference}`.slice(0, 250),
|
||||||
|
marketplaceId,
|
||||||
|
categoryTypes: [{ name: 'ALL_EXCLUDING_MOTORS_VEHICLES' }],
|
||||||
|
handlingTime: { value: deliveryTime, unit: 'DAY' },
|
||||||
|
localPickup: false,
|
||||||
|
globalShipping: false,
|
||||||
|
freightShipping: false,
|
||||||
|
pickupDropOff: false,
|
||||||
|
shippingOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFulfillmentPolicyByName(marketplace, name) {
|
||||||
|
return makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/sell/account/v1/fulfillment_policy/get_by_policy_name',
|
||||||
|
params: {
|
||||||
|
marketplace_id: marketplace.config?.marketplaceId || 'EBAY_GB',
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncFulfillmentPolicy(marketplace, listing) {
|
||||||
|
await ensureSellingPolicyManagement(marketplace);
|
||||||
|
|
||||||
|
if (listing?.fulfillmentPolicyId) {
|
||||||
|
return { fulfillmentPolicyId: String(listing.fulfillmentPolicyId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const services = validateCourierServices(await resolveCourierServices(listing), listing);
|
||||||
|
const policy = buildFulfillmentPolicy(listing, marketplace, services);
|
||||||
|
const existingPolicy = await fetchFulfillmentPolicyByName(marketplace, policy.name);
|
||||||
|
|
||||||
|
if (existingPolicy?.fulfillmentPolicyId) {
|
||||||
|
await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'PUT',
|
||||||
|
path: `/sell/account/v1/fulfillment_policy/${encodeURIComponent(
|
||||||
|
existingPolicy.fulfillmentPolicyId
|
||||||
|
)}`,
|
||||||
|
body: policy,
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
`Updated eBay fulfillment policy "${policy.name}" (${existingPolicy.fulfillmentPolicyId})`
|
||||||
|
);
|
||||||
|
return { fulfillmentPolicyId: String(existingPolicy.fulfillmentPolicyId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/account/v1/fulfillment_policy',
|
||||||
|
body: policy,
|
||||||
|
});
|
||||||
|
if (!result?.fulfillmentPolicyId) {
|
||||||
|
throw new Error(`eBay did not return an ID for fulfillment policy "${policy.name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Created eBay fulfillment policy "${policy.name}" (${result.fulfillmentPolicyId})`);
|
||||||
|
return { fulfillmentPolicyId: String(result.fulfillmentPolicyId) };
|
||||||
|
}
|
||||||
@ -7,17 +7,19 @@ export {
|
|||||||
verifyWebhookSignature,
|
verifyWebhookSignature,
|
||||||
} from './auth.js';
|
} from './auth.js';
|
||||||
|
|
||||||
|
export { syncItems, mapProductToListing, createItem, updateItem, deleteItem } from './listings.js';
|
||||||
|
|
||||||
|
export { syncProductCategory } from './categories.js';
|
||||||
|
export { syncFulfillmentPolicy } from './fulfillmentPolicies.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
syncItems,
|
|
||||||
mapProductToListing,
|
|
||||||
createItem,
|
|
||||||
updateItem,
|
|
||||||
deleteItem,
|
|
||||||
publishOfferById,
|
publishOfferById,
|
||||||
withdrawOfferById,
|
withdrawOfferById,
|
||||||
publishOfferForSku,
|
publishOfferForSku,
|
||||||
withdrawOfferForSku,
|
withdrawOfferForSku,
|
||||||
} from './listings.js';
|
resolveStockLocationCountryCode,
|
||||||
|
resolveStockLocationEbayCountry,
|
||||||
|
} from './listingVarients.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
syncOrders,
|
syncOrders,
|
||||||
|
|||||||
396
src/integrations/marketplaces/ebay/listingVarients.js
Normal file
396
src/integrations/marketplaces/ebay/listingVarients.js
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { stockLocationModel } from '../../../database/schemas/inventory/stocklocation.schema.js';
|
||||||
|
import { FARMCONTROL_GB_SUBDIVISION_STATE, resolveEbayCountry } from './countryCodes.js';
|
||||||
|
import { syncProductCategory } from './categories.js';
|
||||||
|
import { syncFulfillmentPolicy } from './fulfillmentPolicies.js';
|
||||||
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
|
||||||
|
const WAREHOUSE_ADDRESS_DEFAULTS = {
|
||||||
|
GB: { city: 'London', stateOrProvince: 'England', postalCode: 'SW1A 1AA' },
|
||||||
|
US: { city: 'New York', stateOrProvince: 'NY', postalCode: '10001' },
|
||||||
|
AU: { city: 'Sydney', stateOrProvince: 'NSW', postalCode: '2000' },
|
||||||
|
CA: { city: 'Toronto', stateOrProvince: 'ON', postalCode: 'M5H 2N2' },
|
||||||
|
DE: { city: 'Berlin', stateOrProvince: 'Berlin', postalCode: '10115' },
|
||||||
|
FR: { city: 'Paris', stateOrProvince: 'Île-de-France', postalCode: '75001' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const LISTING_STATUS_MAP = {
|
||||||
|
ACTIVE: 'active',
|
||||||
|
OUT_OF_STOCK: 'inactive',
|
||||||
|
ENDED: 'inactive',
|
||||||
|
PUBLISHED: 'active',
|
||||||
|
UNPUBLISHED: 'draft',
|
||||||
|
};
|
||||||
|
|
||||||
|
function isPopulatedStockLocation(stockLocation) {
|
||||||
|
if (!stockLocation || typeof stockLocation !== 'object') return false;
|
||||||
|
if (stockLocation instanceof mongoose.Types.ObjectId) return false;
|
||||||
|
return stockLocation.name != null || stockLocation.address != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureListingStockLocation(listing) {
|
||||||
|
const stockLocationRef = listing?.stockLocation;
|
||||||
|
if (!stockLocationRef || isPopulatedStockLocation(stockLocationRef)) {
|
||||||
|
return listing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockLocationId = stockLocationRef._id || stockLocationRef;
|
||||||
|
const stockLocation = await stockLocationModel.findById(stockLocationId).lean();
|
||||||
|
if (!stockLocation) throw new Error('Listing stock location not found.');
|
||||||
|
return { ...listing, stockLocation };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveStockLocationCountryCode(listing) {
|
||||||
|
return resolveStockLocationEbayCountry(listing)?.ebayCountryCode ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveStockLocationEbayCountry(listing) {
|
||||||
|
const stockLocation = listing?.stockLocation;
|
||||||
|
if (!isPopulatedStockLocation(stockLocation)) return null;
|
||||||
|
try {
|
||||||
|
return resolveEbayCountry(stockLocation.address?.country);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMerchantLocationKey(listing) {
|
||||||
|
const stockLocationId = listing?.stockLocation?._id || listing?.stockLocation;
|
||||||
|
return stockLocationId ? `fc-${String(stockLocationId)}`.slice(0, 36) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEbayWarehouseAddress(addr, ebayCountryCode, farmControlCountryCode) {
|
||||||
|
const address = { country: ebayCountryCode };
|
||||||
|
|
||||||
|
if (addr?.addressLine1) address.addressLine1 = addr.addressLine1;
|
||||||
|
if (addr?.addressLine2) address.addressLine2 = addr.addressLine2;
|
||||||
|
if (addr?.city) address.city = addr.city;
|
||||||
|
if (addr?.state) address.stateOrProvince = addr.state;
|
||||||
|
if (addr?.postcode) address.postalCode = addr.postcode;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!address.stateOrProvince &&
|
||||||
|
farmControlCountryCode &&
|
||||||
|
FARMCONTROL_GB_SUBDIVISION_STATE[farmControlCountryCode]
|
||||||
|
) {
|
||||||
|
address.stateOrProvince = FARMCONTROL_GB_SUBDIVISION_STATE[farmControlCountryCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!address.postalCode && !(address.city && address.stateOrProvince)) {
|
||||||
|
const defaults = WAREHOUSE_ADDRESS_DEFAULTS[ebayCountryCode];
|
||||||
|
if (defaults) {
|
||||||
|
Object.assign(address, defaults);
|
||||||
|
if (farmControlCountryCode && FARMCONTROL_GB_SUBDIVISION_STATE[farmControlCountryCode]) {
|
||||||
|
address.stateOrProvince = FARMCONTROL_GB_SUBDIVISION_STATE[farmControlCountryCode];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
address.city = address.city || 'Unknown';
|
||||||
|
address.stateOrProvince = address.stateOrProvince || ebayCountryCode;
|
||||||
|
address.postalCode = address.postalCode || '00000';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!address.country) {
|
||||||
|
throw new Error('eBay warehouse address requires a country code.');
|
||||||
|
}
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWarehouseLocationBody(listing, ebayCountryCode, farmControlCountryCode) {
|
||||||
|
const stockLocation = listing.stockLocation;
|
||||||
|
return {
|
||||||
|
location: {
|
||||||
|
address: buildEbayWarehouseAddress(
|
||||||
|
stockLocation?.address || {},
|
||||||
|
ebayCountryCode,
|
||||||
|
farmControlCountryCode
|
||||||
|
),
|
||||||
|
},
|
||||||
|
locationTypes: ['WAREHOUSE'],
|
||||||
|
name: stockLocation?.name || `FarmControl ${ebayCountryCode}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureMerchantLocation(marketplace, listing) {
|
||||||
|
const listingWithStockLocation = await ensureListingStockLocation(listing);
|
||||||
|
const countryResolved = resolveEbayCountry(
|
||||||
|
listingWithStockLocation.stockLocation?.address?.country
|
||||||
|
);
|
||||||
|
if (!countryResolved) {
|
||||||
|
throw new Error(
|
||||||
|
'Listing stock location address must include a country before syncing or publishing on eBay.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const merchantLocationKey = resolveMerchantLocationKey(listingWithStockLocation);
|
||||||
|
if (!merchantLocationKey) {
|
||||||
|
throw new Error('Listing must have a stock location before syncing or publishing on eBay.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationBody = buildWarehouseLocationBody(
|
||||||
|
listingWithStockLocation,
|
||||||
|
countryResolved.ebayCountryCode,
|
||||||
|
countryResolved.farmControlCountryCode
|
||||||
|
);
|
||||||
|
listing.stockLocation = listingWithStockLocation.stockLocation;
|
||||||
|
const locationPath = `/sell/inventory/v1/location/${encodeURIComponent(merchantLocationKey)}`;
|
||||||
|
const existing = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: locationPath,
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: `${locationPath}/update_location_details`,
|
||||||
|
body: { location: locationBody.location },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: locationPath,
|
||||||
|
body: locationBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return merchantLocationKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMarketplaceOfferDefaults(
|
||||||
|
offer,
|
||||||
|
marketplace,
|
||||||
|
merchantLocationKey,
|
||||||
|
fulfillmentPolicyId
|
||||||
|
) {
|
||||||
|
offer.merchantLocationKey = merchantLocationKey;
|
||||||
|
const config = marketplace.config || {};
|
||||||
|
const listingPolicies = {};
|
||||||
|
if (fulfillmentPolicyId || config.fulfillmentPolicyId) {
|
||||||
|
listingPolicies.fulfillmentPolicyId = fulfillmentPolicyId || config.fulfillmentPolicyId;
|
||||||
|
}
|
||||||
|
if (config.paymentPolicyId) listingPolicies.paymentPolicyId = config.paymentPolicyId;
|
||||||
|
if (config.returnPolicyId) listingPolicies.returnPolicyId = config.returnPolicyId;
|
||||||
|
if (Object.keys(listingPolicies).length) offer.listingPolicies = listingPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapVarientToInventoryItem(varient, listing) {
|
||||||
|
const item = {
|
||||||
|
product: { title: listing.title || varient._reference || '' },
|
||||||
|
availability: {
|
||||||
|
shipToLocationAvailability: { quantity: varient.inventory ?? 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (listing.description) item.product.description = listing.description;
|
||||||
|
if (listing.imageUrls?.length) item.product.imageUrls = listing.imageUrls;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapVarientToOffer(varient, listing, marketplace, merchantLocationKey) {
|
||||||
|
const offer = {
|
||||||
|
sku: varient._reference,
|
||||||
|
marketplaceId: marketplace.config?.marketplaceId || 'EBAY_GB',
|
||||||
|
format: 'FIXED_PRICE',
|
||||||
|
};
|
||||||
|
applyMarketplaceOfferDefaults(
|
||||||
|
offer,
|
||||||
|
marketplace,
|
||||||
|
merchantLocationKey,
|
||||||
|
listing.fulfillmentPolicyId
|
||||||
|
);
|
||||||
|
|
||||||
|
const price = varient.price ?? listing.price;
|
||||||
|
if (price != null) {
|
||||||
|
offer.pricingSummary = {
|
||||||
|
price: {
|
||||||
|
value: String(price),
|
||||||
|
currency: varient.currency || listing.currency || 'GBP',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (listing.categoryId) offer.categoryId = String(listing.categoryId);
|
||||||
|
return offer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertInventoryItem(marketplace, varient, listing) {
|
||||||
|
const inventoryItem = mapVarientToInventoryItem(varient, listing);
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'PUT',
|
||||||
|
path: `/sell/inventory/v1/inventory_item/${encodeURIComponent(varient._reference)}`,
|
||||||
|
body: inventoryItem,
|
||||||
|
});
|
||||||
|
logger.debug('inventoryItem', inventoryItem);
|
||||||
|
logger.debug('result', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchOffers(marketplace, sku) {
|
||||||
|
try {
|
||||||
|
const data = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
path: '/sell/inventory/v1/offer',
|
||||||
|
params: { sku, limit: 200 },
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
});
|
||||||
|
return data?.offers || [];
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(`No offers found for SKU ${sku}: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertOrCreateOffer(marketplace, varient, listing) {
|
||||||
|
const merchantLocationKey = await ensureMerchantLocation(marketplace, listing);
|
||||||
|
const offers = await fetchOffers(marketplace, varient._reference);
|
||||||
|
const existingOffer = offers[0];
|
||||||
|
const price = varient.price ?? listing.price;
|
||||||
|
|
||||||
|
if (existingOffer?.offerId) {
|
||||||
|
const offerUpdate = { merchantLocationKey };
|
||||||
|
if (price != null) {
|
||||||
|
offerUpdate.pricingSummary = {
|
||||||
|
price: {
|
||||||
|
value: String(price),
|
||||||
|
currency: varient.currency || listing.currency || 'GBP',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (listing.categoryId) offerUpdate.categoryId = String(listing.categoryId);
|
||||||
|
if (listing.fulfillmentPolicyId) {
|
||||||
|
offerUpdate.listingPolicies = {
|
||||||
|
...(existingOffer.listingPolicies || {}),
|
||||||
|
fulfillmentPolicyId: String(listing.fulfillmentPolicyId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const body = { ...existingOffer, ...offerUpdate };
|
||||||
|
await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'PUT',
|
||||||
|
path: `/sell/inventory/v1/offer/${existingOffer.offerId}`,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
logger.debug('offerUpdate', body);
|
||||||
|
return existingOffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offerBody = mapVarientToOffer(varient, listing, marketplace, merchantLocationKey);
|
||||||
|
const result = await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/sell/inventory/v1/offer',
|
||||||
|
body: offerBody,
|
||||||
|
});
|
||||||
|
logger.debug('offerBody', offerBody);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishOfferById(marketplace, offerId) {
|
||||||
|
if (!offerId) throw new Error('offerId is required to publish an offer');
|
||||||
|
return makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: `/sell/inventory/v1/offer/${encodeURIComponent(offerId)}/publish`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withdrawOfferById(marketplace, offerId) {
|
||||||
|
if (!offerId) throw new Error('offerId is required to withdraw an offer');
|
||||||
|
return makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'POST',
|
||||||
|
path: `/sell/inventory/v1/offer/${encodeURIComponent(offerId)}/withdraw`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncOfferAndMaybePublish(marketplace, listing, varient) {
|
||||||
|
const offerResult = await upsertOrCreateOffer(marketplace, varient, listing);
|
||||||
|
if (offerResult?.offerId && listing.state?.type === 'active') {
|
||||||
|
try {
|
||||||
|
const publishResult = await publishOfferById(marketplace, offerResult.offerId);
|
||||||
|
if (publishResult?.listingId) {
|
||||||
|
return `https://www.ebay.com/itm/${publishResult.listingId}`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`Created offer but failed to publish for varient ${varient._reference}: ${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishOfferForSku(marketplace, sku, listing) {
|
||||||
|
if (!sku) throw new Error('SKU (_reference) is required to publish an offer');
|
||||||
|
if (!listing) {
|
||||||
|
throw new Error(
|
||||||
|
'Listing is required to publish an eBay offer (stock location address is used for item location).'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const merchantLocationKey = await ensureMerchantLocation(marketplace, listing);
|
||||||
|
const category = await syncProductCategory(marketplace, listing);
|
||||||
|
const fulfillmentPolicy = await syncFulfillmentPolicy(marketplace, listing);
|
||||||
|
if (!category?.categoryId) {
|
||||||
|
throw new Error(
|
||||||
|
`Listing "${listing._reference || sku}" must have a product with a product category before publishing on eBay.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingOffer = (await fetchOffers(marketplace, sku))[0];
|
||||||
|
if (!existingOffer?.offerId) {
|
||||||
|
throw new Error(
|
||||||
|
`No eBay offer exists for SKU "${sku}". Create or sync the listing so an offer exists before publishing.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await makeRequest({
|
||||||
|
marketplace,
|
||||||
|
method: 'PUT',
|
||||||
|
path: `/sell/inventory/v1/offer/${existingOffer.offerId}`,
|
||||||
|
body: {
|
||||||
|
...existingOffer,
|
||||||
|
merchantLocationKey,
|
||||||
|
categoryId: String(category.categoryId),
|
||||||
|
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) {
|
||||||
|
if (!sku) throw new Error('SKU (_reference) is required to withdraw an offer');
|
||||||
|
const existingOffer = (await fetchOffers(marketplace, sku))[0];
|
||||||
|
if (!existingOffer?.offerId) throw new Error(`No eBay offer exists for SKU "${sku}".`);
|
||||||
|
await withdrawOfferById(marketplace, existingOffer.offerId);
|
||||||
|
return { offerId: existingOffer.offerId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveOfferState(offers) {
|
||||||
|
let stateType = 'draft';
|
||||||
|
for (const offer of offers) {
|
||||||
|
if (offer?.status && LISTING_STATUS_MAP[offer.status]) {
|
||||||
|
const mapped = LISTING_STATUS_MAP[offer.status];
|
||||||
|
if (mapped === 'active') return 'active';
|
||||||
|
if (mapped !== 'draft') stateType = mapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildVarientEntry(item, offers) {
|
||||||
|
const offer = offers?.[0];
|
||||||
|
return {
|
||||||
|
_reference: item.sku,
|
||||||
|
price: offer?.pricingSummary?.price?.value
|
||||||
|
? parseFloat(offer.pricingSummary.price.value)
|
||||||
|
: undefined,
|
||||||
|
currency: offer?.pricingSummary?.price?.currency || undefined,
|
||||||
|
state: { type: resolveOfferState(offers || []) },
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,307 +1,18 @@
|
|||||||
import mongoose from 'mongoose';
|
import { syncProductCategory } from './categories.js';
|
||||||
import { stockLocationModel } from '../../../database/schemas/inventory/stocklocation.schema.js';
|
import { syncFulfillmentPolicy } from './fulfillmentPolicies.js';
|
||||||
import {
|
import {
|
||||||
FARMCONTROL_GB_SUBDIVISION_STATE,
|
buildVarientEntry,
|
||||||
resolveEbayCountry,
|
fetchOffers,
|
||||||
} from './countryCodes.js';
|
resolveOfferState,
|
||||||
|
syncOfferAndMaybePublish,
|
||||||
|
upsertInventoryItem,
|
||||||
|
} from './listingVarients.js';
|
||||||
import { makeRequest, logger } from './shared.js';
|
import { makeRequest, logger } from './shared.js';
|
||||||
|
|
||||||
const WAREHOUSE_ADDRESS_DEFAULTS = {
|
|
||||||
GB: { city: 'London', stateOrProvince: 'England', postalCode: 'SW1A 1AA' },
|
|
||||||
US: { city: 'New York', stateOrProvince: 'NY', postalCode: '10001' },
|
|
||||||
AU: { city: 'Sydney', stateOrProvince: 'NSW', postalCode: '2000' },
|
|
||||||
CA: { city: 'Toronto', stateOrProvince: 'ON', postalCode: 'M5H 2N2' },
|
|
||||||
DE: { city: 'Berlin', stateOrProvince: 'Berlin', postalCode: '10115' },
|
|
||||||
FR: { city: 'Paris', stateOrProvince: 'Île-de-France', postalCode: '75001' },
|
|
||||||
};
|
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPopulatedStockLocation(stockLocation) {
|
|
||||||
if (!stockLocation || typeof stockLocation !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (stockLocation instanceof mongoose.Types.ObjectId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return stockLocation.name != null || stockLocation.address != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureListingStockLocation(listing) {
|
|
||||||
const stockLocationRef = listing?.stockLocation;
|
|
||||||
if (!stockLocationRef) {
|
|
||||||
return listing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPopulatedStockLocation(stockLocationRef)) {
|
|
||||||
return listing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stockLocationId = stockLocationRef._id || stockLocationRef;
|
|
||||||
const stockLocation = await stockLocationModel.findById(stockLocationId).lean();
|
|
||||||
if (!stockLocation) {
|
|
||||||
throw new Error('Listing stock location not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...listing, stockLocation };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveStockLocationCountryCode(listing) {
|
|
||||||
const resolved = resolveStockLocationEbayCountry(listing);
|
|
||||||
return resolved?.ebayCountryCode ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveStockLocationEbayCountry(listing) {
|
|
||||||
const stockLocation = listing?.stockLocation;
|
|
||||||
if (!isPopulatedStockLocation(stockLocation)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return resolveEbayCountry(stockLocation.address?.country);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMerchantLocationKey(listing) {
|
|
||||||
const stockLocationId = listing?.stockLocation?._id || listing?.stockLocation;
|
|
||||||
if (!stockLocationId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `fc-${String(stockLocationId)}`.slice(0, 36);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildEbayWarehouseAddress(addr, ebayCountryCode, farmControlCountryCode) {
|
|
||||||
const address = {
|
|
||||||
country: ebayCountryCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (addr?.addressLine1) address.addressLine1 = addr.addressLine1;
|
|
||||||
if (addr?.addressLine2) address.addressLine2 = addr.addressLine2;
|
|
||||||
if (addr?.city) address.city = addr.city;
|
|
||||||
if (addr?.state) address.stateOrProvince = addr.state;
|
|
||||||
if (addr?.postcode) address.postalCode = addr.postcode;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!address.stateOrProvince &&
|
|
||||||
farmControlCountryCode &&
|
|
||||||
FARMCONTROL_GB_SUBDIVISION_STATE[farmControlCountryCode]
|
|
||||||
) {
|
|
||||||
address.stateOrProvince = FARMCONTROL_GB_SUBDIVISION_STATE[farmControlCountryCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasPostal = Boolean(address.postalCode);
|
|
||||||
const hasCityState = Boolean(address.city && address.stateOrProvince);
|
|
||||||
|
|
||||||
if (!hasPostal && !hasCityState) {
|
|
||||||
const defaults = WAREHOUSE_ADDRESS_DEFAULTS[ebayCountryCode];
|
|
||||||
if (defaults) {
|
|
||||||
Object.assign(address, defaults);
|
|
||||||
if (
|
|
||||||
farmControlCountryCode &&
|
|
||||||
FARMCONTROL_GB_SUBDIVISION_STATE[farmControlCountryCode]
|
|
||||||
) {
|
|
||||||
address.stateOrProvince = FARMCONTROL_GB_SUBDIVISION_STATE[farmControlCountryCode];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
address.city = address.city || 'Unknown';
|
|
||||||
address.stateOrProvince = address.stateOrProvince || ebayCountryCode;
|
|
||||||
address.postalCode = address.postalCode || '00000';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!address.country) {
|
|
||||||
throw new Error('eBay warehouse address requires a country code.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWarehouseLocationBody(listing, ebayCountryCode, farmControlCountryCode) {
|
|
||||||
const stockLocation = listing.stockLocation;
|
|
||||||
const addr = stockLocation?.address || {};
|
|
||||||
const address = buildEbayWarehouseAddress(addr, ebayCountryCode, farmControlCountryCode);
|
|
||||||
|
|
||||||
return {
|
|
||||||
location: { address },
|
|
||||||
locationTypes: ['WAREHOUSE'],
|
|
||||||
name: stockLocation?.name || `FarmControl ${ebayCountryCode}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureMerchantLocation(marketplace, listing) {
|
|
||||||
const listingWithStockLocation = await ensureListingStockLocation(listing);
|
|
||||||
|
|
||||||
const countryResolved = resolveEbayCountry(
|
|
||||||
listingWithStockLocation.stockLocation?.address?.country
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!countryResolved) {
|
|
||||||
throw new Error(
|
|
||||||
'Listing stock location address must include a country before syncing or publishing on eBay.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ebayCountryCode, farmControlCountryCode } = countryResolved;
|
|
||||||
|
|
||||||
const merchantLocationKey = resolveMerchantLocationKey(listingWithStockLocation);
|
|
||||||
if (!merchantLocationKey) {
|
|
||||||
throw new Error('Listing must have a stock location before syncing or publishing on eBay.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const locationBody = buildWarehouseLocationBody(
|
|
||||||
listingWithStockLocation,
|
|
||||||
ebayCountryCode,
|
|
||||||
farmControlCountryCode
|
|
||||||
);
|
|
||||||
listing.stockLocation = listingWithStockLocation.stockLocation;
|
|
||||||
const locationPath = `/sell/inventory/v1/location/${encodeURIComponent(merchantLocationKey)}`;
|
|
||||||
|
|
||||||
const existing = await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
path: locationPath,
|
|
||||||
acceptableStatuses: [404],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'POST',
|
|
||||||
path: `${locationPath}/update_location_details`,
|
|
||||||
body: { location: locationBody.location },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'POST',
|
|
||||||
path: locationPath,
|
|
||||||
body: locationBody,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return merchantLocationKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyMarketplaceOfferDefaults(offer, marketplace, merchantLocationKey) {
|
|
||||||
offer.merchantLocationKey = merchantLocationKey;
|
|
||||||
|
|
||||||
const config = marketplace.config || {};
|
|
||||||
const listingPolicies = {};
|
|
||||||
if (config.fulfillmentPolicyId) {
|
|
||||||
listingPolicies.fulfillmentPolicyId = config.fulfillmentPolicyId;
|
|
||||||
}
|
|
||||||
if (config.paymentPolicyId) {
|
|
||||||
listingPolicies.paymentPolicyId = config.paymentPolicyId;
|
|
||||||
}
|
|
||||||
if (config.returnPolicyId) {
|
|
||||||
listingPolicies.returnPolicyId = config.returnPolicyId;
|
|
||||||
}
|
|
||||||
if (Object.keys(listingPolicies).length > 0) {
|
|
||||||
offer.listingPolicies = listingPolicies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapVarientToInventoryItem(varient, listing) {
|
|
||||||
const item = {
|
|
||||||
product: {
|
|
||||||
title: listing.title || varient._reference || '',
|
|
||||||
},
|
|
||||||
availability: {
|
|
||||||
shipToLocationAvailability: {
|
|
||||||
quantity: varient.inventory ?? 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (listing.description) {
|
|
||||||
item.product.description = listing.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listing.imageUrls?.length) {
|
|
||||||
item.product.imageUrls = listing.imageUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapVarientToOffer(varient, listing, marketplace, merchantLocationKey) {
|
|
||||||
const offer = {
|
|
||||||
sku: varient._reference,
|
|
||||||
marketplaceId: marketplace.config?.marketplaceId || 'EBAY_GB',
|
|
||||||
format: 'FIXED_PRICE',
|
|
||||||
};
|
|
||||||
|
|
||||||
applyMarketplaceOfferDefaults(offer, marketplace, merchantLocationKey);
|
|
||||||
|
|
||||||
const price = varient.price ?? listing.price;
|
|
||||||
if (price != null) {
|
|
||||||
offer.pricingSummary = {
|
|
||||||
price: {
|
|
||||||
value: String(price),
|
|
||||||
currency: varient.currency || listing.currency || 'GBP',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listing.categoryId) {
|
|
||||||
offer.categoryId = listing.categoryId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return offer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upsertInventoryItem(marketplace, varient, listing) {
|
|
||||||
const inventoryItem = mapVarientToInventoryItem(varient, listing);
|
|
||||||
const result = await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'PUT',
|
|
||||||
path: `/sell/inventory/v1/inventory_item/${encodeURIComponent(varient._reference)}`,
|
|
||||||
body: inventoryItem,
|
|
||||||
});
|
|
||||||
logger.debug('inventoryItem', inventoryItem);
|
|
||||||
logger.debug('result', result);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upsertOrCreateOffer(marketplace, varient, listing) {
|
|
||||||
const merchantLocationKey = await ensureMerchantLocation(marketplace, listing);
|
|
||||||
const offers = await fetchOffers(marketplace, varient._reference);
|
|
||||||
const existingOffer = offers[0];
|
|
||||||
|
|
||||||
const price = varient.price ?? listing.price;
|
|
||||||
|
|
||||||
if (existingOffer?.offerId) {
|
|
||||||
const offerUpdate = { merchantLocationKey };
|
|
||||||
if (price != null) {
|
|
||||||
offerUpdate.pricingSummary = {
|
|
||||||
price: {
|
|
||||||
value: String(price),
|
|
||||||
currency: varient.currency || listing.currency || 'GBP',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'PUT',
|
|
||||||
path: `/sell/inventory/v1/offer/${existingOffer.offerId}`,
|
|
||||||
body: { ...existingOffer, ...offerUpdate },
|
|
||||||
});
|
|
||||||
logger.debug('offerUpdate', { ...existingOffer, ...offerUpdate });
|
|
||||||
return existingOffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offerBody = mapVarientToOffer(varient, listing, marketplace, merchantLocationKey);
|
|
||||||
const result = await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'POST',
|
|
||||||
path: '/sell/inventory/v1/offer',
|
|
||||||
body: offerBody,
|
|
||||||
});
|
|
||||||
logger.debug('offerBody', offerBody);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createOrReplaceGroup(marketplace, listing, varients) {
|
async function createOrReplaceGroup(marketplace, listing, varients) {
|
||||||
const groupKey = listing._reference;
|
const groupKey = listing._reference;
|
||||||
const variantSKUs = varients.map((v) => v._reference).filter(Boolean);
|
const variantSKUs = varients.map((v) => v._reference).filter(Boolean);
|
||||||
@ -340,26 +51,6 @@ async function deleteGroup(marketplace, groupKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncOfferAndMaybePublish(marketplace, listing, varient) {
|
|
||||||
const offerResult = await upsertOrCreateOffer(marketplace, varient, listing);
|
|
||||||
|
|
||||||
if (offerResult?.offerId && listing.state?.type === 'active') {
|
|
||||||
try {
|
|
||||||
const publishResult = await publishOfferById(marketplace, offerResult.offerId);
|
|
||||||
|
|
||||||
if (publishResult?.listingId) {
|
|
||||||
return `https://www.ebay.com/itm/${publishResult.listingId}`;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(
|
|
||||||
`Created offer but failed to publish for varient ${varient._reference}: ${err.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncSingleVarientListing(marketplace, listing, varient) {
|
async function syncSingleVarientListing(marketplace, listing, varient) {
|
||||||
await upsertInventoryItem(marketplace, varient, listing);
|
await upsertInventoryItem(marketplace, varient, listing);
|
||||||
|
|
||||||
@ -413,15 +104,23 @@ async function syncListing(marketplace, listing, varients, actionLabel) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const category = await syncProductCategory(marketplace, listing, validVarients);
|
||||||
|
const fulfillmentPolicy = await syncFulfillmentPolicy(marketplace, listing);
|
||||||
|
const listingWithContext = {
|
||||||
|
...listing,
|
||||||
|
...(category ? { categoryId: category.categoryId } : {}),
|
||||||
|
fulfillmentPolicyId: fulfillmentPolicy.fulfillmentPolicyId,
|
||||||
|
};
|
||||||
|
|
||||||
if (validVarients.length === 1) {
|
if (validVarients.length === 1) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Syncing standalone eBay inventory item "${validVarients[0]._reference}" for listing "${ref}"`
|
`Syncing standalone eBay inventory item "${validVarients[0]._reference}" for listing "${ref}"`
|
||||||
);
|
);
|
||||||
const url = await syncSingleVarientListing(marketplace, listing, validVarients[0]);
|
const url = await syncSingleVarientListing(marketplace, listingWithContext, validVarients[0]);
|
||||||
return { url };
|
return { url };
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = await syncGroupedListing(marketplace, listing, validVarients);
|
const url = await syncGroupedListing(marketplace, listingWithContext, validVarients);
|
||||||
return { url };
|
return { url };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,99 +168,6 @@ async function fetchAllInventoryItems(marketplace) {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOffers(marketplace, sku) {
|
|
||||||
try {
|
|
||||||
const data = await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
path: '/sell/inventory/v1/offer',
|
|
||||||
params: { sku, limit: 200 },
|
|
||||||
acceptableStatuses: [404],
|
|
||||||
});
|
|
||||||
return data?.offers || [];
|
|
||||||
} catch (err) {
|
|
||||||
logger.debug(`No offers found for SKU ${sku}: ${err.message}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* eBay Sell Inventory API — publish offer (creates live listing).
|
|
||||||
* @see https://developer.ebay.com/api-docs/sell/inventory/resources/offer/methods/publishOffer
|
|
||||||
*/
|
|
||||||
export async function publishOfferById(marketplace, offerId) {
|
|
||||||
if (!offerId) {
|
|
||||||
throw new Error('offerId is required to publish an offer');
|
|
||||||
}
|
|
||||||
return makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'POST',
|
|
||||||
path: `/sell/inventory/v1/offer/${encodeURIComponent(offerId)}/publish`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* eBay Sell Inventory API — withdraw offer (ends live listing; offer remains for re-publish).
|
|
||||||
* @see https://developer.ebay.com/api-docs/sell/inventory/resources/offer/methods/withdrawOffer
|
|
||||||
*/
|
|
||||||
export async function withdrawOfferById(marketplace, offerId) {
|
|
||||||
if (!offerId) {
|
|
||||||
throw new Error('offerId is required to withdraw an offer');
|
|
||||||
}
|
|
||||||
return makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'POST',
|
|
||||||
path: `/sell/inventory/v1/offer/${encodeURIComponent(offerId)}/withdraw`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function publishOfferForSku(marketplace, sku, listing) {
|
|
||||||
if (!sku) {
|
|
||||||
throw new Error('SKU (_reference) is required to publish an offer');
|
|
||||||
}
|
|
||||||
if (!listing) {
|
|
||||||
throw new Error(
|
|
||||||
'Listing is required to publish an eBay offer (stock location address is used for item location).'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const merchantLocationKey = await ensureMerchantLocation(marketplace, listing);
|
|
||||||
const offers = await fetchOffers(marketplace, sku);
|
|
||||||
const existingOffer = offers[0];
|
|
||||||
if (!existingOffer?.offerId) {
|
|
||||||
throw new Error(
|
|
||||||
`No eBay offer exists for SKU "${sku}". Create or sync the listing so an offer exists before publishing.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingOffer.merchantLocationKey !== merchantLocationKey) {
|
|
||||||
await makeRequest({
|
|
||||||
marketplace,
|
|
||||||
method: 'PUT',
|
|
||||||
path: `/sell/inventory/v1/offer/${existingOffer.offerId}`,
|
|
||||||
body: { ...existingOffer, merchantLocationKey },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const publishResult = await publishOfferById(marketplace, existingOffer.offerId);
|
|
||||||
return {
|
|
||||||
offerId: existingOffer.offerId,
|
|
||||||
listingId: publishResult?.listingId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function withdrawOfferForSku(marketplace, sku) {
|
|
||||||
if (!sku) {
|
|
||||||
throw new Error('SKU (_reference) is required to withdraw an offer');
|
|
||||||
}
|
|
||||||
const offers = await fetchOffers(marketplace, sku);
|
|
||||||
const existingOffer = offers[0];
|
|
||||||
if (!existingOffer?.offerId) {
|
|
||||||
throw new Error(`No eBay offer exists for SKU "${sku}".`);
|
|
||||||
}
|
|
||||||
await withdrawOfferById(marketplace, existingOffer.offerId);
|
|
||||||
return { offerId: existingOffer.offerId };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function safeFetchInventoryItemGroup(marketplace, groupKey) {
|
async function safeFetchInventoryItemGroup(marketplace, groupKey) {
|
||||||
try {
|
try {
|
||||||
return await makeRequest({
|
return await makeRequest({
|
||||||
@ -651,41 +257,6 @@ export async function syncItems(marketplace) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LISTING_STATUS_MAP = {
|
|
||||||
ACTIVE: 'active',
|
|
||||||
OUT_OF_STOCK: 'inactive',
|
|
||||||
ENDED: 'inactive',
|
|
||||||
PUBLISHED: 'active',
|
|
||||||
UNPUBLISHED: 'draft',
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolveOfferState(offers) {
|
|
||||||
let stateType = 'draft';
|
|
||||||
for (const offer of offers) {
|
|
||||||
if (offer?.status && LISTING_STATUS_MAP[offer.status]) {
|
|
||||||
const mapped = LISTING_STATUS_MAP[offer.status];
|
|
||||||
if (mapped === 'active') return 'active';
|
|
||||||
if (mapped !== 'draft') stateType = mapped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stateType;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildVarientEntry(item, offers) {
|
|
||||||
const offer = offers?.[0];
|
|
||||||
const price = offer?.pricingSummary?.price?.value
|
|
||||||
? parseFloat(offer.pricingSummary.price.value)
|
|
||||||
: undefined;
|
|
||||||
const currency = offer?.pricingSummary?.price?.currency || undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
_reference: item.sku,
|
|
||||||
price,
|
|
||||||
currency,
|
|
||||||
state: { type: resolveOfferState(offers || []) },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapProductToListing(ebayItem) {
|
export function mapProductToListing(ebayItem) {
|
||||||
if (ebayItem._type === 'group') {
|
if (ebayItem._type === 'group') {
|
||||||
const group = ebayItem._group;
|
const group = ebayItem._group;
|
||||||
@ -723,10 +294,7 @@ export function mapProductToListing(ebayItem) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const currency = offer?.pricingSummary?.price?.currency || undefined;
|
const currency = offer?.pricingSummary?.price?.currency || undefined;
|
||||||
|
|
||||||
let stateType = 'draft';
|
const stateType = resolveOfferState(ebayItem._offers || []);
|
||||||
if (offer?.status && LISTING_STATUS_MAP[offer.status]) {
|
|
||||||
stateType = LISTING_STATUS_MAP[offer.status];
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = offer?.listingId ? `https://www.ebay.com/itm/${offer.listingId}` : '';
|
const url = offer?.listingId ? `https://www.ebay.com/itm/${offer.listingId}` : '';
|
||||||
|
|
||||||
|
|||||||
@ -152,16 +152,29 @@ export async function makeRequest({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const responseText = await response.text();
|
||||||
|
let data = null;
|
||||||
|
if (responseText) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(responseText);
|
||||||
|
} catch {
|
||||||
|
data = responseText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
console.log('DATA: ' + JSON.stringify(data, null, 2));
|
console.log('DATA: ' + JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok && acceptableStatuses.includes(response.status)) {
|
if (!response.ok && acceptableStatuses.includes(response.status)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const message = data.errors?.[0]?.message || data.error_description || response.statusText;
|
const message =
|
||||||
|
(typeof data === 'object' && (data?.errors?.[0]?.message || data?.error_description)) ||
|
||||||
|
(typeof data === 'string' && data) ||
|
||||||
|
response.statusText;
|
||||||
logger.error(`eBay API error: ${message}`, { status: response.status, path });
|
logger.error(`eBay API error: ${message}`, { status: response.status, path });
|
||||||
throw new Error(`eBay API error (${response.status}): ${message}`);
|
throw new Error(`eBay API error (${response.status}): ${message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -225,7 +225,7 @@ 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'])
|
.populate(['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'])
|
||||||
.lean();
|
.lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,20 +12,40 @@ import {
|
|||||||
listCourierServicesByPropertiesRouteHandler,
|
listCourierServicesByPropertiesRouteHandler,
|
||||||
getCourierServiceStatsRouteHandler,
|
getCourierServiceStatsRouteHandler,
|
||||||
getCourierServiceHistoryRouteHandler,
|
getCourierServiceHistoryRouteHandler,
|
||||||
searchCourierServicesRouteHandler
|
searchCourierServicesRouteHandler,
|
||||||
} from '../../services/management/courierservice.js';
|
} from '../../services/management/courierservice.js';
|
||||||
|
|
||||||
// list of courier services
|
// list of courier services
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = ['courier._id', 'name', 'active', 'deliveryTime'];
|
const allowedFilters = [
|
||||||
|
'_id',
|
||||||
|
'courier',
|
||||||
|
'courier._id',
|
||||||
|
'name',
|
||||||
|
'active',
|
||||||
|
'tracked',
|
||||||
|
'deliveryTime',
|
||||||
|
'cost',
|
||||||
|
'costWithTax',
|
||||||
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listCourierServicesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listCourierServicesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
const allowedFilters = ['courier._id', 'name', 'active', 'deliveryTime', 'courier'];
|
const allowedFilters = [
|
||||||
|
'_id',
|
||||||
|
'courier',
|
||||||
|
'courier._id',
|
||||||
|
'name',
|
||||||
|
'active',
|
||||||
|
'tracked',
|
||||||
|
'deliveryTime',
|
||||||
|
'cost',
|
||||||
|
'costWithTax',
|
||||||
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
listCourierServicesByPropertiesRouteHandler(req, res, properties, filter);
|
listCourierServicesByPropertiesRouteHandler(req, res, properties, filter);
|
||||||
});
|
});
|
||||||
@ -34,7 +54,6 @@ router.get('/search', isAuthenticated, (req, res) => {
|
|||||||
searchCourierServicesRouteHandler(req, res, search);
|
searchCourierServicesRouteHandler(req, res, search);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
router.post('/', isAuthenticated, (req, res) => {
|
||||||
newCourierServiceRouteHandler(req, res);
|
newCourierServiceRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
getListingHistoryRouteHandler,
|
getListingHistoryRouteHandler,
|
||||||
publishListingRouteHandler,
|
publishListingRouteHandler,
|
||||||
unpublishListingRouteHandler,
|
unpublishListingRouteHandler,
|
||||||
searchListingsRouteHandler
|
searchListingsRouteHandler,
|
||||||
} from '../../services/sales/listings.js';
|
} from '../../services/sales/listings.js';
|
||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
@ -24,6 +24,7 @@ router.get('/', isAuthenticated, (req, res) => {
|
|||||||
'vendor',
|
'vendor',
|
||||||
'stockLocation',
|
'stockLocation',
|
||||||
'marketplace',
|
'marketplace',
|
||||||
|
'courierServices',
|
||||||
'state',
|
'state',
|
||||||
'state.type',
|
'state.type',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@ -40,6 +41,7 @@ router.get('/properties', isAuthenticated, (req, res) => {
|
|||||||
'vendor',
|
'vendor',
|
||||||
'stockLocation',
|
'stockLocation',
|
||||||
'marketplace',
|
'marketplace',
|
||||||
|
'courierServices',
|
||||||
'state',
|
'state',
|
||||||
'state.type',
|
'state.type',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@ -53,7 +55,6 @@ router.get('/search', isAuthenticated, (req, res) => {
|
|||||||
searchListingsRouteHandler(req, res, search);
|
searchListingsRouteHandler(req, res, search);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
router.post('/', isAuthenticated, (req, res) => {
|
||||||
newListingRouteHandler(req, res);
|
newListingRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
searchObjects
|
searchObjects,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
const logger = log4js.getLogger('Couriers');
|
const logger = log4js.getLogger('Couriers');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -129,6 +129,7 @@ export const editCourierRouteHandler = async (req, res) => {
|
|||||||
export const newCourierRouteHandler = async (req, res) => {
|
export const newCourierRouteHandler = async (req, res) => {
|
||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
_reference: req.body?._reference,
|
||||||
contact: req.body?.contact,
|
contact: req.body?.contact,
|
||||||
country: req.body?.country,
|
country: req.body?.country,
|
||||||
name: req.body?.name,
|
name: req.body?.name,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
searchObjects
|
searchObjects,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
const logger = log4js.getLogger('CourierServices');
|
const logger = log4js.getLogger('CourierServices');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -36,7 +36,7 @@ export const listCourierServicesRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['courier'],
|
populate: ['courier', 'costTaxRate'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -59,7 +59,7 @@ export const listCourierServicesByPropertiesRouteHandler = async (
|
|||||||
model: courierServiceModel,
|
model: courierServiceModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['courier'],
|
populate: ['courier', 'costTaxRate'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -85,7 +85,7 @@ export const getCourierServiceRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: courierServiceModel,
|
model: courierServiceModel,
|
||||||
id,
|
id,
|
||||||
populate: ['courier'],
|
populate: ['courier', 'costTaxRate'],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Courier service not found with supplied id.`);
|
logger.warn(`Courier service not found with supplied id.`);
|
||||||
@ -109,6 +109,13 @@ export const editCourierServiceRouteHandler = async (req, res) => {
|
|||||||
active: req.body?.active,
|
active: req.body?.active,
|
||||||
tracked: req.body?.tracked,
|
tracked: req.body?.tracked,
|
||||||
name: req.body?.name,
|
name: req.body?.name,
|
||||||
|
cost: req.body?.cost,
|
||||||
|
costTaxRate: req.body?.costTaxRate,
|
||||||
|
costWithTax: req.body?.costWithTax,
|
||||||
|
additionalCost: req.body?.additionalCost,
|
||||||
|
additionalCostWithTax: req.body?.additionalCostWithTax,
|
||||||
|
shippingCurrency: req.body?.shippingCurrency,
|
||||||
|
international: req.body?.international,
|
||||||
};
|
};
|
||||||
// Create audit log before updating
|
// Create audit log before updating
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
@ -116,7 +123,7 @@ export const editCourierServiceRouteHandler = async (req, res) => {
|
|||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: ['courier'],
|
populate: ['courier', 'costTaxRate'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -133,12 +140,20 @@ export const editCourierServiceRouteHandler = async (req, res) => {
|
|||||||
export const newCourierServiceRouteHandler = async (req, res) => {
|
export const newCourierServiceRouteHandler = async (req, res) => {
|
||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
_reference: req.body?._reference,
|
||||||
courier: req.body?.courier,
|
courier: req.body?.courier,
|
||||||
website: req.body?.website,
|
website: req.body?.website,
|
||||||
deliveryTime: req.body?.deliveryTime,
|
deliveryTime: req.body?.deliveryTime,
|
||||||
active: req.body?.active,
|
active: req.body?.active,
|
||||||
tracked: req.body?.tracked,
|
tracked: req.body?.tracked,
|
||||||
name: req.body?.name,
|
name: req.body?.name,
|
||||||
|
cost: req.body?.cost,
|
||||||
|
costTaxRate: req.body?.costTaxRate,
|
||||||
|
costWithTax: req.body?.costWithTax,
|
||||||
|
additionalCost: req.body?.additionalCost,
|
||||||
|
additionalCostWithTax: req.body?.additionalCostWithTax,
|
||||||
|
shippingCurrency: req.body?.shippingCurrency,
|
||||||
|
international: req.body?.international,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: courierServiceModel,
|
model: courierServiceModel,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
checkStates,
|
checkStates,
|
||||||
searchObjects
|
searchObjects,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
import {
|
import {
|
||||||
hasIntegration,
|
hasIntegration,
|
||||||
@ -28,7 +28,12 @@ import {
|
|||||||
const logger = log4js.getLogger('Listings');
|
const logger = log4js.getLogger('Listings');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
function pushToMarketplace(marketplaceId, listingData, user, { isNew = false, isDelete = false } = {}) {
|
function pushToMarketplace(
|
||||||
|
marketplaceId,
|
||||||
|
listingData,
|
||||||
|
user,
|
||||||
|
{ isNew = false, isDelete = false } = {}
|
||||||
|
) {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
try {
|
try {
|
||||||
const marketplace = await marketplaceModel.findById(marketplaceId);
|
const marketplace = await marketplaceModel.findById(marketplaceId);
|
||||||
@ -71,7 +76,7 @@ export const listListingsRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -94,7 +99,7 @@ export const listListingsByPropertiesRouteHandler = async (
|
|||||||
model: listingModel,
|
model: listingModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -120,7 +125,7 @@ export const getListingRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
id,
|
id,
|
||||||
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Listing not found with supplied id.`);
|
logger.warn(`Listing not found with supplied id.`);
|
||||||
@ -143,13 +148,14 @@ export const editListingRouteHandler = async (req, res) => {
|
|||||||
marketplace: req.body.marketplace,
|
marketplace: req.body.marketplace,
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
url: req.body.url,
|
url: req.body.url,
|
||||||
|
courierServices: req.body.courierServices,
|
||||||
};
|
};
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -177,6 +183,7 @@ export const newListingRouteHandler = async (req, res) => {
|
|||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
state: req.body.state || { type: 'draft' },
|
state: req.body.state || { type: 'draft' },
|
||||||
url: req.body.url,
|
url: req.body.url,
|
||||||
|
courierServices: req.body.courierServices,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
@ -306,7 +313,7 @@ export const publishListingRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
const listing = await listingModel
|
const listing = await listingModel
|
||||||
.findById(id)
|
.findById(id)
|
||||||
.populate(['marketplace', 'vendor', 'stockLocation'])
|
.populate(['marketplace', 'vendor', 'stockLocation', 'courierServices'])
|
||||||
.lean();
|
.lean();
|
||||||
if (!listing) {
|
if (!listing) {
|
||||||
return res.status(404).send({ error: 'Listing not found.', code: 404 });
|
return res.status(404).send({ error: 'Listing not found.', code: 404 });
|
||||||
@ -382,13 +389,13 @@ export const publishListingRouteHandler = async (req, res) => {
|
|||||||
id,
|
id,
|
||||||
updateData: listingUpdate,
|
updateData: listingUpdate,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const updated = await getObject({
|
const updated = await getObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
id,
|
id,
|
||||||
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'],
|
||||||
});
|
});
|
||||||
res.send(updated);
|
res.send(updated);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -467,13 +474,13 @@ export const unpublishListingRouteHandler = async (req, res) => {
|
|||||||
lastSyncedAt: new Date(),
|
lastSyncedAt: new Date(),
|
||||||
},
|
},
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const updated = await getObject({
|
const updated = await getObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
id,
|
id,
|
||||||
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace', 'courierServices'],
|
||||||
});
|
});
|
||||||
res.send(updated);
|
res.send(updated);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
checkStates,
|
checkStates,
|
||||||
searchObjects
|
searchObjects,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
import { listingModel } from '../../database/schemas/sales/listing.schema.js';
|
import { listingModel } from '../../database/schemas/sales/listing.schema.js';
|
||||||
import {
|
import {
|
||||||
@ -247,7 +247,12 @@ export const publishListingVarientRouteHandler = async (req, res) => {
|
|||||||
.findById(id)
|
.findById(id)
|
||||||
.populate({
|
.populate({
|
||||||
path: 'listing',
|
path: 'listing',
|
||||||
populate: [{ path: 'marketplace' }, { path: 'vendor' }, { path: 'stockLocation' }],
|
populate: [
|
||||||
|
{ path: 'marketplace' },
|
||||||
|
{ path: 'vendor' },
|
||||||
|
{ path: 'stockLocation' },
|
||||||
|
{ path: 'courierServices' },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.lean();
|
.lean();
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user