← back to Designer Wallcoverings
Add fix-broken-rolls.js — guardrailed live-price fixer (dry-run default)
7cc6bdfcc41e2f43f4d27c48593901c82b457251 · 2026-06-11 13:07:13 -0700 · SteveStudio2
Confirmed-cost vendors only; corrects $4.25 non-sample rolls to cost/0.65/0.85 (MAP-floored).
Sanity gate, price_fix_audit (old_price kept for rollback), velocity cap --limit, kill switch
~/.dw-fixer-stop. Dry-run batch: 197 fixable (Schumacher 196, GB 1). --commit pushes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A shopify/scripts/cadence/fix-broken-rolls.js
Diff
commit 7cc6bdfcc41e2f43f4d27c48593901c82b457251
Author: SteveStudio2 <stevestudio2@SteveStudio2s-Mac-Studio.local>
Date: Thu Jun 11 13:07:13 2026 -0700
Add fix-broken-rolls.js — guardrailed live-price fixer (dry-run default)
Confirmed-cost vendors only; corrects $4.25 non-sample rolls to cost/0.65/0.85 (MAP-floored).
Sanity gate, price_fix_audit (old_price kept for rollback), velocity cap --limit, kill switch
~/.dw-fixer-stop. Dry-run batch: 197 fixable (Schumacher 196, GB 1). --commit pushes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
shopify/scripts/cadence/fix-broken-rolls.js | 158 ++++++++++++++++++++++++++++
1 file changed, 158 insertions(+)
diff --git a/shopify/scripts/cadence/fix-broken-rolls.js b/shopify/scripts/cadence/fix-broken-rolls.js
new file mode 100644
index 00000000..9e01a151
--- /dev/null
+++ b/shopify/scripts/cadence/fix-broken-rolls.js
@@ -0,0 +1,158 @@
+#!/usr/bin/env node
+/**
+ * fix-broken-rolls.js — correct LIVE Shopify roll variants stuck at the $4.25 sample price,
+ * for CONFIRMED-COST vendors only.
+ *
+ * Steve authorized agents to auto-push confirmed-cost fixes (2026-06-11) WITH guardrails.
+ * This is the executor. Default DRY-RUN (computes + shows + writes a batch file, NO Shopify
+ * writes). --commit pushes via productVariantsBulkUpdate.
+ *
+ * CONFIRMED-COST FLOOR: only vendors in vendors.js (verified costExpr + discount_confirmed).
+ * A roll is fixable only if its mfr_sku matches a catalog row with cost>0. Cost from the
+ * vendor's costExpr; retail = round(cost/0.65/0.85, 2), floored at MAP where we have it.
+ *
+ * GUARDRAILS:
+ * - sanity gate: SANE_MIN..SANE_MAX and proposed > current; anything else is SKIPPED (staged)
+ * - reversibility: every change recorded in price_fix_audit with old_price (restore = re-push old)
+ * - velocity cap: --limit N (default 50) per run
+ * - kill switch: presence of ~/.dw-fixer-stop aborts immediately
+ *
+ * Usage:
+ * node fix-broken-rolls.js # DRY-RUN, all confirmed vendors
+ * node fix-broken-rolls.js --vendor Schumacher # one vendor
+ * node fix-broken-rolls.js --limit 25 --commit # LIVE push, capped at 25
+ */
+const https = require('https');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const VENDORS = require('./vendors.js');
+
+const args = process.argv.slice(2);
+const flag = n => args.includes(n);
+const val = (n, d) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : d; };
+const COMMIT = flag('--commit');
+const ONLY = val('--vendor', null);
+const LIMIT = parseInt(val('--limit', '50'), 10);
+
+const SANE_MIN = 5, SANE_MAX = 50000, SAMPLE = 4.25;
+const KILL = path.join(os.homedir(), '.dw-fixer-stop');
+// vendors where we loaded a MAP-floored sell column — floor the proposed price at it
+const SELL_COL = { 'Kravet': 'dw_sell_price', 'Graham & Brown': 'dw_retail_price' };
+
+const RETAIL = c => Math.round((c / 0.65 / 0.85) * 100) / 100;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const psql = sql => execFileSync('psql', ['-d', 'dw_unified', '-At', '-F', '\t', '-c', sql],
+ { encoding: 'utf8', maxBuffer: 1 << 28 }).trim();
+
+// ---- shopify ----
+const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+function gql(query, variables) {
+ return new Promise(res => {
+ const data = JSON.stringify({ query, variables });
+ const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
+ r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { res({ status: r.statusCode, json: JSON.parse(d) }); } catch { res({ status: r.statusCode, raw: d.slice(0, 400) }); } }); });
+ req.on('error', e => res({ status: 0, err: e.message }));
+ req.write(data); req.end();
+ });
+}
+async function gqlRetry(q, v) {
+ for (let a = 0; a < 5; a++) {
+ const r = await gql(q, v);
+ const t = r.json && r.json.errors && JSON.stringify(r.json.errors).includes('THROTTLED');
+ if (r.status === 429 || t) { await sleep(2000 * (a + 1)); continue; }
+ await sleep(550); return r;
+ }
+ return { status: 429, raw: 'throttled' };
+}
+const MUT = `mutation($pid: ID!, $vid: ID!, $price: Money!) {
+ productVariantsBulkUpdate(productId: $pid, variants: [{ id: $vid, price: $price }]) {
+ productVariants { id price } userErrors { field message }
+ }
+}`;
+
+function ensureAudit() {
+ psql(`CREATE TABLE IF NOT EXISTS price_fix_audit (
+ id serial PRIMARY KEY, vendor text, mfr_sku text, variant_id text, shopify_id text,
+ old_price numeric, new_price numeric, cost numeric, committed boolean, ts timestamptz DEFAULT now());`);
+}
+
+// pull fixable broken rolls for one confirmed vendor
+function fixableRows(vendor, cfg) {
+ const ce = cfg.costExpr;
+ const sellCol = SELL_COL[vendor];
+ const sellSel = sellCol ? `, ${sellCol} AS sell` : `, NULL::numeric AS sell`;
+ const sql = `
+ WITH costed AS (SELECT mfr_sku, (${ce}) AS cost ${sellSel} FROM ${cfg.table} WHERE (${ce}) > 0)
+ SELECT s.variant_id, s.shopify_id, s.mfr_sku, COALESCE(s.title,''), s.price::numeric, c.cost, COALESCE(c.sell,0)
+ FROM shopify_products s
+ JOIN costed c ON upper(trim(c.mfr_sku)) = upper(trim(s.mfr_sku))
+ WHERE s.price::numeric = ${SAMPLE}
+ AND (s.variant_sku IS NULL OR s.variant_sku NOT ILIKE '%sample%')
+ AND s.variant_id IS NOT NULL AND s.shopify_id IS NOT NULL`;
+ const out = psql(sql);
+ if (!out) return [];
+ return out.split('\n').map(line => {
+ const [variant_id, shopify_id, mfr_sku, title, old, cost, sell] = line.split('\t');
+ const c = parseFloat(cost);
+ const proposed = Math.max(RETAIL(c), parseFloat(sell) || 0);
+ return { vendor, variant_id, shopify_id, mfr_sku, title, old: parseFloat(old), cost: c, proposed };
+ });
+}
+
+(async () => {
+ if (COMMIT && fs.existsSync(KILL)) { console.log(`KILL SWITCH present (${KILL}) — aborting.`); return; }
+ ensureAudit();
+
+ const vendors = Object.entries(VENDORS).filter(([v]) => !ONLY || v === ONLY);
+ let batch = [], skipped = [];
+ for (const [v, cfg] of vendors) {
+ let rows;
+ try { rows = fixableRows(v, cfg); } catch (e) { console.log(` ${v}: query error ${e.message.slice(0,120)}`); continue; }
+ for (const r of rows) {
+ // sanity gate
+ if (!(r.proposed > r.old) || r.proposed < SANE_MIN || r.proposed > SANE_MAX || !isFinite(r.proposed)) {
+ skipped.push(r); continue;
+ }
+ batch.push(r);
+ }
+ }
+ batch.sort((a, b) => b.proposed - a.proposed);
+
+ console.log(`\nfixable confirmed-cost broken rolls: ${batch.length} (sanity-skipped: ${skipped.length})`);
+ const byV = {};
+ batch.forEach(r => { byV[r.vendor] = (byV[r.vendor] || 0) + 1; });
+ Object.entries(byV).forEach(([v, n]) => console.log(` ${v}: ${n}`));
+ console.log('\nsample of proposed fixes:');
+ batch.slice(0, 8).forEach(r => console.log(` [${r.vendor}] ${r.mfr_sku} $${r.old} -> $${r.proposed} (cost $${r.cost}) ${r.title.slice(0,40)}`));
+
+ // write the full proposed batch for review
+ const outDir = path.join(__dirname, 'data'); fs.mkdirSync(outDir, { recursive: true });
+ const outFile = path.join(outDir, 'broken-roll-fixes.json');
+ fs.writeFileSync(outFile, JSON.stringify({ generatedFor: COMMIT ? 'commit' : 'dry-run', total: batch.length, batch, skipped }, null, 1));
+ console.log(`\nfull batch -> ${outFile}`);
+
+ if (!COMMIT) {
+ console.log(`\nDRY-RUN — no Shopify writes. Re-run with --commit (--limit ${LIMIT}) to push.`);
+ return;
+ }
+
+ // LIVE push, capped
+ const todo = batch.slice(0, LIMIT);
+ console.log(`\nLIVE: pushing ${todo.length} of ${batch.length} (velocity cap --limit ${LIMIT})...`);
+ let ok = 0, fail = 0;
+ for (const r of todo) {
+ if (fs.existsSync(KILL)) { console.log('KILL SWITCH — stopping.'); break; }
+ const res = await gqlRetry(MUT, { pid: r.shopify_id, vid: r.variant_id, price: r.proposed.toFixed(2) });
+ const ue = res.json && res.json.data && res.json.data.productVariantsBulkUpdate && res.json.data.productVariantsBulkUpdate.userErrors;
+ const good = res.status === 200 && ue && ue.length === 0;
+ psql(`INSERT INTO price_fix_audit (vendor,mfr_sku,variant_id,shopify_id,old_price,new_price,cost,committed)
+ VALUES ('${r.vendor.replace(/'/g,"''")}','${(r.mfr_sku||'').replace(/'/g,"''")}','${r.variant_id}','${r.shopify_id}',${r.old},${r.proposed},${r.cost},${good});`);
+ if (good) { ok++; } else { fail++; console.log(` ✗ ${r.mfr_sku} ${JSON.stringify(ue || res.raw || res.err).slice(0,140)}`); }
+ }
+ console.log(`\nDONE: ${ok} fixed, ${fail} failed. Audit in dw_unified.price_fix_audit (old_price kept for rollback).`);
+})();
← c3d80f3d Sweep: fix flushInv fallback (set-per-item before activate)
·
back to Designer Wallcoverings
·
Fix accented-title casing (Unicode \p{L}) + re-title 46 Schu ac43bae8 →