[object Object]

← back to Designer Wallcoverings

active-gate-add-samples: additive sample-variant pass for the missing-sample residue (idempotent, resumable, $4.25 + inv 2026)

1c17d27cfcb30e10930ef2b6dbdf9706e28203d7 · 2026-06-21 06:59:05 -0700 · Steve

Files touched

Diff

commit 1c17d27cfcb30e10930ef2b6dbdf9706e28203d7
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 21 06:59:05 2026 -0700

    active-gate-add-samples: additive sample-variant pass for the missing-sample residue (idempotent, resumable, $4.25 + inv 2026)
---
 shopify/scripts/active-gate-add-samples.js | 102 +++++++++++++++++++++++++++++
 1 file changed, 102 insertions(+)

diff --git a/shopify/scripts/active-gate-add-samples.js b/shopify/scripts/active-gate-add-samples.js
new file mode 100644
index 00000000..68b721ff
--- /dev/null
+++ b/shopify/scripts/active-gate-add-samples.js
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+/**
+ * active-gate-add-samples.js — ADDITIVE sample-variant pass for the active-gate
+ * residue (products the backfill flagged "missing sample variant").
+ *
+ * Standing rule: EVERY product must have a "Sample" variant @ $4.25 (CLAUDE.md).
+ * This adds the MISSING one — additive only. It never changes status, never
+ * touches the existing variant, and is idempotent (skips products that already
+ * have a Sample). Resumable via a jsonl ledger.
+ *
+ * Proven mechanic (from create-missing-sample-variants.js): REST
+ *   POST /products/<id>/variants.json {variant:{option1:'Sample',price:'4.25',sku:<base>-Sample,...}}
+ *
+ * USAGE:
+ *   node shopify/scripts/active-gate-add-samples.js --dry-run --limit 10
+ *   node shopify/scripts/active-gate-add-samples.js --limit 200
+ *   node shopify/scripts/active-gate-add-samples.js                # all missing-sample residue
+ */
+'use strict';
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const https = require('https');
+
+const args = process.argv.slice(2);
+const DRY = args.includes('--dry-run');
+const LIMIT = args.includes('--limit') ? parseInt(args[args.indexOf('--limit') + 1], 10) : Infinity;
+
+function loadToken() {
+  if (process.env.SHOPIFY_ADMIN_TOKEN) return process.env.SHOPIFY_ADMIN_TOKEN.replace(/['"]/g, '').trim();
+  for (const p of ['Projects/secrets-manager/.env', 'Projects/Designer-Wallcoverings/.env', 'Projects/Designer-Wallcoverings/shopify/.env']) {
+    try { const m = fs.readFileSync(path.join(os.homedir(), p), 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m); if (m && m[1].trim()) return m[1].replace(/['"]/g, '').trim(); } catch {}
+  }
+  return null;
+}
+const TOKEN = loadToken();
+if (!TOKEN) { console.error('FATAL: no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const LOCATION_GID = 'gid://shopify/Location/5795643504';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function rest(method, p, body) {
+  return new Promise(res => {
+    const payload = body ? JSON.stringify(body) : null;
+    const req = https.request({ hostname: STORE, path: `/admin/api/${API}${p}`, method,
+      headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}) } },
+      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { res({ status: r.statusCode, body: JSON.parse(d) }); } catch { res({ status: r.statusCode, body: {} }); } }); });
+    req.on('error', e => res({ status: 0, err: e.message })); if (payload) req.write(payload); req.end();
+  });
+}
+async function restR(method, p, body) { // throttle/retry
+  for (let a = 0; a < 6; a++) { const r = await rest(method, p, body); if (r.status === 429 || r.status === 0) { await sleep(2000 * (a + 1)); continue; } return r; }
+  return { status: 429, body: {} };
+}
+
+(async () => {
+  const RES = path.join(__dirname, 'data', 'active-gate', 'residue-all.jsonl');
+  const rows = fs.readFileSync(RES, 'utf8').trim().split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
+  // target rows whose remaining gate reasons include a missing sample variant
+  const targets = rows.filter(x => (x.remainingReasons || []).some(s => /sample variant/i.test(s)));
+  // dedupe by product id (gid)
+  const seen = new Set(); const queue0 = [];
+  for (const t of targets) { const id = t.id; if (id && !seen.has(id)) { seen.add(id); queue0.push(t); } }
+
+  const DATADIR = path.join(__dirname, 'data', 'active-gate');
+  const LEDGER = path.join(DATADIR, 'samples-ledger.jsonl');
+  const done = new Set();
+  if (fs.existsSync(LEDGER)) for (const l of fs.readFileSync(LEDGER, 'utf8').split('\n')) { if (!l.trim()) continue; try { done.add(JSON.parse(l).id); } catch {} }
+  const queue = queue0.filter(t => !done.has(t.id)).slice(0, LIMIT === Infinity ? undefined : LIMIT);
+  console.log(`add-samples ${DRY ? '[DRY-RUN] ' : ''}missing-sample residue=${queue0.length} done=${done.size} thisRun=${queue.length}`);
+  if (!queue.length) { console.log('Nothing to do.'); return; }
+
+  const stats = { created: 0, already: 0, skipNoSku: 0, failed: 0 };
+  for (let i = 0; i < queue.length; i++) {
+    const t = queue[i];
+    const numId = String(t.id).replace(/^.*\/Product\//, '');
+    // fetch product variants to get base SKU + check for an existing Sample
+    const pr = await restR('GET', `/products/${numId}.json?fields=id,title,variants`);
+    const prod = pr.body && pr.body.product;
+    if (!prod) { stats.failed++; continue; }
+    const variants = prod.variants || [];
+    if (variants.some(v => /sample/i.test(v.title || '') || /-sample$/i.test(v.sku || ''))) {
+      stats.already++; fs.appendFileSync(LEDGER, JSON.stringify({ id: t.id, result: 'already-has-sample', ts: new Date().toISOString() }) + '\n'); continue;
+    }
+    const base = (variants[0] && variants[0].sku || '').trim();
+    const sampleSku = base ? `${base}-Sample` : `SAMPLE-${numId}`;
+    if (DRY) { console.log(`  • ${(prod.title || '').slice(0, 50)} → ${sampleSku}`); stats.created++; continue; }
+    const r = await restR('POST', `/products/${numId}/variants.json`, { variant: { option1: 'Sample', price: '4.25', sku: sampleSku, weight: 0, weight_unit: 'lb', inventory_management: 'shopify', requires_shipping: true } });
+    if (r.status === 201 || r.status === 200) {
+      stats.created++;
+      const inv = r.body.variant && r.body.variant.inventory_item_id;
+      if (inv) await restR('POST', `/inventory_levels/set.json`, { location_id: LOCATION_GID.replace(/^.*\/Location\//, ''), inventory_item_id: inv, available: 2026 });
+      fs.appendFileSync(LEDGER, JSON.stringify({ id: t.id, sampleSku, result: 'created', ts: new Date().toISOString() }) + '\n');
+      if (stats.created % 25 === 0) console.log(`  [${i + 1}/${queue.length}] ✓ created ${stats.created} (last ${sampleSku})`);
+    } else if (r.status === 422 && JSON.stringify(r.body).toLowerCase().includes('already')) {
+      stats.already++; fs.appendFileSync(LEDGER, JSON.stringify({ id: t.id, result: 'already', ts: new Date().toISOString() }) + '\n');
+    } else { stats.failed++; if (stats.failed <= 5) console.log(`  ! ${numId}: ${r.status} ${JSON.stringify(r.body).slice(0, 100)}`); }
+  }
+  console.log(`\n=== ${DRY ? 'DRY-RUN ' : ''}SUMMARY ===`); console.log(JSON.stringify(stats, null, 2));
+  if (!DRY) console.log(`ledger → ${LEDGER}`);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });

← 63dcdeee Newwall width re-crawl: public-HTML spec-table scraper (DTD-  ·  back to Designer Wallcoverings  ·  validate-before-activate: fix sample-variant false-positive e69ef2ba →