[object Object]

← back to Tk10895 GroupB

TK-10895 Group B reprice: fabric-row cost -> mill WP-row cost, with rollback

44e3debf77984f177c33e0a77c2b3d35d061bc53 · 2026-09-10 11:16:04 -0700 · Steve

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 44e3debf77984f177c33e0a77c2b3d35d061bc53
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Sep 10 11:16:04 2026 -0700

    TK-10895 Group B reprice: fabric-row cost -> mill WP-row cost, with rollback
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 reprice.mjs  | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 rollback.mjs | 54 ++++++++++++++++++++++++++++++++++++
 2 files changed, 144 insertions(+)

diff --git a/reprice.mjs b/reprice.mjs
new file mode 100755
index 0000000..fe5e18d
--- /dev/null
+++ b/reprice.mjs
@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+// TK-10895 Group B reprice — 5 wallpapers that joined to the mill's FABRIC row instead of its WP row.
+// DTD verdict C (unanimous, 6/6 valid votes, 2026-09-10). Steve ungated 2026-09-10.
+//
+// This is a JOIN CORRECTION, not a price guess: each product is a 27in/26in WALLPAPER whose cost
+// was taken from the mill sheet's FABRIC row for the same pattern. The replacement is the mill's
+// OWN published WP-row cost. Proof the join is wrong: mfr 709221 is labelled "Bijou Stripe FABRIC",
+// typed Wallpaper at 27in, and carries $183 — identical to mfr 901622, the genuine "Bijou Stripe
+// FABRIC" row typed Fabric at 54in.
+//
+// Dry-run by default. --apply to write. Rollback map is written BEFORE any write.
+import fs from 'node:fs';
+
+const TOK = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8')
+  .split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN='))
+  ?.split('=').slice(1).join('=').trim();
+if (!TOK) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const APPLY = process.argv.includes('--apply');
+const EV = process.env.HOME + '/.claude/yolo-queue/evidence/TK-10895';
+
+// mfr -> the mill's OWN published WP-row cost (NOT an inference; the FABRIC row was the wrong join)
+const T = {
+  '708421': { wp: 145, row: '8250-05WP', pat: 'Barbados Batik' },
+  '708521': { wp: 145, row: '8250-05WP', pat: 'Barbados Batik' },
+  '709221': { wp: 148, row: '5060-05WP', pat: 'Bijou Stripe' },
+  '709321': { wp: 142, row: '5050-04WP', pat: 'Birds II' },
+  '709421': { wp: 142, row: '5050-04WP', pat: 'Birds II' },
+};
+const retail = c => Math.round((c / 0.65 / 0.85) * 100) / 100;
+
+const wl = JSON.parse(fs.readFileSync(EV + '/trancheB-worklist.json', 'utf8'));
+const byMfr = Object.fromEntries(wl.map(r => [r.mfr, r]));
+
+async function api(path, opt = {}) {
+  const r = await fetch(`https://${SHOP}/admin/api/${API}/${path}`, {
+    ...opt,
+    headers: { 'X-Shopify-Access-Token': TOK, 'Content-Type': 'application/json', ...(opt.headers || {}) },
+  });
+  if (!r.ok) throw new Error(`${r.status} ${path} :: ${(await r.text()).slice(0, 200)}`);
+  return r.json();
+}
+
+const map = [];
+for (const [mfr, t] of Object.entries(T)) {
+  const pid = byMfr[mfr]?.pid;
+  if (!pid) { console.log(`  ${mfr} NOT IN WORKLIST — skip`); continue; }
+  const p = (await api(`products/${pid}.json?fields=id,handle,variants`)).product;
+  const v = p.variants.find(v => parseFloat(v.price) > 10);
+  if (!v) { console.log(`  ${mfr} no sellable variant — skip`); continue; }
+  const want = retail(t.wp).toFixed(2);
+  if (v.price === want) { console.log(`  ${mfr} ${p.handle} already $${want} — skip (idempotent)`); continue; }
+  map.push({
+    mfr, pattern: t.pat, wp_row: t.row, wp_cost: t.wp,
+    pid, handle: p.handle, variant_id: v.id, title: v.title,
+    price_before: v.price, price_after: want,
+  });
+  console.log(`  ${mfr} ${p.handle.padEnd(14)} ${t.pat.padEnd(16)} $${v.price} -> $${want}   (WP row ${t.row} @ $${t.wp})`);
+}
+
+if (!map.length) { console.log('\nnothing to do — all already correct.'); process.exit(0); }
+
+fs.writeFileSync(EV + '/groupB-reprice-rollback-20260910.json', JSON.stringify(map, null, 1));
+console.log(`\nrollback map -> ${EV}/groupB-reprice-rollback-20260910.json  (${map.length} variants)`);
+
+if (!APPLY) { console.log('\nDRY RUN — nothing written. Re-run with --apply.'); process.exit(0); }
+
+let ok = 0, fail = 0;
+for (const m of map) {
+  try {
+    const r = await api(`variants/${m.variant_id}.json`, {
+      method: 'PUT',
+      body: JSON.stringify({ variant: { id: m.variant_id, price: m.price_after } }),
+    });
+    const got = r.variant.price;
+    if (got !== m.price_after) throw new Error(`read-back ${got} != ${m.price_after}`);
+    console.log(`  OK  ${m.mfr} ${m.handle} now $${got}`);
+    ok++;
+  } catch (e) {
+    console.log(`  FAIL ${m.mfr} ${m.handle} :: ${e.message}`);
+    fail++;
+  }
+  await new Promise(r => setTimeout(r, 600));
+}
+console.log(`\napplied=${ok} failed=${fail}`);
+console.log('NOTE: Shopify has read-after-write lag (this is what made the position fixer report 226');
+console.log('false failures). Verify from the STOREFRONT after ~30s, not immediately.');
+console.log(`UNDO: node rollback.mjs --apply   (restores every price_before from the map)`);
diff --git a/rollback.mjs b/rollback.mjs
new file mode 100755
index 0000000..d64c7ed
--- /dev/null
+++ b/rollback.mjs
@@ -0,0 +1,54 @@
+#!/usr/bin/env node
+// TK-10895 Group B reprice — ROLLBACK. Restores every price_before recorded by reprice.mjs.
+// Dry-run by default. --apply to write.
+// Safety: refuses to touch a variant whose CURRENT price is not the price_after we recorded
+// (i.e. something else changed it since) unless --force.
+import fs from 'node:fs';
+
+const TOK = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8')
+  .split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN='))
+  ?.split('=').slice(1).join('=').trim();
+if (!TOK) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const APPLY = process.argv.includes('--apply');
+const FORCE = process.argv.includes('--force');
+const MAP = process.env.HOME + '/.claude/yolo-queue/evidence/TK-10895/groupB-reprice-rollback-20260910.json';
+
+if (!fs.existsSync(MAP)) { console.error('no rollback map at ' + MAP); process.exit(1); }
+const map = JSON.parse(fs.readFileSync(MAP, 'utf8'));
+if (!map.length) { console.log('map is empty — nothing to roll back.'); process.exit(0); }
+
+async function api(path, opt = {}) {
+  const r = await fetch(`https://${SHOP}/admin/api/${API}/${path}`, {
+    ...opt,
+    headers: { 'X-Shopify-Access-Token': TOK, 'Content-Type': 'application/json', ...(opt.headers || {}) },
+  });
+  if (!r.ok) throw new Error(`${r.status} ${path} :: ${(await r.text()).slice(0, 200)}`);
+  return r.json();
+}
+
+let ok = 0, skip = 0, fail = 0;
+for (const m of map) {
+  try {
+    const v = (await api(`variants/${m.variant_id}.json`)).variant;
+    if (v.price === m.price_before) { console.log(`  ${m.mfr} ${m.handle} already $${m.price_before} — skip`); skip++; continue; }
+    if (v.price !== m.price_after && !FORCE) {
+      console.log(`  ${m.mfr} ${m.handle} DRIFTED (live $${v.price}, expected $${m.price_after}) — refusing without --force`);
+      skip++; continue;
+    }
+    if (!APPLY) { console.log(`  ${m.mfr} ${m.handle} would restore $${v.price} -> $${m.price_before}`); continue; }
+    const r = await api(`variants/${m.variant_id}.json`, {
+      method: 'PUT',
+      body: JSON.stringify({ variant: { id: m.variant_id, price: m.price_before } }),
+    });
+    console.log(`  OK  ${m.mfr} ${m.handle} restored to $${r.variant.price}`);
+    ok++;
+  } catch (e) {
+    console.log(`  FAIL ${m.mfr} ${m.handle} :: ${e.message}`);
+    fail++;
+  }
+  await new Promise(r => setTimeout(r, 600));
+}
+console.log(APPLY ? `\nrestored=${ok} skipped=${skip} failed=${fail}` : '\nDRY RUN — nothing written. Re-run with --apply.');

(oldest)  ·  back to Tk10895 GroupB  ·  TK-10895: Quadrille vendor email — corrected body + human-ap 92adb5c →