← back to Dw Yolo Loop

scripts/kravet-true-coverage.js

52 lines

#!/usr/bin/env node
/* Poll the GraphQL bulk op → download JSONL → compute TRUE 2026-price coverage per Kravet vendor
   using the RELIABLE GraphQL manufacturer_sku (REST GET /metafields.json is broken on this store).
   Writes /tmp/kravet_true_gaps.csv (vendor,mfr_sku,product_gid) for items with no 2026 price. */
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 SET = new Set(fs.readFileSync('/tmp/auth2026_skus.txt','utf8').trim().split('\n').map(s=>s.trim().toUpperCase()));
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();

(async()=>{
  // poll
  let url=null;
  for(let i=0;i<120;i++){
    const s=await gql(`{ currentBulkOperation{ id status objectCount url errorCode } }`);
    const op=s.data?.currentBulkOperation;
    if(i%4===0) console.log(`  poll ${i}: ${op?.status} objects=${op?.objectCount||0}`);
    if(op?.status==='COMPLETED'){ url=op.url; break; }
    if(op?.status==='FAILED'){ console.log('bulk FAILED',op.errorCode); process.exit(1); }
    await sleep(5000);
  }
  if(!url){ console.log('no url (still running or empty)'); process.exit(1); }
  console.log('downloading JSONL...');
  const data=await download(url);
  const lines=data.trim().split('\n').filter(Boolean).map(l=>JSON.parse(l));
  console.log('product nodes:',lines.length);
  // each line: { id, vendor, status, metafield: {value} | null }
  const per={}; const gaps=['vendor,mfr_sku,product_gid'];
  let total=0,have=0,nosku=0;
  for(const n of lines){
    if(!n.vendor || !KFAM.has(n.vendor)) continue;
    if(n.status && n.status!=='ACTIVE') continue;
    total++;
    per[n.vendor]=per[n.vendor]||{t:0,have:0,nosku:0,miss:0};
    per[n.vendor].t++;
    const sku=norm(n.metafield?.value);
    if(!sku){ per[n.vendor].nosku++; nosku++; gaps.push(`${n.vendor},(no mfr_sku),${n.id}`); continue; }
    if(SET.has(sku)){ per[n.vendor].have++; have++; }
    else { per[n.vendor].miss++; gaps.push(`${n.vendor},${sku},${n.id}`); }
  }
  fs.writeFileSync('/tmp/kravet_true_gaps.csv', gaps.join('\n'));
  console.log('\n=== TRUE 2026 COVERAGE (GraphQL) ===');
  for(const [v,s] of Object.entries(per).sort((a,b)=>b[1].t-a[1].t)){
    console.log(`  ${v.padEnd(20)} ${s.t} total | 2026:${s.have} (${(100*s.have/s.t).toFixed(1)}%) | missing-price:${s.miss} | no-sku:${s.nosku}`);
  }
  console.log(`\nTOTAL: ${total} live Kravet items | have 2026 price: ${have} (${(100*have/total).toFixed(1)}%) | no-sku:${nosku} | gap CSV: /tmp/kravet_true_gaps.csv`);
})().catch(e=>{console.error(e);process.exit(1);});