All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
313 lines
8.8 KiB
JavaScript
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,
|
|
};
|
|
}
|