[object Object]

← back to Designer Wallcoverings

cadence-import: pre-push dedup guard — query Shopify by base-SKU before create; if a live (non-archived) product exists, backfill link + skip create instead of duplicating

79f2fa5943dba37f5695b31ad8b8f057244f0c5a · 2026-06-12 08:59:18 -0700 · SteveStudio2

Files touched

Diff

commit 79f2fa5943dba37f5695b31ad8b8f057244f0c5a
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Fri Jun 12 08:59:18 2026 -0700

    cadence-import: pre-push dedup guard — query Shopify by base-SKU before create; if a live (non-archived) product exists, backfill link + skip create instead of duplicating
---
 shopify/scripts/cadence/cadence-import.js | 40 ++++++++++++++++++++++++++++---
 1 file changed, 37 insertions(+), 3 deletions(-)

diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index a5f773f6..17f59446 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -211,7 +211,40 @@ function buildInput(vendor, cfg, row, retail) {
 
 const PRODUCTSET = `mutation push($input: ProductSetInput!){productSet(synchronous:true,input:$input){product{id handle status} userErrors{field message}}}`;
 
+// ---- PRE-PUSH DEDUP GUARD (Steve 2026-06-12, DTD-picked structural fix) ----
+// The net-new gate selects rows where shopify_product_id='' and blindly CREATEs. If that link
+// is empty due to drift (a Mac2-created product whose id never reached this DB, a late sync, a
+// re-import) but the product ALREADY exists on Shopify, a blind create makes a LIVE duplicate.
+// Before every create we ask Shopify itself whether a NON-archived product already carries this
+// exact variant sku. If so: backfill the catalog link and SKIP the create (idempotent, never
+// clobbers the existing listing). Only archived-only matches fall through to a fresh create
+// (the sku has no live listing). Makes link drift permanently harmless for every vendor.
+async function findExistingProduct(dwSku) {
+  const q = `query($q:String){products(first:15,query:$q){edges{node{id status variants(first:25){edges{node{sku price}}}}}}}`;
+  const r = await gqlRetry(q, { q: `sku:${dwSku}` });
+  const edges = r.json?.data?.products?.edges || [];
+  // base-sku match: strip -Roll/-Sample so the bare dw_sku matches whatever suffix convention the
+  // existing product used (DWAE-500010 ≡ DWAE-500010-Roll). Avoids the "sku:" prefix search's loose
+  // false positives (e.g. DWKK-1000 matching DWKK-100099) while catching cross-tool dups.
+  const baseSku = s => String(s || '').toUpperCase().replace(/-(SAMPLE|ROLL)$/i, '').trim();
+  const key = baseSku(dwSku);
+  const matches = edges.map(e => e.node).filter(n => (n.variants?.edges || []).some(v => baseSku(v.node.sku) === key));
+  if (!matches.length) return null;
+  const live = matches.find(n => n.status === 'ACTIVE') || matches.find(n => n.status === 'DRAFT');
+  if (live) return { id: live.id, status: live.status, statuses: matches.map(m => m.status) };
+  return { id: matches[0].id, status: 'ARCHIVED', archivedOnly: true, statuses: matches.map(m => m.status) };
+}
+
 async function createProduct(table, row, payload) {
+  // DEDUP GUARD — never create a second copy of a sku that already has a live listing.
+  const existing = await findExistingProduct(row.dw_sku);
+  if (existing && !existing.archivedOnly) {
+    const num = String(existing.id).replace(/.*\//, '');
+    // log our computed price to the DB (the hard rule) but DO NOT overwrite the live Shopify product.
+    logPriceToDb(table, row.dw_sku, payload.retail);
+    psql(`UPDATE ${table} SET shopify_product_id='${num}' WHERE dw_sku='${row.dw_sku.replace(/'/g,"''")}' AND coalesce(shopify_product_id::text,'')='';`);
+    return { ok: true, pid: existing.id, action: 'linked-existing', status: existing.status };
+  }
   // dw_unified FIRST
   logPriceToDb(table, row.dw_sku, payload.retail);
   const r = await gqlRetry(PRODUCTSET, { input: payload.input });
@@ -248,7 +281,7 @@ async function createProduct(table, row, payload) {
   for (let i=0;i<Math.min(VENDORS_PER_SLOT, ready.length);i++) picked.push(ready[(start+i)%ready.length]);
   console.log(`\n=== ${COMMIT?'LIVE COMMIT':'DRY-RUN'} — slot ${SLOT}: ${picked.length} vendor(s) × up to ${SKUS_PER_VENDOR} SKUs ===`);
 
-  const plan = []; let created=0, failed=0;
+  const plan = []; let created=0, failed=0, linked=0;
   for (const r of picked) {
     if (COMMIT) ensureOurPriceCol(r.cfg.table);
     const skus = selectSkus(r.cfg, SKUS_PER_VENDOR);
@@ -260,7 +293,8 @@ async function createProduct(table, row, payload) {
       plan.push({ vendor:r.vendor, dw_sku:row.dw_sku, cost:row.cost, retail, sample:SAMPLE_PRICE, title:payload.title });
       if (!COMMIT) { console.log(`  · ${row.dw_sku}  cost $${row.cost.toFixed(2)} → roll $${retail}  sample $${SAMPLE_PRICE}  | ${payload.title.slice(0,52)}`); continue; }
       const res = await createProduct(r.cfg.table, row, payload);
-      if (res.ok) { created++; if(created%10===0) process.stdout.write(`\r  created ${created}…`); }
+      if (res.ok && res.action === 'linked-existing') { linked++; console.log(`  ↪ ${row.dw_sku} already on Shopify (${res.status}) → backfilled link ${String(res.pid).replace(/.*\//,'')}, skipped create (dedup guard)`); }
+      else if (res.ok) { created++; if(created%10===0) process.stdout.write(`\r  created ${created}…`); }
       else { failed++; console.log(`  ✗ ${row.dw_sku} ${JSON.stringify(res.err).slice(0,140)}`); }
       if (res.stop) { console.log(`\n⛔ DAILY_VARIANT_LIMIT reached — stopping (resets in ~24h). created ${created} this run.`); break; }
     }
@@ -270,7 +304,7 @@ async function createProduct(table, row, payload) {
   saveCursor({ idx: (start + picked.length) % ready.length, lastSlot: SLOT, lastRun: TODAY });
   const planFile = path.join(DATADIR, `cadence-plan-${SLOT}-${TODAY}.json`);
   fs.mkdirSync(DATADIR,{recursive:true}); fs.writeFileSync(planFile, JSON.stringify(plan,null,1));
-  console.log(`\n\n${COMMIT?`CREATED ${created}, failed ${failed}`:`DRY-RUN planned ${plan.length} products`} → ${planFile}`);
+  console.log(`\n\n${COMMIT?`CREATED ${created}, linked-existing ${linked} (dedup guard), failed ${failed}`:`DRY-RUN planned ${plan.length} products`} → ${planFile}`);
   if (!COMMIT) console.log('Re-run with --commit to create these on Shopify (DRAFT status, dw_unified logged first).');
 
   // refresh the CNCP "Price Coverage" goal panel now that new cost may have landed.

← 6f53fddf Add dedup-kravet-active.js: dry-run-default tool to archive  ·  back to Designer Wallcoverings  ·  worker-guard: pre-push dedup guard module + 9-test harness + 6cc85e55 →