← back to Dw Yolo Loop
scripts/kravet-master-2026/dryrun_roll_reprice.mjs
77 lines
#!/usr/bin/env node
/* DRY-RUN (read-only): Kravet-family ROLL wallcovering reprice to Jan/Apr-2026 NEW MAP.
Reads VARIANT-level data from Shopify Admin (NOT the sample-contaminated mirror),
finds the sellable (non-sample) Price-Per-Roll variant, compares its price to new_map.
Input : /tmp/kravet_roll_targets.csv (shopify_gid,vendor,mfr_sku,new_map)
Output: /tmp/kravet_roll_dryrun.csv + console summary. NO WRITES.
Token : SHOPIFY_ADMIN_TOKEN (sourced from secrets-manager/.env). */
import fs from 'fs';
const STORE='designer-laboratory-sandbox.myshopify.com';
const TOKEN=process.env.SHOPIFY_ADMIN_TOKEN;
if(!TOKEN){ console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const rows=fs.readFileSync('/tmp/kravet_roll_targets.csv','utf8').trim().split('\n')
.map(l=>{const c=l.split(','); return {gid:c[0],vendor:c[1],sku:c[2],new_map:parseFloat(c[3])};})
.filter(r=>r.gid && r.gid.startsWith('gid://'));
async function gql(q,vars){
const r=await fetch(`https://${STORE}/admin/api/2024-10/graphql.json`,{
method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},
body:JSON.stringify({query:q,variables:vars})});
const j=await r.json();
if(j.errors){ throw new Error(JSON.stringify(j.errors).slice(0,300)); }
return j.data;
}
const Q=`query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product{ id vendor status
variants(first:20){ edges{ node{ id title price sku selectedOptions{ name value } } } } } } }`;
const isSample=v=>{
const t=(v.title||'').toLowerCase(), s=(v.sku||'').toLowerCase();
if(t.includes('sample')||t.includes('memo')||t.includes('swatch')) return true;
if(s.includes('sample')) return true;
if(v.selectedOptions?.some(o=>/sample|memo|swatch/i.test(o.value))) return true;
const p=parseFloat(v.price); if(p>=4&&p<=4.5) return true; // $4.25 sample trap
return false;
};
const out=[['gid','vendor','mfr_sku','new_map','sellable_variant_id','cur_price','delta','n_variants','has_sample','has_sellable','category']];
const cats={};
const batch=40;
for(let i=0;i<rows.length;i+=batch){
const slice=rows.slice(i,i+batch);
let data;
try{ data=await gql(Q,{ids:slice.map(r=>r.gid)}); }
catch(e){ console.error('batch',i,'err',e.message); await sleep(2000); i-=batch; continue; }
const byId={}; for(const n of (data.nodes||[])) if(n&&n.id) byId[n.id]=n;
for(const r of slice){
const p=byId[r.gid];
if(!p){ push(r,null,null,0,false,false,'PRODUCT_MISSING'); continue; }
const vs=(p.variants?.edges||[]).map(e=>e.node);
const samples=vs.filter(isSample), sellables=vs.filter(v=>!isSample(v));
const has_sample=samples.length>0, has_sellable=sellables.length>0;
if(!has_sellable){ push(r,null,null,vs.length,has_sample,false,'SAMPLE_ONLY_NEEDS_PRICE'); continue; }
// pick the highest-priced sellable variant (the real roll)
const sv=sellables.sort((a,b)=>parseFloat(b.price)-parseFloat(a.price))[0];
const cur=parseFloat(sv.price), delta=+(r.new_map-cur).toFixed(2);
let cat;
if(Math.abs(delta)<=0.50) cat='AT_MAP';
else if(cur<r.new_map) cat='BELOW_MAP';
else cat='ABOVE_MAP';
push(r,sv.id.split('/').pop(),cur,vs.length,has_sample,true,cat,delta);
}
process.stdout.write(`\r${Math.min(i+batch,rows.length)}/${rows.length}`);
await sleep(600);
}
function push(r,vid,cur,nv,hs,hsell,cat,delta){
cats[cat]=(cats[cat]||0)+1;
out.push([r.gid,r.vendor,r.sku,r.new_map,vid??'',cur??'',delta??'',nv,hs,hsell,cat]);
}
fs.writeFileSync('/tmp/kravet_roll_dryrun.csv',out.map(r=>r.join(',')).join('\n'));
console.log('\n\n=== DRY-RUN SUMMARY (no writes) ===');
for(const [k,v] of Object.entries(cats).sort((a,b)=>b[1]-a[1])) console.log(` ${k.padEnd(26)} ${v}`);
const below=out.slice(1).filter(r=>r[10]==='BELOW_MAP');
const totalUplift=below.reduce((s,r)=>s+parseFloat(r[6]||0),0);
console.log(`\n BELOW_MAP reprice candidates: ${below.length}, total uplift if applied: $${totalUplift.toFixed(2)}`);
console.log(' detail -> /tmp/kravet_roll_dryrun.csv');