[object Object]

← 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

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 →