← back to Jeffrey Stevens Margin Fix
reprice-rolls.mjs
33 lines
#!/usr/bin/env node
// GATED reprice for JS roll goods below cost. Reads data/js882-cost-manifest.json
// (each row must have york_code + verified_cost from the PORTAL — NOT the snapshot wholesale).
// retail = verified_cost/0.65/0.85. KEEPS the roll unit + min (these are Single Roll goods, NOT yard).
// DRY-RUN default; --apply writes. Reversible: data/reprice-restore.jsonl. Skips rows without verified_cost.
import { readFileSync, appendFileSync, existsSync } from 'node:fs';
const APPLY=process.argv.includes('--apply');
const TOKEN=readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=')[1].trim();
const SHOP='https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const DIR=new URL('.',import.meta.url).pathname;
const MF=DIR+'data/js882-cost-manifest.json';
if(!existsSync(MF)){console.error('MISSING data/js882-cost-manifest.json — run cost verification first (see MEMO). No rows to price.');process.exit(1);}
const rows=JSON.parse(readFileSync(MF)).filter(r=>r.verified_cost>0);
const REST=DIR+'data/reprice-restore.jsonl';
const retail=c=>Math.round(c/0.65/0.85*100)/100;
const gql=(q,v)=>fetch(SHOP,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})}).then(r=>r.json());
let ok=0,skip=0,fail=0;
for(const t of rows){
const pid=`gid://shopify/Product/${t.product_id}`;
const r=await gql(`query($id:ID!){product(id:$id){variants(first:5){edges{node{id title price}}}}}`,{id:pid});
const vs=r.data?.product?.variants.edges.map(e=>e.node)||[];
const sell=vs.find(v=>!/sample/i.test(v.title)); if(!sell){console.log(' MISS',t.sell_sku);fail++;continue;}
const newp=String(retail(t.verified_cost));
if(sell.price===newp){console.log(' = already',t.sell_sku,newp);skip++;continue;}
if(!APPLY){console.log(` DRY ${t.sell_sku} ${t.york_code} $${sell.price}->$${newp} (cost $${t.verified_cost}, keep '${sell.title}')`);ok++;continue;}
appendFileSync(REST,JSON.stringify({pid:t.product_id,vid:sell.id,old_price:sell.price,title:sell.title})+'\n');
const vu=await gql(`mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){productVariantsBulkUpdate(productId:$pid,variants:$v){userErrors{message}}}`,
{pid,v:[{id:sell.id,price:newp}]}); // price ONLY — no uom/option/min change (roll good stays a roll)
if(vu.data.productVariantsBulkUpdate.userErrors.length){console.log(' ERR',t.sell_sku,JSON.stringify(vu.data.productVariantsBulkUpdate.userErrors));fail++;continue;}
console.log(` OK ${t.sell_sku} $${sell.price}->$${newp}`);ok++;await new Promise(x=>setTimeout(x,400));
}
console.log(`\n${APPLY?'APPLIED':'DRY-RUN'} — ok=${ok} skip=${skip} fail=${fail} of ${rows.length} (rows without verified_cost are held out)`);