← 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
A shopify/scripts/cost-handoff/schu-link-export.shA shopify/scripts/cost-handoff/schu-link-ingest.jsA shopify/scripts/cost-handoff/schu-link-push.sh
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 →