import { syncProductCategory } from './categories.js'; import { syncFulfillmentPolicy } from './fulfillmentPolicies.js'; import { buildVarientEntry, fetchOffers, resolveOfferState, syncOfferAndMaybePublish, upsertInventoryItem, } from './listingVarients.js'; import { makeRequest, logger } from './shared.js'; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function createOrReplaceGroup(marketplace, listing, varients) { const groupKey = listing._reference; const variantSKUs = varients.map((v) => v._reference).filter(Boolean); const body = { title: listing.title || groupKey, variantSKUs, }; if (listing.description) { body.description = listing.description; } if (listing.imageUrls?.length) { body.imageUrls = listing.imageUrls; } await makeRequest({ marketplace, method: 'PUT', path: `/sell/inventory/v1/inventory_item_group/${encodeURIComponent(groupKey)}`, body, }); } async function deleteGroup(marketplace, groupKey) { try { await makeRequest({ marketplace, method: 'DELETE', path: `/sell/inventory/v1/inventory_item_group/${encodeURIComponent(groupKey)}`, acceptableStatuses: [404], }); } catch (err) { logger.warn(`Failed to delete inventory item group "${groupKey}": ${err.message}`); } } async function syncSingleVarientListing(marketplace, listing, varient) { await upsertInventoryItem(marketplace, varient, listing); // If this listing used to be grouped, remove the stale group before treating it as a standalone item. if (listing._reference) { const existingGroup = await safeFetchInventoryItemGroup(marketplace, listing._reference); if (existingGroup) { await deleteGroup(marketplace, listing._reference); } } return syncOfferAndMaybePublish(marketplace, listing, varient); } async function syncGroupedListing(marketplace, listing, varients) { logger.info( `Syncing eBay inventory item group "${listing._reference}" with ${varients.length} varient(s)` ); for (const varient of varients) { await upsertInventoryItem(marketplace, varient, listing); } // Brief delay so eBay can resolve the new inventory item SKUs before creating the group. await sleep(1000); await createOrReplaceGroup(marketplace, listing, varients); let firstPublishedUrl = ''; for (const varient of varients) { try { const publishedUrl = await syncOfferAndMaybePublish(marketplace, listing, varient); if (publishedUrl && !firstPublishedUrl) firstPublishedUrl = publishedUrl; } catch (err) { logger.warn(`Failed to create offer for varient ${varient._reference}: ${err.message}`); } } return firstPublishedUrl; } async function syncListing(marketplace, listing, varients, actionLabel) { const ref = listing._reference; if (!ref) { throw new Error(`Listing must have a _reference to ${actionLabel} on eBay`); } const validVarients = (varients || []).filter((varient) => varient?._reference); if (validVarients.length === 0) { throw new Error( `Listing must have at least one varient with a _reference to ${actionLabel} on eBay` ); } 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) { logger.info( `Syncing standalone eBay inventory item "${validVarients[0]._reference}" for listing "${ref}"` ); const url = await syncSingleVarientListing(marketplace, listingWithContext, validVarients[0]); return { url }; } const url = await syncGroupedListing(marketplace, listingWithContext, validVarients); return { url }; } export async function createItem(marketplace, listing, varients) { return syncListing(marketplace, listing, varients, 'create'); } export async function updateItem(marketplace, listing, varients) { return syncListing(marketplace, listing, varients, 'update'); } export async function deleteItem(marketplace, listing) { const ref = listing._reference; if (!ref) return; logger.info(`Deleting eBay inventory item group "${ref}"`); await deleteGroup(marketplace, ref); } // --- Sync helpers (inbound from eBay) --- async function fetchAllInventoryItems(marketplace) { const items = []; let offset = 0; const limit = 100; do { const data = await makeRequest({ marketplace, path: '/sell/inventory/v1/inventory_item', params: { limit, offset }, }); if (data?.inventoryItems?.length) { items.push(...data.inventoryItems); } if (!data?.inventoryItems?.length || items.length >= (data.total || 0)) { break; } offset += limit; } while (true); return items; } async function safeFetchInventoryItemGroup(marketplace, groupKey) { try { return await makeRequest({ marketplace, path: `/sell/inventory/v1/inventory_item_group/${encodeURIComponent(groupKey)}`, acceptableStatuses: [404], }); } catch (err) { logger.warn(`Failed to fetch inventory item group "${groupKey}": ${err.message}`); return null; } } export async function syncItems(marketplace) { logger.info(`Syncing inventory from eBay marketplace: ${marketplace.name}`); const inventoryItems = await fetchAllInventoryItems(marketplace); const itemsBySku = new Map(); const groupKeysSet = new Set(); const groupedSkus = new Set(); for (const item of inventoryItems) { itemsBySku.set(item.sku, item); if (item.inventoryItemGroupKeys?.length) { for (const key of item.inventoryItemGroupKeys) { groupKeysSet.add(key); } } } const results = []; for (const groupKey of groupKeysSet) { const group = await safeFetchInventoryItemGroup(marketplace, groupKey); if (!group) continue; const variantSkus = group.variantSKUs || []; for (const sku of variantSkus) { groupedSkus.add(sku); } const variantItems = []; for (const sku of variantSkus) { const item = itemsBySku.get(sku); if (item) { try { const offers = await fetchOffers(marketplace, sku); variantItems.push({ ...item, _offers: offers }); } catch (err) { logger.warn(`Failed to fetch offers for group variant SKU ${sku}: ${err.message}`); variantItems.push({ ...item, _offers: [] }); } } } results.push({ _type: 'group', _groupKey: groupKey, _group: group, _variants: variantItems, }); } for (const item of inventoryItems) { if (groupedSkus.has(item.sku)) continue; try { const offers = await fetchOffers(marketplace, item.sku); results.push({ _type: 'single', ...item, _offers: offers, }); } catch (err) { logger.warn(`Failed to fetch offers for SKU ${item.sku}: ${err.message}`); results.push({ _type: 'single', ...item, _offers: [], }); } } logger.info( `Fetched ${results.length} listing(s) from eBay (${groupKeysSet.size} group(s), ${results.length - groupKeysSet.size} standalone)` ); return results; } export function mapProductToListing(ebayItem) { if (ebayItem._type === 'group') { const group = ebayItem._group; const variants = ebayItem._variants || []; const allOffers = variants.flatMap((v) => v._offers || []); const stateType = resolveOfferState(allOffers); const firstPublishedOffer = allOffers.find((o) => o?.listingId); const url = firstPublishedOffer?.listingId ? `https://www.ebay.com/itm/${firstPublishedOffer.listingId}` : ''; const firstOffer = allOffers[0]; const price = firstOffer?.pricingSummary?.price?.value ? parseFloat(firstOffer.pricingSummary.price.value) : undefined; const currency = firstOffer?.pricingSummary?.price?.currency || undefined; const varients = variants.map((v) => buildVarientEntry(v, v._offers || [])); return { _reference: ebayItem._groupKey, title: group.title || ebayItem._groupKey, state: { type: stateType }, price, currency, url, varients, }; } const offer = ebayItem._offers?.[0]; const price = offer?.pricingSummary?.price?.value ? parseFloat(offer.pricingSummary.price.value) : undefined; const currency = offer?.pricingSummary?.price?.currency || undefined; const stateType = resolveOfferState(ebayItem._offers || []); const url = offer?.listingId ? `https://www.ebay.com/itm/${offer.listingId}` : ''; const varients = [buildVarientEntry(ebayItem, ebayItem._offers || [])]; return { _reference: ebayItem.sku, title: ebayItem.product?.title || ebayItem.sku, state: { type: stateType }, price, currency, url, varients, }; }