Implemented stock locations.
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 is contained in:
parent
d3c662a9ec
commit
57f057e3aa
@ -2,11 +2,21 @@ import mongoose from 'mongoose';
|
|||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const addressSchema = new Schema({
|
||||||
|
building: { required: false, type: String },
|
||||||
|
addressLine1: { required: false, type: String },
|
||||||
|
addressLine2: { required: false, type: String },
|
||||||
|
city: { required: false, type: String },
|
||||||
|
state: { required: false, type: String },
|
||||||
|
postcode: { required: false, type: String },
|
||||||
|
country: { required: false, type: String },
|
||||||
|
});
|
||||||
|
|
||||||
const stockLocationSchema = new Schema(
|
const stockLocationSchema = new Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
notes: { type: String, required: false },
|
address: { required: false, type: addressSchema },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -56,6 +56,8 @@ const hostSchema = new mongoose.Schema(
|
|||||||
connectedAt: { required: false, type: Date },
|
connectedAt: { required: false, type: Date },
|
||||||
authCode: { type: { required: false, type: String } },
|
authCode: { type: { required: false, type: String } },
|
||||||
deviceInfo: { deviceInfoSchema },
|
deviceInfo: { deviceInfoSchema },
|
||||||
|
otp: { type: { required: false, type: String } },
|
||||||
|
otpExpiresAt: { required: false, type: Date },
|
||||||
files: [{ type: mongoose.Schema.Types.ObjectId, ref: 'file' }],
|
files: [{ type: mongoose.Schema.Types.ObjectId, ref: 'file' }],
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
|
|||||||
@ -6,6 +6,8 @@ const listingSchema = new Schema(
|
|||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
product: { type: Schema.Types.ObjectId, ref: 'product', required: false },
|
product: { type: Schema.Types.ObjectId, ref: 'product', required: false },
|
||||||
|
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
||||||
|
stockLocation: { type: Schema.Types.ObjectId, ref: 'stockLocation', required: true },
|
||||||
marketplace: { type: Schema.Types.ObjectId, ref: 'marketplace', required: true },
|
marketplace: { type: Schema.Types.ObjectId, ref: 'marketplace', required: true },
|
||||||
title: { type: String, required: false },
|
title: { type: String, required: false },
|
||||||
state: {
|
state: {
|
||||||
|
|||||||
102
src/integrations/marketplaces/ebay/countryCodes.js
Normal file
102
src/integrations/marketplaces/ebay/countryCodes.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Maps FarmControl country codes (farmcontrol-ui/src/database/Countries.js)
|
||||||
|
* to eBay Sell Inventory API CountryCodeEnum values.
|
||||||
|
* @see https://developer.ebay.com/api-docs/sell/inventory/types/ba:CountryCodeEnum
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
export const EBAY_COUNTRY_CODES = new Set([
|
||||||
|
'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', 'AZ',
|
||||||
|
'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT',
|
||||||
|
'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR',
|
||||||
|
'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER',
|
||||||
|
'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL',
|
||||||
|
'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID',
|
||||||
|
'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI',
|
||||||
|
'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV',
|
||||||
|
'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS',
|
||||||
|
'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR',
|
||||||
|
'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY',
|
||||||
|
'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL',
|
||||||
|
'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL',
|
||||||
|
'TM', 'TN', 'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE',
|
||||||
|
'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FarmControl-only codes (Countries.js) that must map to an eBay enum value.
|
||||||
|
*/
|
||||||
|
const FARMCONTROL_TO_EBAY = {
|
||||||
|
UK: 'GB',
|
||||||
|
'GB-ENG': 'GB',
|
||||||
|
'GB-NIR': 'GB',
|
||||||
|
'GB-SCT': 'GB',
|
||||||
|
'GB-UKM': 'GB',
|
||||||
|
'GB-WLS': 'GB',
|
||||||
|
'BQ-BO': 'BQ',
|
||||||
|
'BQ-SA': 'BQ',
|
||||||
|
'BQ-SE': 'BQ',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FarmControl codes with no eBay equivalent — sync/publish will fail with a clear message.
|
||||||
|
*/
|
||||||
|
const FARMCONTROL_UNSUPPORTED_ON_EBAY = new Set(['SS']);
|
||||||
|
|
||||||
|
/** State/province hints when a UK subdivision code is mapped to GB. */
|
||||||
|
export const FARMCONTROL_GB_SUBDIVISION_STATE = {
|
||||||
|
'GB-ENG': 'England',
|
||||||
|
'GB-NIR': 'Northern Ireland',
|
||||||
|
'GB-SCT': 'Scotland',
|
||||||
|
'GB-WLS': 'Wales',
|
||||||
|
'GB-UKM': 'England',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | null | undefined} farmControlCountryCode
|
||||||
|
* @returns {string | null} eBay CountryCodeEnum or null if missing/blank
|
||||||
|
*/
|
||||||
|
export function toEbayCountryCode(farmControlCountryCode) {
|
||||||
|
if (farmControlCountryCode == null || typeof farmControlCountryCode !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = farmControlCountryCode.trim().toUpperCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FARMCONTROL_UNSUPPORTED_ON_EBAY.has(normalized)) {
|
||||||
|
throw new Error(
|
||||||
|
`Country "${normalized}" is not supported by eBay. Choose a different stock location country.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = FARMCONTROL_TO_EBAY[normalized];
|
||||||
|
if (mapped) {
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EBAY_COUNTRY_CODES.has(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Country "${normalized}" is not supported by eBay. Use a country from the stock location address list.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | null | undefined} farmControlCountryCode
|
||||||
|
* @returns {{ ebayCountryCode: string, farmControlCountryCode: string } | null}
|
||||||
|
*/
|
||||||
|
export function resolveEbayCountry(farmControlCountryCode) {
|
||||||
|
const ebayCountryCode = toEbayCountryCode(farmControlCountryCode);
|
||||||
|
if (!ebayCountryCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ebayCountryCode,
|
||||||
|
farmControlCountryCode: farmControlCountryCode.trim().toUpperCase(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,9 +1,208 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { stockLocationModel } from '../../../database/schemas/inventory/stocklocation.schema.js';
|
||||||
|
import {
|
||||||
|
FARMCONTROL_GB_SUBDIVISION_STATE,
|
||||||
|
resolveEbayCountry,
|
||||||
|
} from './countryCodes.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) {
|
function mapVarientToInventoryItem(varient, listing) {
|
||||||
const item = {
|
const item = {
|
||||||
product: {
|
product: {
|
||||||
@ -27,13 +226,15 @@ function mapVarientToInventoryItem(varient, listing) {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapVarientToOffer(varient, listing, marketplace) {
|
function mapVarientToOffer(varient, listing, marketplace, merchantLocationKey) {
|
||||||
const offer = {
|
const offer = {
|
||||||
sku: varient._reference,
|
sku: varient._reference,
|
||||||
marketplaceId: marketplace.config?.marketplaceId || 'EBAY_GB',
|
marketplaceId: marketplace.config?.marketplaceId || 'EBAY_GB',
|
||||||
format: 'FIXED_PRICE',
|
format: 'FIXED_PRICE',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
applyMarketplaceOfferDefaults(offer, marketplace, merchantLocationKey);
|
||||||
|
|
||||||
const price = varient.price ?? listing.price;
|
const price = varient.price ?? listing.price;
|
||||||
if (price != null) {
|
if (price != null) {
|
||||||
offer.pricingSummary = {
|
offer.pricingSummary = {
|
||||||
@ -59,17 +260,19 @@ async function upsertInventoryItem(marketplace, varient, listing) {
|
|||||||
path: `/sell/inventory/v1/inventory_item/${encodeURIComponent(varient._reference)}`,
|
path: `/sell/inventory/v1/inventory_item/${encodeURIComponent(varient._reference)}`,
|
||||||
body: inventoryItem,
|
body: inventoryItem,
|
||||||
});
|
});
|
||||||
console.log(result);
|
logger.debug('inventoryItem', inventoryItem);
|
||||||
|
logger.debug('result', result);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertOrCreateOffer(marketplace, varient, listing) {
|
async function upsertOrCreateOffer(marketplace, varient, listing) {
|
||||||
|
const merchantLocationKey = await ensureMerchantLocation(marketplace, listing);
|
||||||
const offers = await fetchOffers(marketplace, varient._reference);
|
const offers = await fetchOffers(marketplace, varient._reference);
|
||||||
const existingOffer = offers[0];
|
const existingOffer = offers[0];
|
||||||
|
|
||||||
const price = varient.price ?? listing.price;
|
const price = varient.price ?? listing.price;
|
||||||
|
|
||||||
if (existingOffer?.offerId) {
|
if (existingOffer?.offerId) {
|
||||||
const offerUpdate = {};
|
const offerUpdate = { merchantLocationKey };
|
||||||
if (price != null) {
|
if (price != null) {
|
||||||
offerUpdate.pricingSummary = {
|
offerUpdate.pricingSummary = {
|
||||||
price: {
|
price: {
|
||||||
@ -78,24 +281,24 @@ async function upsertOrCreateOffer(marketplace, varient, listing) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (Object.keys(offerUpdate).length > 0) {
|
await makeRequest({
|
||||||
await makeRequest({
|
marketplace,
|
||||||
marketplace,
|
method: 'PUT',
|
||||||
method: 'PUT',
|
path: `/sell/inventory/v1/offer/${existingOffer.offerId}`,
|
||||||
path: `/sell/inventory/v1/offer/${existingOffer.offerId}`,
|
body: { ...existingOffer, ...offerUpdate },
|
||||||
body: { ...existingOffer, ...offerUpdate },
|
});
|
||||||
});
|
logger.debug('offerUpdate', { ...existingOffer, ...offerUpdate });
|
||||||
}
|
|
||||||
return existingOffer;
|
return existingOffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const offerBody = mapVarientToOffer(varient, listing, marketplace);
|
const offerBody = mapVarientToOffer(varient, listing, marketplace, merchantLocationKey);
|
||||||
const result = await makeRequest({
|
const result = await makeRequest({
|
||||||
marketplace,
|
marketplace,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/sell/inventory/v1/offer',
|
path: '/sell/inventory/v1/offer',
|
||||||
body: offerBody,
|
body: offerBody,
|
||||||
});
|
});
|
||||||
|
logger.debug('offerBody', offerBody);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,10 +514,17 @@ export async function withdrawOfferById(marketplace, offerId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function publishOfferForSku(marketplace, sku) {
|
export async function publishOfferForSku(marketplace, sku, listing) {
|
||||||
if (!sku) {
|
if (!sku) {
|
||||||
throw new Error('SKU (_reference) is required to publish an offer');
|
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 offers = await fetchOffers(marketplace, sku);
|
||||||
const existingOffer = offers[0];
|
const existingOffer = offers[0];
|
||||||
if (!existingOffer?.offerId) {
|
if (!existingOffer?.offerId) {
|
||||||
@ -322,6 +532,16 @@ export async function publishOfferForSku(marketplace, sku) {
|
|||||||
`No eBay offer exists for SKU "${sku}". Create or sync the listing so an offer exists before publishing.`
|
`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);
|
const publishResult = await publishOfferById(marketplace, existingOffer.offerId);
|
||||||
return {
|
return {
|
||||||
offerId: existingOffer.offerId,
|
offerId: existingOffer.offerId,
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export function hasIntegration(provider) {
|
|||||||
return !!providers[provider];
|
return !!providers[provider];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function publishMarketplaceOfferForSku(marketplace, user, sku) {
|
export async function publishMarketplaceOfferForSku(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.publishOfferForSku !== 'function') {
|
if (typeof provider.publishOfferForSku !== 'function') {
|
||||||
@ -37,7 +37,7 @@ export async function publishMarketplaceOfferForSku(marketplace, user, sku) {
|
|||||||
`Marketplace provider "${marketplace.provider}" does not support publishing offers`
|
`Marketplace provider "${marketplace.provider}" does not support publishing offers`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return provider.publishOfferForSku(authenticatedMarketplace, sku);
|
return provider.publishOfferForSku(authenticatedMarketplace, sku, listing);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function withdrawMarketplaceOfferForSku(marketplace, user, sku) {
|
export async function withdrawMarketplaceOfferForSku(marketplace, user, sku) {
|
||||||
@ -223,7 +223,10 @@ async function recalculateMarketplaceState(marketplace, user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFullListing(listingId) {
|
async function fetchFullListing(listingId) {
|
||||||
return listingModel.findById(listingId).lean();
|
return listingModel
|
||||||
|
.findById(listingId)
|
||||||
|
.populate(['product', 'vendor', 'stockLocation', 'marketplace'])
|
||||||
|
.lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchListingVarients(listingId) {
|
async function fetchListingVarients(listingId) {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
|
|
||||||
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 = ['name', 'notes'];
|
const allowedFilters = ['name'];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listStockLocationsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listStockLocationsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,14 +18,32 @@ import {
|
|||||||
|
|
||||||
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 = ['product', 'marketplace', 'state', 'state.type', 'createdAt', 'updatedAt'];
|
const allowedFilters = [
|
||||||
|
'product',
|
||||||
|
'vendor',
|
||||||
|
'stockLocation',
|
||||||
|
'marketplace',
|
||||||
|
'state',
|
||||||
|
'state.type',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listListingsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listListingsRouteHandler(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 = ['product', 'marketplace', 'state', 'state.type', 'createdAt', 'updatedAt'];
|
const allowedFilters = [
|
||||||
|
'product',
|
||||||
|
'vendor',
|
||||||
|
'stockLocation',
|
||||||
|
'marketplace',
|
||||||
|
'state',
|
||||||
|
'state.type',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
listListingsByPropertiesRouteHandler(req, res, properties, filter);
|
listListingsByPropertiesRouteHandler(req, res, properties, filter);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -92,7 +92,7 @@ export const editStockLocationRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
notes: req.body.notes,
|
address: req.body.address,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
@ -140,7 +140,7 @@ export const editMultipleStockLocationsRouteHandler = async (req, res) => {
|
|||||||
export const newStockLocationRouteHandler = async (req, res) => {
|
export const newStockLocationRouteHandler = async (req, res) => {
|
||||||
const newData = {
|
const newData = {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
notes: req.body.notes,
|
address: req.body.address,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: stockLocationModel,
|
model: stockLocationModel,
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const EXPORT_FILTER_BY_TYPE = {
|
|||||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'],
|
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'],
|
||||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||||
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
||||||
stockLocation: ['name'],
|
stockLocation: ['name', 'address'],
|
||||||
stockTransfer: ['state.type', 'postedAt'],
|
stockTransfer: ['state.type', 'postedAt'],
|
||||||
stockAudit: ['filamentStock._id', 'partStock._id'],
|
stockAudit: ['filamentStock._id', 'partStock._id'],
|
||||||
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export const listListingsRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['product', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -93,7 +93,7 @@ export const listListingsByPropertiesRouteHandler = async (
|
|||||||
model: listingModel,
|
model: listingModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['product', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -111,7 +111,7 @@ export const getListingRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
id,
|
id,
|
||||||
populate: ['product', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Listing not found with supplied id.`);
|
logger.warn(`Listing not found with supplied id.`);
|
||||||
@ -129,6 +129,8 @@ export const editListingRouteHandler = async (req, res) => {
|
|||||||
const updateData = {
|
const updateData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
product: req.body.product,
|
product: req.body.product,
|
||||||
|
vendor: req.body.vendor,
|
||||||
|
stockLocation: req.body.stockLocation,
|
||||||
marketplace: req.body.marketplace,
|
marketplace: req.body.marketplace,
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
url: req.body.url,
|
url: req.body.url,
|
||||||
@ -138,7 +140,7 @@ export const editListingRouteHandler = async (req, res) => {
|
|||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: ['product', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -160,6 +162,8 @@ export const newListingRouteHandler = async (req, res) => {
|
|||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
product: req.body.product,
|
product: req.body.product,
|
||||||
|
vendor: req.body.vendor,
|
||||||
|
stockLocation: req.body.stockLocation,
|
||||||
marketplace: req.body.marketplace,
|
marketplace: req.body.marketplace,
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
state: req.body.state || { type: 'draft' },
|
state: req.body.state || { type: 'draft' },
|
||||||
@ -291,11 +295,21 @@ export const publishListingRouteHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const listing = await listingModel.findById(id).populate('marketplace').lean();
|
const listing = await listingModel
|
||||||
|
.findById(id)
|
||||||
|
.populate(['marketplace', 'vendor', 'stockLocation'])
|
||||||
|
.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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!listing.stockLocation) {
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Listing must have a stock location before publishing.',
|
||||||
|
code: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const marketplace = listing.marketplace;
|
const marketplace = listing.marketplace;
|
||||||
if (!marketplace?._id) {
|
if (!marketplace?._id) {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
@ -328,7 +342,8 @@ export const publishListingRouteHandler = async (req, res) => {
|
|||||||
const apiResult = await publishMarketplaceOfferForSku(
|
const apiResult = await publishMarketplaceOfferForSku(
|
||||||
marketplace,
|
marketplace,
|
||||||
req.user,
|
req.user,
|
||||||
v._reference
|
v._reference,
|
||||||
|
listing
|
||||||
);
|
);
|
||||||
await editObject({
|
await editObject({
|
||||||
model: listingVarientModel,
|
model: listingVarientModel,
|
||||||
@ -358,13 +373,13 @@ export const publishListingRouteHandler = async (req, res) => {
|
|||||||
id,
|
id,
|
||||||
updateData: listingUpdate,
|
updateData: listingUpdate,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: ['product', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const updated = await getObject({
|
const updated = await getObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
id,
|
id,
|
||||||
populate: ['product', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
||||||
});
|
});
|
||||||
res.send(updated);
|
res.send(updated);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -443,13 +458,13 @@ export const unpublishListingRouteHandler = async (req, res) => {
|
|||||||
lastSyncedAt: new Date(),
|
lastSyncedAt: new Date(),
|
||||||
},
|
},
|
||||||
user: req.user,
|
user: req.user,
|
||||||
populate: ['product', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const updated = await getObject({
|
const updated = await getObject({
|
||||||
model: listingModel,
|
model: listingModel,
|
||||||
id,
|
id,
|
||||||
populate: ['product', 'marketplace'],
|
populate: ['product', 'vendor', 'stockLocation', 'marketplace'],
|
||||||
});
|
});
|
||||||
res.send(updated);
|
res.send(updated);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -236,7 +236,10 @@ export const publishListingVarientRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
const doc = await listingVarientModel
|
const doc = await listingVarientModel
|
||||||
.findById(id)
|
.findById(id)
|
||||||
.populate({ path: 'listing', populate: { path: 'marketplace' } })
|
.populate({
|
||||||
|
path: 'listing',
|
||||||
|
populate: [{ path: 'marketplace' }, { path: 'vendor' }, { path: 'stockLocation' }],
|
||||||
|
})
|
||||||
.lean();
|
.lean();
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
return res.status(404).send({ error: 'Listing varient not found.', code: 404 });
|
return res.status(404).send({ error: 'Listing varient not found.', code: 404 });
|
||||||
@ -267,10 +270,18 @@ export const publishListingVarientRouteHandler = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!doc.listing?.stockLocation) {
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Listing must have a stock location before publishing.',
|
||||||
|
code: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const apiResult = await publishMarketplaceOfferForSku(
|
const apiResult = await publishMarketplaceOfferForSku(
|
||||||
marketplace,
|
marketplace,
|
||||||
req.user,
|
req.user,
|
||||||
doc._reference
|
doc._reference,
|
||||||
|
doc.listing
|
||||||
);
|
);
|
||||||
|
|
||||||
await editObject({
|
await editObject({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user