← 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 →