← back to Tk10895 GroupB
reprice.mjs
99 lines
#!/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' },
};
// Retail formula: cost / WHOLESALE_MARGIN / RETAIL_MARGIN — unchanged DW markup chain (TK-10895 verdict).
const WHOLESALE_MARGIN = 0.65;
const RETAIL_MARGIN = 0.85;
const retail = c => Math.round((c / WHOLESALE_MARGIN / RETAIL_MARGIN) * 100) / 100;
// Variants priced at/under this are placeholder/sample rows, not the real sellable SKU.
const SELLABLE_PRICE_FLOOR = 10;
// Pace between writes — stay well under Shopify's REST rate limit.
const PACE_MS = 600;
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) > SELLABLE_PRICE_FLOOR);
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, PACE_MS));
}
if (fail) process.exitCode = 1; // a partial failure must not read as success to a caller/canary
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)`);