← 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);});