← back to Designer Wallcoverings
Add dry-run-first fix scripts for active-last-week 2-variant review (41 missing-sample + 434 only-sample)
fb9148137dc391c8a8adbf31bf389ade9192f4f9 · 2026-06-26 23:35:30 -0700 · Steve
Files touched
A shopify/scripts/fix-active-lastweek-only-sample.jsA shopify/scripts/fix-active-lastweek-samples.js
Diff
commit fb9148137dc391c8a8adbf31bf389ade9192f4f9
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jun 26 23:35:30 2026 -0700
Add dry-run-first fix scripts for active-last-week 2-variant review (41 missing-sample + 434 only-sample)
---
shopify/scripts/fix-active-lastweek-only-sample.js | 86 +++++++++++++++++
shopify/scripts/fix-active-lastweek-samples.js | 102 +++++++++++++++++++++
2 files changed, 188 insertions(+)
diff --git a/shopify/scripts/fix-active-lastweek-only-sample.js b/shopify/scripts/fix-active-lastweek-only-sample.js
new file mode 100644
index 00000000..38b118a0
--- /dev/null
+++ b/shopify/scripts/fix-active-lastweek-only-sample.js
@@ -0,0 +1,86 @@
+#!/usr/bin/env node
+/**
+ * fix-active-lastweek-only-sample.js
+ * Remediation for the "only-sample, no sellable roll" cohort from the 2026-06-26
+ * active-last-week 2-variant review (434 products across Phillipe Romano,
+ * Quadrille, Alan Campbell, Suncloth, Plains, Charles Burger, Home Couture,
+ * Cloth and Paper, Fentucci). These went ACTIVE+published with ONLY a Sample
+ * variant — a customer sees the product page but cannot buy the roll/yardage.
+ *
+ * Per Steve's HARD activation gate, a SKU must NOT be active without a sellable
+ * product variant priced off CONFIRMED vendor cost. We must NOT fabricate a roll
+ * price. The gate-compliant, reversible default is therefore: revert to DRAFT
+ * (status=draft) until purchasing confirms cost and the roll variant is built,
+ * after which cadence re-activates it normally.
+ *
+ * The gap snapshot already classifies 91 of these as fix_type='build-roll' +
+ * priceable=FALSE (no catalog cost). The remaining 343 weren't in the snapshot
+ * (too recent) and need a fresh cost check before any build-roll path.
+ *
+ * SAFE BY DEFAULT — dry-run unless --apply. status flip is fully reversible.
+ * node shopify/scripts/fix-active-lastweek-only-sample.js # dry-run
+ * node shopify/scripts/fix-active-lastweek-only-sample.js --apply # GATED: set DRAFT
+ */
+const fs = require('fs');
+const path = require('path');
+
+const APPLY = process.argv.includes('--apply');
+const TSV = process.env.TSV || '/tmp/dw_only_sample_no_product.tsv';
+const LEDGER = path.join(__dirname, 'data', 'fix-active-lastweek-only-sample.ledger.jsonl');
+
+const env = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8');
+const ev = k => (env.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1];
+const STORE = ev('SHOPIFY_STORE_DOMAIN') || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = ev('SHOPIFY_ADMIN_TOKEN');
+const REST = `https://${STORE}/admin/api/2024-10`;
+
+async function rest(method, p, body) {
+ const r = await fetch(REST + p, {
+ method,
+ headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ return { status: r.status, json: await r.json().catch(() => ({})) };
+}
+
+function loadTargets() {
+ return fs.readFileSync(TSV, 'utf8').trim().split('\n').slice(1).map(l => {
+ const [pid, vendor, , , title] = l.split('\t');
+ return { pid, vendor, title: (title || '').replace(/^"|"$/g, '') };
+ });
+}
+
+(async () => {
+ if (!TOKEN) { console.error('No SHOPIFY_ADMIN_TOKEN in .env'); process.exit(1); }
+ fs.mkdirSync(path.dirname(LEDGER), { recursive: true });
+ const done = new Set(
+ fs.existsSync(LEDGER)
+ ? fs.readFileSync(LEDGER, 'utf8').trim().split('\n').filter(Boolean)
+ .map(l => JSON.parse(l)).filter(x => x.result === 'drafted').map(x => x.pid)
+ : []
+ );
+ const targets = loadTargets().filter(t => !done.has(t.pid));
+ console.log(`fix-active-lastweek-only-sample ${APPLY ? '[APPLY → DRAFT]' : '[DRY-RUN]'} targets=${targets.length}`);
+ const byV = {};
+ const stats = { drafted: 0, err: 0 };
+ for (let i = 0; i < targets.length; i++) {
+ const t = targets[i];
+ byV[t.vendor] = (byV[t.vendor] || 0) + 1;
+ if (!APPLY) { continue; }
+ const r = await rest('PUT', `/products/${t.pid}.json`, { product: { id: Number(t.pid), status: 'draft' } });
+ if (r.status >= 200 && r.status < 300) {
+ stats.drafted++;
+ fs.appendFileSync(LEDGER, JSON.stringify({ pid: t.pid, vendor: t.vendor, result: 'drafted', ts: new Date().toISOString() }) + '\n');
+ if (stats.drafted % 25 === 0) console.log(` [${i + 1}/${targets.length}] drafted ${stats.drafted}`);
+ } else {
+ stats.err++;
+ fs.appendFileSync(LEDGER, JSON.stringify({ pid: t.pid, result: 'error', status: r.status, ts: new Date().toISOString() }) + '\n');
+ }
+ }
+ if (!APPLY) {
+ console.log('would set status=DRAFT for these vendors:', JSON.stringify(byV));
+ console.log('(re-activation happens via cadence once roll variant + confirmed cost land)');
+ } else {
+ console.log(`done: drafted=${stats.drafted} err=${stats.err}`);
+ }
+})();
diff --git a/shopify/scripts/fix-active-lastweek-samples.js b/shopify/scripts/fix-active-lastweek-samples.js
new file mode 100644
index 00000000..0207ea51
--- /dev/null
+++ b/shopify/scripts/fix-active-lastweek-samples.js
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+/**
+ * fix-active-lastweek-samples.js
+ * ADDITIVE sample-variant pass for the "missing-sample" cohort surfaced by the
+ * 2026-06-26 active-last-week 2-variant review (41 products: 39 Zoffany + 2 PR).
+ * Each target already has a real, priced roll variant on the live store and is
+ * MISSING only its Sample variant — so this is purely additive (never edits the
+ * existing roll variant, never changes price/status).
+ *
+ * Convention mirrors active-gate-add-samples.js (the proven mechanic):
+ * POST /products/<id>/variants.json {variant:{option1:'Sample',price:<samplePrice>,sku:<base>-Sample,...}}
+ *
+ * Sample price defaults to $4.25 (DW standard memo-sample). Vendor overrides via
+ * SAMPLE_PRICE_BY_VENDOR below — confirm with purchasing before --apply if a line
+ * prices its memo differently.
+ *
+ * SAFE BY DEFAULT — dry-run unless --apply is passed. Resumable via ledger.
+ * node shopify/scripts/fix-active-lastweek-samples.js # dry-run
+ * node shopify/scripts/fix-active-lastweek-samples.js --apply # GATED: execute
+ */
+const fs = require('fs');
+const path = require('path');
+
+const APPLY = process.argv.includes('--apply');
+const TSV = process.env.TSV || '/tmp/dw_missing_sample.tsv';
+const LEDGER = path.join(__dirname, 'data', 'fix-active-lastweek-samples.ledger.jsonl');
+
+// env-first token (never hardcode)
+const env = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8');
+const ev = k => (env.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1];
+const STORE = ev('SHOPIFY_STORE_DOMAIN') || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = ev('SHOPIFY_ADMIN_TOKEN');
+const REST = `https://${STORE}/admin/api/2024-10`;
+
+const DEFAULT_SAMPLE_PRICE = '4.25';
+const SAMPLE_PRICE_BY_VENDOR = {
+ // 'Zoffany': '4.25', // confirm w/ purchasing if Style Library memo differs
+};
+
+async function rest(method, p, body) {
+ const r = await fetch(REST + p, {
+ method,
+ headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ return { status: r.status, json: await r.json().catch(() => ({})) };
+}
+
+function loadTargets() {
+ const lines = fs.readFileSync(TSV, 'utf8').trim().split('\n').slice(1);
+ return lines.map(l => {
+ const [pid, vendor, , , title] = l.split('\t');
+ return { pid, vendor, title: (title || '').replace(/^"|"$/g, '') };
+ });
+}
+
+(async () => {
+ if (!TOKEN) { console.error('No SHOPIFY_ADMIN_TOKEN in .env'); process.exit(1); }
+ fs.mkdirSync(path.dirname(LEDGER), { recursive: true });
+ const done = new Set(
+ fs.existsSync(LEDGER)
+ ? fs.readFileSync(LEDGER, 'utf8').trim().split('\n').filter(Boolean)
+ .map(l => JSON.parse(l)).filter(x => x.result === 'created' || x.result === 'already').map(x => x.pid)
+ : []
+ );
+ const targets = loadTargets().filter(t => !done.has(t.pid));
+ console.log(`fix-active-lastweek-samples ${APPLY ? '[APPLY]' : '[DRY-RUN]'} targets=${targets.length}`);
+ const stats = { created: 0, already: 0, err: 0 };
+
+ for (let i = 0; i < targets.length; i++) {
+ const t = targets[i];
+ // pull live variants to derive base sku + guard against an existing sample
+ const { json } = await rest('GET', `/products/${t.pid}/variants.json`);
+ const vs = json.variants || [];
+ if (!vs.length) { console.log(` ! ${t.pid} no variants (skip)`); stats.err++; continue; }
+ if (vs.some(v => /sample/i.test(v.title || '') || /-sample$/i.test(v.sku || ''))) {
+ stats.already++;
+ fs.appendFileSync(LEDGER, JSON.stringify({ pid: t.pid, result: 'already', ts: new Date().toISOString() }) + '\n');
+ continue;
+ }
+ const base = (vs[0].sku || '').replace(/-sample$/i, '');
+ const sampleSku = base ? `${base}-Sample` : `SAMPLE-${t.pid}`;
+ const price = SAMPLE_PRICE_BY_VENDOR[t.vendor] || DEFAULT_SAMPLE_PRICE;
+ if (!APPLY) {
+ console.log(` • [${t.vendor}] ${t.title.slice(0, 48)} → +Sample ${sampleSku} @ $${price}`);
+ stats.created++; continue;
+ }
+ const r = await rest('POST', `/products/${t.pid}/variants.json`, {
+ variant: { option1: 'Sample', price, sku: sampleSku, weight: 0, weight_unit: 'lb', inventory_management: 'shopify', requires_shipping: true },
+ });
+ if (r.status >= 200 && r.status < 300) {
+ stats.created++;
+ fs.appendFileSync(LEDGER, JSON.stringify({ pid: t.pid, sampleSku, price, result: 'created', ts: new Date().toISOString() }) + '\n');
+ if (stats.created % 10 === 0) console.log(` [${i + 1}/${targets.length}] ✓ ${stats.created} (last ${sampleSku})`);
+ } else {
+ stats.err++;
+ fs.appendFileSync(LEDGER, JSON.stringify({ pid: t.pid, result: 'error', status: r.status, body: r.json, ts: new Date().toISOString() }) + '\n');
+ console.log(` ✗ ${t.pid} HTTP ${r.status}`);
+ }
+ }
+ console.log(`done: created=${stats.created} already=${stats.already} err=${stats.err}`);
+})();
← 2afc375b auto-save: 2026-06-26T17:04:47 (3 files) — pending-approval/
·
back to Designer Wallcoverings
·
Add contact-for-price PDP section + additive tagging script 0b2efa54 →