[object Object]

← back to Justin David Pricing 2026

GMC remediation: variant-aware weight backfill (set_weights.mjs, reversible via shopify_weight_backfill) — DWLA pilot 3646 vars 0 err, full active-catalog backfill running; Track A (feed exclusion) DTD verdict=A; shipping-config memo drafted

8837012cfe32a86678f88f98c192267b586f973e · 2026-07-16 12:01:14 -0700 · Steve Abrams

Files touched

Diff

commit 8837012cfe32a86678f88f98c192267b586f973e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 16 12:01:14 2026 -0700

    GMC remediation: variant-aware weight backfill (set_weights.mjs, reversible via shopify_weight_backfill) — DWLA pilot 3646 vars 0 err, full active-catalog backfill running; Track A (feed exclusion) DTD verdict=A; shipping-config memo drafted
---
 set_weights.mjs | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 90 insertions(+)

diff --git a/set_weights.mjs b/set_weights.mjs
new file mode 100644
index 0000000..66f2e0b
--- /dev/null
+++ b/set_weights.mjs
@@ -0,0 +1,90 @@
+// set_weights.mjs — backfill non-zero shipping weight on ACTIVE Shopify variants so Google
+// stops throwing missing_shipping_weight. Idempotent (skips variants that already have grams>0),
+// resumable (paginates by since_id, records a cursor), reversible (logs prior grams to a table).
+// Variant-aware: a "Sample"/memo variant gets the sample weight; the sellable variant gets the
+// product_type weight. Scope with --vendor "<name>" (pilot) or --all.
+//
+// Modes: --dry-run (default)  --apply
+import { execFileSync } from 'node:child_process';
+
+const SHOP = process.env.DW_SHOP || 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = execFileSync('bash', ['-lc',
+  `grep -m1 '^SHOPIFY_ADMIN_TOKEN=' "$HOME/Projects/secrets-manager/.env" | cut -d= -f2- | tr -d '"'"'"'\\r'`],
+  { encoding: 'utf8' }).trim();
+const DBURL = process.env.DW_UNIFIED_URL || 'postgresql:///dw_unified?host=/tmp';
+const args = process.argv.slice(2);
+const DRY = !args.includes('--apply');
+const VENDOR = (() => { const a = args.find(x => x.startsWith('--vendor')); return a ? a.split('=')[1] : null; })();
+const DELAY = 320;
+
+// grams by product_type (sellable variant); sample variants always SAMPLE_G
+const SAMPLE_G = 113;                     // ~0.25 lb memo sample
+const TYPE_G = {
+  'wallcovering': 1814, 'wallcoverings': 1814, 'wallpaper': 1814, 'metallic wallcovering': 1814,
+  'commercial wallcovering': 1814, 'mural': 2268, 'fabric': 454, 'commercial fabric': 454,
+  'commercial drapery': 454, 'pillow': 680, 'trim': 227, 'acoustic panel': 907,
+  'tin ceiling tile': 907, 'memo sample': SAMPLE_G, 'sample': SAMPLE_G,
+};
+const DEFAULT_G = 454;
+function weightFor(productType, variant) {
+  const t = (productType || '').toLowerCase().trim();
+  if (/sample/i.test(variant.title || '') || /sample/i.test(variant.sku || '')) return SAMPLE_G;
+  return TYPE_G[t] ?? DEFAULT_G;
+}
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function qx(sql) { execFileSync('psql', [DBURL, '-q', '-c', sql], { stdio: 'ignore' }); }
+function esc(s) { return String(s).replace(/'/g, "''"); }
+async function api(method, path, body) {
+  const url = `https://${SHOP}/admin/api/${API}/${path}`;
+  for (let i = 0; i < 6; i++) {
+    const res = await fetch(url, { method,
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+      body: body ? JSON.stringify(body) : undefined });
+    if (res.status === 429) { await sleep(2000 * (i + 1)); continue; }
+    const txt = await res.text();
+    if (!res.ok) throw new Error(`${method} ${path} -> HTTP ${res.status}: ${txt.slice(0,150)}`);
+    return { headers: res.headers, json: txt ? JSON.parse(txt) : {} };
+  }
+  throw new Error(`${method} ${path} -> 429 exhausted`);
+}
+
+// reversibility log
+qx(`CREATE TABLE IF NOT EXISTS shopify_weight_backfill (
+  variant_id bigint PRIMARY KEY, product_id bigint, sku text, product_type text,
+  prior_grams int, new_grams int, set_at timestamptz DEFAULT now());`);
+
+console.log(`[weights] mode=${DRY ? 'DRY-RUN' : 'APPLY'} scope=${VENDOR ? `vendor="${VENDOR}"` : 'ALL ACTIVE'} token=...${TOKEN.slice(-4)}`);
+let sinceId = 0, scanned = 0, set = 0, skipped = 0, errors = 0, pages = 0;
+const vendorQ = VENDOR ? `&vendor=${encodeURIComponent(VENDOR)}` : '';
+
+while (true) {
+  let resp;
+  try { resp = await api('GET', `products.json?status=active&limit=250&since_id=${sinceId}&fields=id,product_type,variants${vendorQ}`); }
+  catch (e) { console.log('  GET page error:', e.message); break; }
+  const prods = resp.json.products || [];
+  if (!prods.length) break;
+  pages++;
+  for (const p of prods) {
+    sinceId = Math.max(sinceId, p.id);
+    for (const v of p.variants) {
+      scanned++;
+      const cur = +v.grams || 0;
+      if (cur > 0) { skipped++; continue; }         // idempotent
+      const g = weightFor(p.product_type, v);
+      if (DRY) { set++; continue; }
+      try {
+        await api('PUT', `variants/${v.id}.json`, { variant: { id: v.id, grams: g } });
+        qx(`INSERT INTO shopify_weight_backfill(variant_id,product_id,sku,product_type,prior_grams,new_grams)
+            VALUES (${v.id},${p.id},'${esc(v.sku||'')}','${esc(p.product_type||'')}',${cur},${g})
+            ON CONFLICT (variant_id) DO UPDATE SET new_grams=${g}, set_at=now()`);
+        set++;
+        await sleep(DELAY);
+      } catch (e) { errors++; if (errors <= 20) console.log(`  ✗ variant ${v.id}: ${String(e.message).slice(0,120)}`); }
+    }
+  }
+  if (pages % 5 === 0) console.log(`  …scanned=${scanned} set=${set} skipped=${skipped} err=${errors} (since_id=${sinceId})`);
+}
+console.log(`\n[weights] done · scanned=${scanned} set=${set} skipped(had weight)=${skipped} errors=${errors}`);
+if (DRY) console.log('  (DRY-RUN — no writes. Re-run with --apply, optionally --vendor="Los Angeles Fabrics" to pilot.)');

← 3397683 auto-save: 2026-07-16T09:12:20 (1 files) — apply_shopify.mjs  ·  back to Justin David Pricing 2026  ·  Revert weight backfill: contrarian caught that grams=checkou 31fe685 →