← 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).');