← back to Dw Yolo Loop
scripts/kravet-offmap-unitaware.js
63 lines
#!/usr/bin/env node
/* UNIT-AWARE off-MAP: poll bulk op → for each Kravet ACTIVE product, take the REAL (non-sample)
variant, derive its unit from the option name (Yard/Roll/Bolt/SqFt/Each/Panel), and compare its
price to the 2026 MAP ONLY when the variant's unit == the MAP's unit (from the Jan sheet).
Buckets: unit-matched off-MAP (TRUSTWORTHY) | unit-mismatch (suspect) | unit-unknown | sample-only.
READ-ONLY. Writes /tmp/kravet_offmap_trusted.csv (vendor,sku,unit,cur,map,delta,gid). */
const https=require('https'); const fs=require('fs');
const TOKEN=process.env.T, STORE='designer-laboratory-sandbox.myshopify.com';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gql(q){return new Promise(r=>{const rq=https.request(`https://${STORE}/admin/api/2024-10/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'}},x=>{let b='';x.on('data',d=>b+=d);x.on('end',()=>r(JSON.parse(b)))});rq.write(JSON.stringify({query:q}));rq.end();});}
function download(url){return new Promise((res,rej)=>{https.get(url,r=>{let b='';r.on('data',d=>b+=d);r.on('end',()=>res(b));}).on('error',rej);});}
const MAP={}; fs.readFileSync('/tmp/all_sku_newmap.txt','utf8').trim().split('\n').forEach(l=>{const[s,m]=l.split('|');if(s)MAP[s.trim().toUpperCase()]=parseFloat(m);});
const UNIT=JSON.parse(fs.readFileSync('/tmp/sku_unit.json','utf8')); // sku → YARD/ROLL/SQUARE FOOT/EACH
const KFAM=new Set(['Kravet','Kravet Couture','Kravet Design','Lee Jofa','Lee Jofa Modern','Brunschwig & Fils','Cole & Son','GP & J Baker','Clarke And Clarke','Mulberry','Threads','Baker Lifestyle','Gaston y Daniela','Andrew Martin']);
const norm=s=>(s||'').toUpperCase().replace(/\s+/g,' ').trim();
function unitOf(opt){const o=(opt||'').toLowerCase();
if(/sample/.test(o))return 'SAMPLE';
if(/yard/.test(o))return 'YARD';
if(/roll|bolt/.test(o))return 'ROLL';
if(/sq|square|foot|sf\b/.test(o))return 'SQUARE FOOT';
if(/panel|mural/.test(o))return 'PANEL';
if(/each|tile|set/.test(o))return 'EACH';
return 'OTHER';}
(async()=>{
let url=null;
for(let i=0;i<180;i++){const s=await gql(`{ currentBulkOperation{ status objectCount url errorCode } }`);const op=s.data?.currentBulkOperation;
if(i%4===0)console.log(` poll ${i}: ${op?.status} obj=${op?.objectCount||0}`);
if(op?.status==='COMPLETED'){url=op.url;break;} if(op?.status==='FAILED'){console.log('FAILED',op.errorCode);process.exit(1);} await sleep(5000);}
if(!url){console.log('no url');process.exit(1);}
console.log('downloading...');const data=await download(url);
const lines=data.trim().split('\n').filter(Boolean).map(l=>JSON.parse(l));
const prod={}, vmap={};
for(const n of lines){ if(n.id&&n.id.includes('/Product/'))prod[n.id]={vendor:n.vendor,status:n.status,sku:norm(n.metafield?.value)};
else if(n.__parentId&&n.price!==undefined){(vmap[n.__parentId]=vmap[n.__parentId]||[]).push({p:parseFloat(n.price),u:unitOf((n.selectedOptions||[]).map(o=>o.value).join(' '))});} }
const out=['vendor,mfr_sku,unit,cur,map,delta,product_gid'];
let sampleOnly=0, noMap=0, unitUnknown=0, unitMismatch=0, atMap=0, under=0, over=0; const per={};
let underAmt=0, overAmt=0;
for(const [pid,p] of Object.entries(prod)){
if(!KFAM.has(p.vendor)||p.status!=='ACTIVE')continue;
const vs=vmap[pid]||[]; const real=vs.filter(v=>v.u!=='SAMPLE');
if(!real.length){sampleOnly++;continue;}
const map=MAP[p.sku]; if(!map){noMap++;continue;}
const mapUnit=UNIT[p.sku]; // may be undefined
// pick the real variant whose unit matches the MAP unit (if we know it); else the highest real
let v = mapUnit ? real.find(x=>x.u===mapUnit) : null;
if(!v){ if(mapUnit){ // map unit known but no matching variant unit → mismatch
const top=real.sort((a,b)=>b.p-a.p)[0]; if(top.u!=='OTHER'){unitMismatch++;continue;} v=top; }
else { v=real.sort((a,b)=>b.p-a.p)[0]; unitUnknown++; /* still compare, but flag */ } }
const d=v.p-map; per[p.vendor]=per[p.vendor]||{at:0,under:0,over:0};
if(Math.abs(d)<=0.5){atMap++;per[p.vendor].at++;}
else { if(d<0){under++;per[p.vendor].under++;underAmt+=(map-v.p);}else{over++;per[p.vendor].over++;overAmt+=(v.p-map);}
out.push(`${p.vendor},${p.sku},${v.u}${mapUnit?'':'(unit?)'} ,${v.p},${map},${d.toFixed(2)},${pid}`); }
}
fs.writeFileSync('/tmp/kravet_offmap_trusted.csv',out.join('\n'));
console.log('\n=== UNIT-AWARE off-MAP (real variant, unit-matched where possible) ===');
console.log(`sample-only (skip): ${sampleOnly} | no MAP: ${noMap} | unit-mismatch (skip, suspect): ${unitMismatch} | unit-unknown (compared, flagged): ${unitUnknown}`);
console.log(`compared: AT MAP ${atMap} | UNDER ${under} (+$${underAmt.toFixed(0)}) | OVER ${over} (-$${overAmt.toFixed(0)})`);
for(const [v,s] of Object.entries(per).sort((a,b)=>(b[1].under+b[1].over)-(a[1].under+a[1].over))) console.log(` ${v.padEnd(20)} at:${s.at} under:${s.under} over:${s.over}`);
console.log(`\ntrusted off-MAP list → /tmp/kravet_offmap_trusted.csv`);
})().catch(e=>{console.error(e);process.exit(1);});