← back to Dw Yolo Loop
scripts/kravet-offmap-paginated.js
74 lines
#!/usr/bin/env node
/* READ-ONLY off-MAP measurement WITHOUT a bulk op (avoids collision w/ scheduled catalog op).
Paginates products per Kravet-family vendor via GraphQL, joins live price to 2026 MAP via the
custom.manufacturer_sku metafield (== MAP-file key), unit-aware where /tmp/sku_unit.json knows it,
sample-trap guarded ($4.25 / option=Sample excluded). Writes /tmp/kravet_offmap_paginated.csv.
MAP source = /tmp/all_sku_newmap.txt (Jun-13 dump of auth_pricing.new_map). No writes anywhere. */
const https=require('https'), fs=require('fs');
const T=process.env.T, S='designer-laboratory-sandbox.myshopify.com';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gql(q){return new Promise((res,rej)=>{const rq=https.request(`https://${S}/admin/api/2024-10/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json'}},x=>{let b='';x.on('data',d=>b+=d);x.on('end',()=>{try{res(JSON.parse(b))}catch(e){rej(b.slice(0,300))}})});rq.write(JSON.stringify({query:q}));rq.end();});}
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'));
const KFAM=['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()=>{
const out=['vendor,mfr_sku,unit,cur,map,delta,product_gid'];
let sampleOnly=0,noMap=0,unitMismatch=0,unitUnknown=0,atMap=0,under=0,over=0,underAmt=0,overAmt=0,seen=0;
const per={};
for(const vendor of KFAM){
let cursor=null, vendorCount=0, page=0;
const vq=vendor.replace(/"/g,'\\"');
while(true){
page++;
const after=cursor?`, after:"${cursor}"`:'';
const q=`{ products(first:100, query:"vendor:'${vq}' status:active"${after}){
pageInfo{ hasNextPage endCursor }
edges{ node{ id status
metafield(namespace:"custom",key:"manufacturer_sku"){ value }
variants(first:10){ edges{ node{ price selectedOptions{ value } } } } } } } }`;
let s; try{ s=await gql(q); }catch(e){ console.log(` ${vendor} p${page} ERR ${e}`); break; }
const pr=s.data?.products; if(!pr){ console.log(` ${vendor} no data: ${JSON.stringify(s).slice(0,160)}`); break; }
for(const e of pr.edges){
const n=e.node; seen++; vendorCount++;
const sku=norm(n.metafield?.value);
const vs=(n.variants?.edges||[]).map(x=>({p:parseFloat(x.node.price),u:unitOf((x.node.selectedOptions||[]).map(o=>o.value).join(' '))}));
const real=vs.filter(v=>v.u!=='SAMPLE' && v.p>5);
if(!real.length){ sampleOnly++; continue; }
const map=MAP[sku]; if(!map){ noMap++; continue; }
const mapUnit=UNIT[sku];
let v = mapUnit ? real.find(x=>x.u===mapUnit) : null;
if(!v){ if(mapUnit){ 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++; } }
per[vendor]=per[vendor]||{at:0,under:0,over:0};
const d=v.p-map;
if(Math.abs(d)<=0.5){ atMap++; per[vendor].at++; }
else { if(d<0){under++;per[vendor].under++;underAmt+=(map-v.p);}else{over++;per[vendor].over++;overAmt+=(v.p-map);}
out.push(`${vendor},${sku},${v.u},${v.p},${map},${d.toFixed(2)},${n.id}`); }
}
if(!pr.pageInfo.hasNextPage) break;
cursor=pr.pageInfo.endCursor; await sleep(350);
}
console.log(` ${vendor.padEnd(20)} active=${vendorCount}`);
}
fs.writeFileSync('/tmp/kravet_offmap_paginated.csv',out.join('\n'));
console.log('\n=== KRAVET-FAMILY off-MAP (READ-ONLY, vs Jun-13 auth_pricing.new_map snapshot) ===');
console.log(`active scanned: ${seen}`);
console.log(`sample-only(skip): ${sampleOnly} | no-MAP(skip): ${noMap} | unit-mismatch(skip): ${unitMismatch} | unit-unknown(compared): ${unitUnknown}`);
console.log(`COMPARED: AT-MAP ${atMap} | UNDER ${under} (+$${underAmt.toFixed(0)} to recover) | OVER ${over} (-$${overAmt.toFixed(0)})`);
console.log('per-vendor (compared only):');
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(`\noff-MAP rows → /tmp/kravet_offmap_paginated.csv (${out.length-1})`);
})().catch(e=>{console.error(e);process.exit(1);});