import { makeRequest, logger, getEbayMarketplaceId } from './shared.js'; const SELLING_POLICY_PROGRAM = 'SELLING_POLICY_MANAGEMENT'; export const POLICY_CATEGORY_TYPE = 'ALL_EXCLUDING_MOTORS_VEHICLES'; export function idOf(value) { if (value == null) return ''; if (typeof value === 'object') { if (value._id != null) return String(value._id); return String(value); } return String(value); } export function isPopulatedDocument(value) { return ( value && typeof value === 'object' && !(value.constructor?.name === 'ObjectId') && (value.name != null || value._reference != null || Array.isArray(value.marketplaces)) ); } export function getMarketplaceMapping(doc, marketplace) { const marketplaceId = idOf(marketplace); if (!marketplaceId) return (doc?.marketplaces || [])[0] || null; return ( (doc?.marketplaces || []).find((entry) => idOf(entry.marketplace) === marketplaceId) || null ); } export function mappingExternalReference(doc, marketplace) { return getMarketplaceMapping(doc, marketplace)?.externalReference || ''; } export async function persistMarketplaceMapping( model, doc, marketplace, { externalReference, stateType, message } = {} ) { if (!model || !doc) return null; const marketplaceId = idOf(marketplace); const docId = idOf(doc); if (!marketplaceId || !docId) return null; const current = (await model.findById(docId).lean()) || doc; const mappings = [...(current?.marketplaces || [])]; const index = mappings.findIndex((entry) => idOf(entry.marketplace) === marketplaceId); const previous = index >= 0 ? mappings[index] : {}; const next = { ...(previous._id ? { _id: previous._id } : {}), marketplace: previous.marketplace || marketplaceId, externalReference: externalReference !== undefined ? externalReference : previous.externalReference, state: { type: stateType || previous.state?.type || 'pending', ...(message != null && message !== '' ? { message } : stateType === 'failed' ? { message: previous.state?.message } : {}), }, }; if (index >= 0) mappings[index] = next; else mappings.push(next); await model.updateOne({ _id: docId }, { $set: { marketplaces: mappings } }); try { const { deleteObjectCache } = await import('../../../database/database.js'); const { distributeUpdate } = await import('../../../utils.js'); await deleteObjectCache({ model, id: docId }); let broadcastMappings = mappings; const query = model.findById(docId); if (typeof query?.populate === 'function') { const populated = await query.populate('marketplaces.marketplace').lean(); if (populated?.marketplaces) broadcastMappings = populated.marketplaces; } await distributeUpdate({ marketplaces: broadcastMappings }, docId, model.modelName); } catch (err) { logger.debug(`Could not broadcast marketplace mapping update for ${docId}: ${err.message}`); } return next; } async function fetchOptedInPrograms(marketplace) { const result = await makeRequest({ marketplace, path: '/sell/account/v1/program/get_opted_in_programs', }); return result?.programs || []; } function hasSellingPolicyManagement(programs) { return programs.some((program) => program?.programType === SELLING_POLICY_PROGRAM); } export async function ensureSellingPolicyManagement(marketplace) { if (hasSellingPolicyManagement(await fetchOptedInPrograms(marketplace))) { return; } logger.info('Opting eBay seller account into Selling Policy Management'); await makeRequest({ marketplace, method: 'POST', path: '/sell/account/v1/program/opt_in', body: { programType: SELLING_POLICY_PROGRAM }, acceptableStatuses: [409], }); if (!hasSellingPolicyManagement(await fetchOptedInPrograms(marketplace))) { throw new Error( 'eBay Selling Policy Management enrollment was requested but is not active yet. eBay can take up to 24 hours to process enrollment; retry publishing later.' ); } } export function isDefaultAccountPolicy(existingPolicy, allPolicies = [], idField) { const existingId = existingPolicy?.[idField]; if (!existingId) return false; const existingIdString = String(existingId); const sources = [existingPolicy, ...allPolicies]; if ( sources.some( (policy) => String(policy?.[idField]) === existingIdString && (policy.categoryTypes || []).some((type) => type?.default === true) ) ) { return true; } const uniqueIds = [ ...new Set( allPolicies.filter((policy) => policy?.[idField]).map((policy) => String(policy[idField])) ), ]; return uniqueIds.length === 1 && uniqueIds[0] === existingIdString; } export function buildCategoryTypes(existingPolicy, allPolicies = [], idField) { const categoryType = { name: POLICY_CATEGORY_TYPE }; if (idField && isDefaultAccountPolicy(existingPolicy, allPolicies, idField)) { categoryType.default = true; } return [categoryType]; } function isDefaultStatusError(err) { return /changing the default status/i.test(err?.message || ''); } function isUnchangedPolicyError(err) { return /same as in the system/i.test(err?.message || ''); } export async function updateAccountPolicy(marketplace, path, policy) { try { await makeRequest({ marketplace, method: 'PUT', path, body: policy }); } catch (err) { if (isUnchangedPolicyError(err)) { logger.debug(`eBay account policy ${path} is already up to date`); return; } if (policy.categoryTypes?.[0]?.default === true || !isDefaultStatusError(err)) { throw err; } logger.debug(`Retrying account policy ${path} with categoryTypes.default=true`); try { await makeRequest({ marketplace, method: 'PUT', path, body: { ...policy, categoryTypes: [{ name: POLICY_CATEGORY_TYPE, default: true }], }, }); } catch (retryErr) { if (isUnchangedPolicyError(retryErr)) { logger.debug(`eBay account policy ${path} is already up to date`); return; } throw retryErr; } } } export async function loadPolicyDocument(model, value) { if (!value) return null; if (isPopulatedDocument(value)) return value; const id = idOf(value); if (!id) return null; return model.findById(id).populate(['marketplaces.marketplace']).lean(); } export function resolveListingPolicy(listing, marketplace, listingField, defaultField) { return ( listing?.[listingField] || listing?.marketplace?.[defaultField] || marketplace?.[defaultField] || null ); } export async function findLocalPolicyByExternalReference(model, marketplace, externalReference) { if (!externalReference) return null; return model .findOne({ marketplaces: { $elemMatch: { marketplace: idOf(marketplace), externalReference: String(externalReference), }, }, }) .lean(); } export async function findLocalPolicyByName(model, marketplace, name) { if (!name) return null; return model .findOne({ name, 'marketplaces.marketplace': idOf(marketplace), }) .lean(); } export async function upsertLocalPolicyFromRemote({ model, marketplace, remoteId, name, fields, user, }) { let local = (await findLocalPolicyByExternalReference(model, marketplace, remoteId)) || (await findLocalPolicyByName(model, marketplace, name)); const mapping = { marketplace: idOf(marketplace), externalReference: String(remoteId), state: { type: 'ready' }, }; if (local) { const mappings = [...(local.marketplaces || [])]; const index = mappings.findIndex((entry) => idOf(entry.marketplace) === idOf(marketplace)); if (index >= 0) mappings[index] = { ...mappings[index], ...mapping }; else mappings.push(mapping); await model.updateOne( { _id: local._id }, { $set: { ...fields, name, marketplaces: mappings, }, } ); return model.findById(local._id).lean(); } const created = await model.create({ ...fields, name, marketplaces: [mapping], }); return created.toObject ? created.toObject() : created; } export { getEbayMarketplaceId };