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 '';
|
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:
|
// 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
|
// 2. listing id match, disambiguated by size when several products share it
|
||||||
const matchProduct = (
|
const matchProduct = (
|
||||||
products: IProduct[],
|
products: IProduct[],
|
||||||
transaction: any
|
transaction: any
|
||||||
): IProduct | undefined => {
|
): IProduct | undefined => {
|
||||||
const variantStr = (transaction.variations || [])
|
const variantStr = transactionVariantString(transaction);
|
||||||
.map((v: any) => `${v.formatted_name}: ${v.formatted_value}`)
|
|
||||||
.join(', ');
|
|
||||||
const candidatesByTitle = [
|
const candidatesByTitle = [
|
||||||
transaction.title || '',
|
|
||||||
variantStr ? `${transaction.title} ${variantStr}` : '',
|
variantStr ? `${transaction.title} ${variantStr}` : '',
|
||||||
|
transaction.title || '',
|
||||||
].filter(Boolean).map(normalizeTitle);
|
].filter(Boolean).map(normalizeTitle);
|
||||||
|
|
||||||
for (const product of products) {
|
for (const candidate of candidatesByTitle) {
|
||||||
const names = [product.title, ...(product.aliases || [])].map(normalizeTitle);
|
const product = products.find(p =>
|
||||||
if (candidatesByTitle.some(c => names.includes(c))) return product;
|
[p.title, ...(p.aliases || [])].map(normalizeTitle).includes(candidate)
|
||||||
|
);
|
||||||
|
if (product) return product;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transaction.listing_id) {
|
if (transaction.listing_id) {
|
||||||
|
|
@ -243,11 +250,15 @@ const upsertOrderFromReceipt = async (
|
||||||
// Snapshot catalog costs onto items at sync time (same model as PDF import)
|
// Snapshot catalog costs onto items at sync time (same model as PDF import)
|
||||||
let allMatched = true;
|
let allMatched = true;
|
||||||
const items = (receipt.transactions || []).map((transaction: any) => {
|
const items = (receipt.transactions || []).map((transaction: any) => {
|
||||||
|
const variantStr = transactionVariantString(transaction);
|
||||||
const product = matchProduct(products, transaction);
|
const product = matchProduct(products, transaction);
|
||||||
if (!product) {
|
if (!product) {
|
||||||
allMatched = false;
|
allMatched = false;
|
||||||
if (!result.unmatchedItems.includes(transaction.title)) {
|
// Report with the variant suffix so each size resolves to its own
|
||||||
result.unmatchedItems.push(transaction.title);
|
// 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) {
|
} else if (!product.etsyListingId && transaction.listing_id) {
|
||||||
// Learn the listing id for future deterministic matching
|
// Learn the listing id for future deterministic matching
|
||||||
|
|
@ -255,10 +266,6 @@ const upsertOrderFromReceipt = async (
|
||||||
product.save().catch(() => {});
|
product.save().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantStr = (transaction.variations || [])
|
|
||||||
.map((v: any) => `${v.formatted_name}: ${v.formatted_value}`)
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: transaction.title,
|
title: transaction.title,
|
||||||
quantity: transaction.quantity || 1,
|
quantity: transaction.quantity || 1,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue