← back to Tk10895 GroupB
erica/reprice-erica.mjs
79 lines
#!/usr/bin/env node
// TK-10895 — reprice the 11 LIVE products whose cost token Erica Nyarko (Quadrille) corrected in writing.
//
// BASIS (not an inference — the mill said it): msg 1a08d0a98d30b250, 2026-09-10 20:37Z —
// "First price is the fabric, the second is the wallpaper $Fabric/$WP."
// "Wildflowers II Fabric $172 | Wallpaper $142"
// "Club cane stating Custom was an error from my new assistant ... the accurate standard roll price of $174"
// msg 1a08d46c0609ead2, 21:43Z — "In wallpaper we have 7 colorways priced at $174 ... not a custom."
// The mill sheet's own Fabric/Wallpaper column selects the token. All 11 targets are F/W=Wallpaper,
// so they take the WALLPAPER cost. Both moves are price REDUCTIONS (we were overcharging).
//
// Dry-run by default. Rollback map written BEFORE any write. Fails closed on any price drift.
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', API='2024-10';
const APPLY=process.argv.includes('--apply');
const EV=process.env.HOME+'/.claude/yolo-queue/evidence/TK-10895';
const SELLABLE_FLOOR=10, PACE_MS=600;
const retail=c=>(Math.round((c/0.65/0.85)*100)/100).toFixed(2);
// mfr -> {before: the ONLY price we will overwrite, cost_before, cost_after, pat}
const T={};
for(const m of ['785121','785221','785321','785421','785521','785621'])
T[m]={before:'311.31',cost_before:172,cost_after:142,pat:'Wildflowers II'};
for(const m of ['718121','718321','718421','718521','718621'])
T[m]={before:'390.95',cost_before:216,cost_after:174,pat:'Club Cane'};
// 718221 is DRAFT / sample-only — releasing it is a CREATE, not a reprice. Deliberately excluded.
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} ${await r.text()}`);
return r.json();
}
const pidOf=Object.fromEntries(fs.readFileSync('pids.txt','utf8').trim().split('\n')
.map(l=>{const [mfr,pid]=l.split('|');return [mfr,pid];}));
const map=[];
for(const [mfr,t] of Object.entries(T)){
const pid=pidOf[mfr];
if(!pid){console.log(` ${mfr} no pid — skip`);continue;}
const p=(await api(`products/${pid}.json?fields=id,handle,status,variants`)).product;
const v=p.variants.find(v=>parseFloat(v.price)>SELLABLE_FLOOR);
const want=retail(t.cost_after);
if(!v){console.log(` ${mfr} ${p.handle} sample-only — skip`);continue;}
if(v.price===want){console.log(` ${mfr} ${p.handle} already $${want} — skip (idempotent)`);continue;}
if(v.price!==t.before){
console.error(` REFUSING ${mfr} ${p.handle}: live $${v.price}, expected $${t.before} — drifted, not touching it`);
continue;
}
map.push({mfr,pid,handle:p.handle,status:p.status,variant_id:v.id,variant_title:v.title,
pattern:t.pat,price_before:v.price,price_after:want,
cost_before:t.cost_before,cost_after:t.cost_after});
console.log(` ${mfr} ${t.pat.padEnd(16)} ${p.handle.slice(0,30).padEnd(31)} $${v.price} -> $${want} (cost $${t.cost_before} -> $${t.cost_after})`);
}
if(!map.length){console.log('\nnothing to do.');process.exit(0);}
const RB=EV+'/erica-reprice-rollback-20260910.json';
fs.writeFileSync(RB,JSON.stringify(map,null,1));
const delta=map.reduce((s,m)=>s+(parseFloat(m.price_before)-parseFloat(m.price_after)),0);
console.log(`\nrollback map -> ${RB} (${map.length} rows)`);
console.log(`total customer-facing reduction: $${delta.toFixed(2)} across ${map.length} products`);
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}})});
if(r.variant.price!==m.price_after) throw new Error(`read-back ${r.variant.price} != ${m.price_after}`);
console.log(` OK ${m.mfr} ${m.handle} now $${r.variant.price}`); 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;
console.log(`\napplied=${ok} failed=${fail}`);
console.log('Verify from the STOREFRONT after ~30s, never the Admin API (its variant order is stale).');