[object Object]

← back to Designer Wallcoverings

Part 2 twin: Schumacher dw_sku→shopify_product_id LINK handoff (prevents Full Monte dups)

c90065ca2f5b8a40aaf2b6a15423e08efaac1e2c · 2026-06-11 19:28:12 -0700 · SteveStudio2

Mac2 exports links (normalized numeric), pushes to Kamatera link-staging, prod ingester
fills ONLY empty shopify_product_id (never clobbers/nulls), --dry default + validation gates.
Dry-run: would fill 471 missing links, 1561 already-linked (kept), 0 unknown. Apply is GATED
(queued: schumacher-relink-and-reenable.md).

Files touched

Diff

commit c90065ca2f5b8a40aaf2b6a15423e08efaac1e2c
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu Jun 11 19:28:12 2026 -0700

    Part 2 twin: Schumacher dw_sku→shopify_product_id LINK handoff (prevents Full Monte dups)
    
    Mac2 exports links (normalized numeric), pushes to Kamatera link-staging, prod ingester
    fills ONLY empty shopify_product_id (never clobbers/nulls), --dry default + validation gates.
    Dry-run: would fill 471 missing links, 1561 already-linked (kept), 0 unknown. Apply is GATED
    (queued: schumacher-relink-and-reenable.md).
---
 shopify/scripts/cost-handoff/schu-link-export.sh | 29 ++++++++
 shopify/scripts/cost-handoff/schu-link-ingest.js | 88 ++++++++++++++++++++++++
 shopify/scripts/cost-handoff/schu-link-push.sh   | 15 ++++
 3 files changed, 132 insertions(+)

diff --git a/shopify/scripts/cost-handoff/schu-link-export.sh b/shopify/scripts/cost-handoff/schu-link-export.sh
new file mode 100755
index 00000000..99c5bd2e
--- /dev/null
+++ b/shopify/scripts/cost-handoff/schu-link-export.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# Mac2 → export Schumacher dw_sku→shopify_product_id LINKS for handoff to Kamatera.
+# Companion to the cost handoff (DTD verdict B). Prod has Schumacher cost now but is missing
+# the shopify_product_id links for the ~700 products created from the Mac2 pipeline; without
+# them the prod net-new gate would let Full Monte DUPLICATE them. This ships the links up so
+# the prod net-new gate is accurate. shopify_product_id is normalized to bare numeric (the
+# catalog mixes bare-id and full gid://).
+#
+# Output: cost-handoff/out/schu-links-<UTCstamp>.csv  header: mfr_sku,shopify_product_id
+set -euo pipefail
+DIR="$(cd "$(dirname "$0")" && pwd)"
+OUT="$DIR/out"; mkdir -p "$OUT"
+STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
+FILE="$OUT/schu-links-$STAMP.csv"
+
+psql -d dw_unified -v ON_ERROR_STOP=1 -c "\copy (
+  SELECT mfr_sku,
+         regexp_replace(shopify_product_id::text, '.*/', '') AS shopify_product_id
+  FROM schumacher_catalog
+  WHERE mfr_sku IS NOT NULL AND mfr_sku<>''
+    AND shopify_product_id IS NOT NULL AND shopify_product_id<>''
+    AND regexp_replace(shopify_product_id::text, '.*/', '') ~ '^[0-9]+\$'
+  ORDER BY mfr_sku
+) TO '$FILE' WITH (FORMAT csv, HEADER true)"
+
+rows=$(($(wc -l < "$FILE") - 1))
+echo "$STAMP  exported $rows links → $FILE"
+ln -sf "$FILE" "$OUT/links-latest.csv"
+echo "$FILE"
diff --git a/shopify/scripts/cost-handoff/schu-link-ingest.js b/shopify/scripts/cost-handoff/schu-link-ingest.js
new file mode 100644
index 00000000..5690cb48
--- /dev/null
+++ b/shopify/scripts/cost-handoff/schu-link-ingest.js
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+/**
+ * KAMATERA PROD-SIDE LINK INGESTER (DTD verdict B twin of schu-cost-ingest).
+ * Deploy: /root/DW-Agents/cost-ingest/schu-link-ingest.js  ·  staging: ./link-staging/
+ *
+ * Reads newest link CSV (mfr_sku,shopify_product_id) pushed from Mac2, validates hard, and
+ * fills shopify_product_id ONLY where prod's is currently empty — so the prod net-new gate
+ * stops treating already-created Schumacher products as new (prevents Full Monte duplicating
+ * the ~700 made from the Mac2 pipeline). NEVER overwrites an existing prod link; never nulls.
+ *
+ * SAFETY: --dry is the DEFAULT (no write unless --commit). min rows, numeric pid only, sku
+ * must exist, max-fraction tripwire, one transaction, reject→quarantine on any error.
+ *
+ *   node schu-link-ingest.js            # DRY (default) — validate + report, no write
+ *   node schu-link-ingest.js --commit   # fill empty links transactionally
+ */
+const fs = require('fs');
+const path = require('path');
+const { Client } = require(fs.existsSync('/root/DW-Agents/node_modules/pg') ? '/root/DW-Agents/node_modules/pg' : 'pg');
+
+const ROOT = __dirname;
+const STAGE = path.join(ROOT, 'link-staging');
+const DONE = path.join(ROOT, 'link-applied');
+const REJECT = path.join(ROOT, 'link-rejected');
+[STAGE, DONE, REJECT].forEach(d => fs.mkdirSync(d, { recursive: true }));
+
+const DB_URL = process.env.DW_DB_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const COMMIT = process.argv.includes('--commit');     // default = DRY
+const MIN_ROWS = 20, MAX_FRACTION = 0.95;
+
+function parseCsv(txt) {
+  const lines = txt.split(/\r?\n/).filter(Boolean);
+  const hdr = lines.shift().split(',').map(s => s.trim().toLowerCase());
+  const iSku = hdr.indexOf('mfr_sku'), iPid = hdr.indexOf('shopify_product_id');
+  if (iSku < 0 || iPid < 0) throw new Error('bad header: expected mfr_sku,shopify_product_id');
+  return lines.map(l => { const c = l.split(','); return { sku: (c[iSku] || '').trim(), pid: (c[iPid] || '').trim() }; });
+}
+
+(async () => {
+  const files = fs.readdirSync(STAGE).filter(f => f.endsWith('.csv')).sort();
+  if (!files.length) { console.log(new Date().toISOString(), 'no staged link files'); return; }
+  const file = files[files.length - 1];
+  const fp = path.join(STAGE, file);
+  const rows = parseCsv(fs.readFileSync(fp, 'utf8'));
+  const reject = (why) => { if (COMMIT) fs.renameSync(fp, path.join(REJECT, file)); console.error(new Date().toISOString(), COMMIT ? 'REJECT' : 'DRY-FAIL', file, '-', why); process.exit(2); };
+
+  if (rows.length < MIN_ROWS) reject(`too few rows (${rows.length} < ${MIN_ROWS})`);
+  const valid = [];
+  for (const r of rows) {
+    if (!r.sku) reject('blank mfr_sku');
+    if (!/^[0-9]+$/.test(r.pid)) reject(`non-numeric shopify_product_id for ${r.sku}: ${r.pid}`);  // never null/garbage
+    valid.push(r);
+  }
+
+  const client = new Client({ connectionString: DB_URL });
+  await client.connect();
+  try {
+    const { rows: [{ total }] } = await client.query('SELECT count(*)::int total FROM schumacher_catalog');
+    if (valid.length > total * MAX_FRACTION) reject(`run would touch ${valid.length}/${total} (> ${MAX_FRACTION}) — tripwire`);
+
+    await client.query('BEGIN');
+    let filled = 0, alreadyset = 0, unknown = 0;
+    for (const r of valid) {
+      // fill ONLY where prod link is currently empty — never overwrite an existing prod link
+      const res = await client.query(
+        `UPDATE schumacher_catalog SET shopify_product_id=$1 WHERE mfr_sku=$2 AND coalesce(shopify_product_id::text,'')=''`,
+        [r.pid, r.sku]);
+      if (res.rowCount > 0) filled += res.rowCount;
+      else {
+        const chk = await client.query('SELECT shopify_product_id FROM schumacher_catalog WHERE mfr_sku=$1 LIMIT 1', [r.sku]);
+        if (!chk.rowCount) unknown++; else alreadyset++;   // sku missing, or already had a (kept) link
+      }
+    }
+    if (COMMIT) {
+      await client.query('COMMIT');
+      fs.renameSync(fp, path.join(DONE, file));
+      console.log(new Date().toISOString(), `APPLIED ${file}: filled ${filled}, already-linked ${alreadyset}, unknown-sku ${unknown}, of ${valid.length}`);
+    } else {
+      await client.query('ROLLBACK');
+      console.log(new Date().toISOString(), `DRY ${file}: WOULD fill ${filled}, already-linked ${alreadyset}, unknown-sku ${unknown}, of ${valid.length} (rolled back; file staged)`);
+    }
+  } catch (e) {
+    await client.query('ROLLBACK').catch(() => {});
+    if (COMMIT) fs.renameSync(fp, path.join(REJECT, file));
+    console.error(new Date().toISOString(), 'ROLLBACK', file, '-', e.message);
+    process.exit(1);
+  } finally { await client.end(); }
+})();
diff --git a/shopify/scripts/cost-handoff/schu-link-push.sh b/shopify/scripts/cost-handoff/schu-link-push.sh
new file mode 100755
index 00000000..0b0b995a
--- /dev/null
+++ b/shopify/scripts/cost-handoff/schu-link-push.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Mac2 → push the latest Schumacher LINK CSV up to Kamatera link-staging (DTD verdict B twin).
+set -euo pipefail
+DIR="$(cd "$(dirname "$0")" && pwd)"
+HOST="root@45.61.58.125"
+REMOTE_STAGE="/root/DW-Agents/cost-ingest/link-staging"
+
+FILE="$("$DIR/schu-link-export.sh" | tail -1)"
+[ -s "$FILE" ] || { echo "link export produced no file"; exit 1; }
+base="$(basename "$FILE")"
+
+ssh -o ConnectTimeout=15 "$HOST" "mkdir -p '$REMOTE_STAGE'"
+scp -o ConnectTimeout=15 "$FILE" "$HOST:$REMOTE_STAGE/$base.part"
+ssh -o ConnectTimeout=15 "$HOST" "mv '$REMOTE_STAGE/$base.part' '$REMOTE_STAGE/$base'"
+echo "pushed $base → $HOST:$REMOTE_STAGE/"

← 0de24336 Morning summary of overnight Kravet price audit (CNCP + noti  ·  back to Designer Wallcoverings  ·  cost-handoff: README documenting the DTD-B decoupled archite 7c4e1121 →