Make Etsy sync matching and unmatched reporting variant-aware
One listing can sell multiple sizes that map to different catalog products with different costs. Match 'title + variant' before bare title so size-specific products win, and report unmatched items with their variant suffix so each size resolves to its own product. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
acce14c000
commit
9219f01ae5
1 changed files with 21 additions and 14 deletions
|
|
@ -151,24 +151,31 @@ const sizeToken = (text: string): string => {
|
|||
return '';
|
||||
};
|
||||
|
||||
// Build the variant suffix for a transaction, e.g. "Size: Large"
|
||||
export const transactionVariantString = (transaction: any): string =>
|
||||
(transaction.variations || [])
|
||||
.map((v: any) => `${v.formatted_name}: ${v.formatted_value}`)
|
||||
.join(', ');
|
||||
|
||||
// Match an Etsy transaction to a catalog product:
|
||||
// 1. exact title/alias match (including "title variations" form)
|
||||
// 1. exact title/alias match — "title variant" first so a size-specific
|
||||
// product beats a generic one when listings have size variants
|
||||
// 2. listing id match, disambiguated by size when several products share it
|
||||
const matchProduct = (
|
||||
products: IProduct[],
|
||||
transaction: any
|
||||
): IProduct | undefined => {
|
||||
const variantStr = (transaction.variations || [])
|
||||
.map((v: any) => `${v.formatted_name}: ${v.formatted_value}`)
|
||||
.join(', ');
|
||||
const variantStr = transactionVariantString(transaction);
|
||||
const candidatesByTitle = [
|
||||
transaction.title || '',
|
||||
variantStr ? `${transaction.title} ${variantStr}` : '',
|
||||
transaction.title || '',
|
||||
].filter(Boolean).map(normalizeTitle);
|
||||
|
||||
for (const product of products) {
|
||||
const names = [product.title, ...(product.aliases || [])].map(normalizeTitle);
|
||||
if (candidatesByTitle.some(c => names.includes(c))) return product;
|
||||
for (const candidate of candidatesByTitle) {
|
||||
const product = products.find(p =>
|
||||
[p.title, ...(p.aliases || [])].map(normalizeTitle).includes(candidate)
|
||||
);
|
||||
if (product) return product;
|
||||
}
|
||||
|
||||
if (transaction.listing_id) {
|
||||
|
|
@ -243,11 +250,15 @@ const upsertOrderFromReceipt = async (
|
|||
// Snapshot catalog costs onto items at sync time (same model as PDF import)
|
||||
let allMatched = true;
|
||||
const items = (receipt.transactions || []).map((transaction: any) => {
|
||||
const variantStr = transactionVariantString(transaction);
|
||||
const product = matchProduct(products, transaction);
|
||||
if (!product) {
|
||||
allMatched = false;
|
||||
if (!result.unmatchedItems.includes(transaction.title)) {
|
||||
result.unmatchedItems.push(transaction.title);
|
||||
// Report with the variant suffix so each size resolves to its own
|
||||
// product; this exact string is what the matcher checks on re-sync
|
||||
const unmatchedKey = variantStr ? `${transaction.title} ${variantStr}` : transaction.title;
|
||||
if (!result.unmatchedItems.includes(unmatchedKey)) {
|
||||
result.unmatchedItems.push(unmatchedKey);
|
||||
}
|
||||
} else if (!product.etsyListingId && transaction.listing_id) {
|
||||
// Learn the listing id for future deterministic matching
|
||||
|
|
@ -255,10 +266,6 @@ const upsertOrderFromReceipt = async (
|
|||
product.save().catch(() => {});
|
||||
}
|
||||
|
||||
const variantStr = (transaction.variations || [])
|
||||
.map((v: any) => `${v.formatted_name}: ${v.formatted_value}`)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
title: transaction.title,
|
||||
quantity: transaction.quantity || 1,
|
||||
|
|
|
|||
Loading…
Reference in a new issue