// Pure helpers for importPurchaseOrderCsvLines: detect CSV rows that target a // product variant (same product + color + size) that already exists under a // DIFFERENT SKU. This happens with renamed SKUs — e.g. the old SKU // "FW26THD18473330L" and the new SKU "AOTHD1847BLKL" both resolve to // "ADG Stamp Hoodie" / Black / Large. Inserting the second would violate the // unique index on ProductVariant (productId, axisValueKey) and abort the whole // import with an opaque "function call error" (#763). Detect up front instead and // surface an actionable, aggregated message. export interface VariantRowInput { sku: string; product: string; color: string; size: string; productId: string; colorValueId: string; sizeValueId: string; } export interface VariantCollision { sku: string; product: string; color: string; size: string; /** The SKU that already claims this (product, color, size) slot. */ conflictWith: string; } /** * Canonical axisValueKey: the attribute value IDs sorted lexicographically and * joined with "|". Must match erp-kit's generateVariants form and the direct * insert in importPurchaseOrderCsvLines Phase G, so keys built here line up with * keys already stored in the DB. */ export function axisValueKeyOf(colorValueId: string, sizeValueId: string): string { return [colorValueId, sizeValueId].sort().join("|"); } /** * Return one entry per CSV row whose (product, color, size) variant is already * claimed by a different SKU — either in the existing catalog * (`existingSkuByProductKey`, keyed by "productId|axisValueKey") or earlier in * the same CSV. Pure: no DB access. */ export function detectVariantCollisions( rows: VariantRowInput[], existingSkuByProductKey: Map, ): VariantCollision[] { const collisions: VariantCollision[] = []; const seenInCsv = new Map(); // "productId|axisValueKey" -> first SKU seen for (const r of rows) { const key = `${r.productId}|${axisValueKeyOf(r.colorValueId, r.sizeValueId)}`; const record = (conflictWith: string) => collisions.push({ sku: r.sku, product: r.product, color: r.color, size: r.size, conflictWith, }); const existing = existingSkuByProductKey.get(key); if (existing && existing !== r.sku) { record(existing); continue; } const prev = seenInCsv.get(key); if (prev && prev !== r.sku) { record(prev); continue; } if (!prev) seenInCsv.set(key, r.sku); } return collisions; } /** Human-readable, aggregated error listing every colliding row. */ export function formatVariantCollisionError(collisions: VariantCollision[]): string { const details = collisions .map( (c) => ` • SKU "${c.sku}" (${c.product} / ${c.color} / ${c.size}) already exists as SKU "${c.conflictWith}"`, ) .join("\n"); return ( `${collisions.length} CSV row(s) map to a product variant (same product, color, and size) that already ` + `exists under a different SKU — this usually means the SKU was renamed. Reconcile the SKUs ` + `(or put the existing SKU on the PO line) before importing:\n${details}` ); }