313 lines
8.8 KiB
JavaScript

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,
};
}