diff --git a/src/database/schemas/inventory/stocklocation.schema.js b/src/database/schemas/inventory/stocklocation.schema.js index db99cc6..af135f1 100644 --- a/src/database/schemas/inventory/stocklocation.schema.js +++ b/src/database/schemas/inventory/stocklocation.schema.js @@ -2,11 +2,21 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; 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( { _reference: { type: String, default: () => generateId()() }, name: { type: String, required: true }, - notes: { type: String, required: false }, + address: { required: false, type: addressSchema }, }, { timestamps: true } ); diff --git a/src/database/schemas/management/host.schema.js b/src/database/schemas/management/host.schema.js index 4e1098f..fa26f13 100644 --- a/src/database/schemas/management/host.schema.js +++ b/src/database/schemas/management/host.schema.js @@ -56,6 +56,8 @@ const hostSchema = new mongoose.Schema( connectedAt: { required: false, type: Date }, authCode: { type: { required: false, type: String } }, deviceInfo: { deviceInfoSchema }, + otp: { type: { required: false, type: String } }, + otpExpiresAt: { required: false, type: Date }, files: [{ type: mongoose.Schema.Types.ObjectId, ref: 'file' }], }, { timestamps: true } diff --git a/src/database/schemas/sales/listing.schema.js b/src/database/schemas/sales/listing.schema.js index cb824e8..20f14fd 100644 --- a/src/database/schemas/sales/listing.schema.js +++ b/src/database/schemas/sales/listing.schema.js @@ -6,6 +6,8 @@ const listingSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, 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 }, title: { type: String, required: false }, state: { diff --git a/src/integrations/marketplaces/ebay/countryCodes.js b/src/integrations/marketplaces/ebay/countryCodes.js new file mode 100644 index 0000000..23b50b7 --- /dev/null +++ b/src/integrations/marketplaces/ebay/countryCodes.js @@ -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} */ +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(), + }; +} diff --git a/src/integrations/marketplaces/ebay/listings.js b/src/integrations/marketplaces/ebay/listings.js index c4a8c00..a14557a 100644 --- a/src/integrations/marketplaces/ebay/listings.js +++ b/src/integrations/marketplaces/ebay/listings.js @@ -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'; +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) { 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: { @@ -27,13 +226,15 @@ function mapVarientToInventoryItem(varient, listing) { return item; } -function mapVarientToOffer(varient, listing, marketplace) { +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 = { @@ -59,17 +260,19 @@ async function upsertInventoryItem(marketplace, varient, listing) { path: `/sell/inventory/v1/inventory_item/${encodeURIComponent(varient._reference)}`, body: inventoryItem, }); - console.log(result); + 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 = {}; + const offerUpdate = { merchantLocationKey }; if (price != null) { offerUpdate.pricingSummary = { price: { @@ -78,24 +281,24 @@ async function upsertOrCreateOffer(marketplace, varient, listing) { }, }; } - if (Object.keys(offerUpdate).length > 0) { - await makeRequest({ - marketplace, - method: 'PUT', - path: `/sell/inventory/v1/offer/${existingOffer.offerId}`, - body: { ...existingOffer, ...offerUpdate }, - }); - } + 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); + 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; } @@ -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) { 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) { @@ -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.` ); } + + 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, diff --git a/src/integrations/marketplaceworker.js b/src/integrations/marketplaceworker.js index a243dc5..a46b2fe 100644 --- a/src/integrations/marketplaceworker.js +++ b/src/integrations/marketplaceworker.js @@ -29,7 +29,7 @@ export function hasIntegration(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 provider = getProvider(authenticatedMarketplace); 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` ); } - return provider.publishOfferForSku(authenticatedMarketplace, sku); + return provider.publishOfferForSku(authenticatedMarketplace, sku, listing); } export async function withdrawMarketplaceOfferForSku(marketplace, user, sku) { @@ -223,7 +223,10 @@ async function recalculateMarketplaceState(marketplace, user) { } async function fetchFullListing(listingId) { - return listingModel.findById(listingId).lean(); + return listingModel + .findById(listingId) + .populate(['product', 'vendor', 'stockLocation', 'marketplace']) + .lean(); } async function fetchListingVarients(listingId) { diff --git a/src/routes/inventory/stocklocations.js b/src/routes/inventory/stocklocations.js index 6db748b..f020938 100644 --- a/src/routes/inventory/stocklocations.js +++ b/src/routes/inventory/stocklocations.js @@ -17,7 +17,7 @@ import { router.get('/', isAuthenticated, (req, res) => { const { page, limit, property, search, sort, order } = req.query; - const allowedFilters = ['name', 'notes']; + const allowedFilters = ['name']; const filter = getFilter(req.query, allowedFilters); listStockLocationsRouteHandler(req, res, page, limit, property, filter, search, sort, order); }); diff --git a/src/routes/sales/listings.js b/src/routes/sales/listings.js index 70f3eb4..1914bb2 100644 --- a/src/routes/sales/listings.js +++ b/src/routes/sales/listings.js @@ -18,14 +18,32 @@ import { router.get('/', isAuthenticated, (req, res) => { 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); listListingsRouteHandler(req, res, page, limit, property, filter, search, sort, order); }); router.get('/properties', isAuthenticated, (req, res) => { 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); listListingsByPropertiesRouteHandler(req, res, properties, filter); }); diff --git a/src/services/inventory/stocklocations.js b/src/services/inventory/stocklocations.js index e46f8aa..5a0cc5c 100644 --- a/src/services/inventory/stocklocations.js +++ b/src/services/inventory/stocklocations.js @@ -92,7 +92,7 @@ export const editStockLocationRouteHandler = async (req, res) => { const updateData = { name: req.body.name, - notes: req.body.notes, + address: req.body.address, }; const result = await editObject({ @@ -140,7 +140,7 @@ export const editMultipleStockLocationsRouteHandler = async (req, res) => { export const newStockLocationRouteHandler = async (req, res) => { const newData = { name: req.body.name, - notes: req.body.notes, + address: req.body.address, }; const result = await newObject({ model: stockLocationModel, diff --git a/src/services/misc/export.js b/src/services/misc/export.js index 5c147e9..c959276 100644 --- a/src/services/misc/export.js +++ b/src/services/misc/export.js @@ -23,7 +23,7 @@ export const EXPORT_FILTER_BY_TYPE = { orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'], shipment: ['order._id', 'orderType', 'courierService._id'], stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'], - stockLocation: ['name'], + stockLocation: ['name', 'address'], stockTransfer: ['state.type', 'postedAt'], stockAudit: ['filamentStock._id', 'partStock._id'], documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'], diff --git a/src/services/sales/listings.js b/src/services/sales/listings.js index e5032fd..e75895b 100644 --- a/src/services/sales/listings.js +++ b/src/services/sales/listings.js @@ -70,7 +70,7 @@ export const listListingsRouteHandler = async ( search, sort, order, - populate: ['product', 'marketplace'], + populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); if (result?.error) { @@ -93,7 +93,7 @@ export const listListingsByPropertiesRouteHandler = async ( model: listingModel, properties, filter, - populate: ['product', 'marketplace'], + populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); if (result?.error) { @@ -111,7 +111,7 @@ export const getListingRouteHandler = async (req, res) => { const result = await getObject({ model: listingModel, id, - populate: ['product', 'marketplace'], + populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); if (result?.error) { logger.warn(`Listing not found with supplied id.`); @@ -129,6 +129,8 @@ export const editListingRouteHandler = async (req, res) => { const updateData = { updatedAt: new Date(), product: req.body.product, + vendor: req.body.vendor, + stockLocation: req.body.stockLocation, marketplace: req.body.marketplace, title: req.body.title, url: req.body.url, @@ -138,7 +140,7 @@ export const editListingRouteHandler = async (req, res) => { id, updateData, user: req.user, - populate: ['product', 'marketplace'], + populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); if (result.error) { @@ -160,6 +162,8 @@ export const newListingRouteHandler = async (req, res) => { const newData = { updatedAt: new Date(), product: req.body.product, + vendor: req.body.vendor, + stockLocation: req.body.stockLocation, marketplace: req.body.marketplace, title: req.body.title, 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) { 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; if (!marketplace?._id) { return res.status(400).send({ @@ -328,7 +342,8 @@ export const publishListingRouteHandler = async (req, res) => { const apiResult = await publishMarketplaceOfferForSku( marketplace, req.user, - v._reference + v._reference, + listing ); await editObject({ model: listingVarientModel, @@ -358,13 +373,13 @@ export const publishListingRouteHandler = async (req, res) => { id, updateData: listingUpdate, user: req.user, - populate: ['product', 'marketplace'], + populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); const updated = await getObject({ model: listingModel, id, - populate: ['product', 'marketplace'], + populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); res.send(updated); } catch (err) { @@ -443,13 +458,13 @@ export const unpublishListingRouteHandler = async (req, res) => { lastSyncedAt: new Date(), }, user: req.user, - populate: ['product', 'marketplace'], + populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); const updated = await getObject({ model: listingModel, id, - populate: ['product', 'marketplace'], + populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); res.send(updated); } catch (err) { diff --git a/src/services/sales/listingvarients.js b/src/services/sales/listingvarients.js index 5ab7755..9781a39 100644 --- a/src/services/sales/listingvarients.js +++ b/src/services/sales/listingvarients.js @@ -236,7 +236,10 @@ export const publishListingVarientRouteHandler = async (req, res) => { const doc = await listingVarientModel .findById(id) - .populate({ path: 'listing', populate: { path: 'marketplace' } }) + .populate({ + path: 'listing', + populate: [{ path: 'marketplace' }, { path: 'vendor' }, { path: 'stockLocation' }], + }) .lean(); if (!doc) { return res.status(404).send({ error: 'Listing varient not found.', code: 404 }); @@ -267,10 +270,18 @@ export const publishListingVarientRouteHandler = async (req, res) => { } 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( marketplace, req.user, - doc._reference + doc._reference, + doc.listing ); await editObject({