← back to Dwjs Consolidation 2026 04 23

yb_qty_coverage_audit.js

140 lines

#!/usr/bin/env node
// READ-ONLY York/Brewster double-roll MOQ coverage audit.
// Scope refinement (DTD Option C HYBRID): prefix-glob spine (sku:DWJS*, sku:DWQW*)
// UNIONed then FILTERED by actual York/Brewster VENDOR identity:
//   IN SCOPE  = vendor in ('Jeffrey Stevens','Malibu Wallpaper')
//   EXCLUDED  = Apartment Wallpaper, PS Removable Wallpaper, Fentucci (peel&stick / wrong vendor)
// Mural/fabric exemption (belt-and-suspenders OR-logic): EXEMPT if ANY of
//   global.unit_of_measure is a NON-ROLL unit (set AND does not contain 'roll')
//     -- NOTE: unit_of_measure stores phrases like "Priced Per Single Roll",
//        "Full Roll", "Sold Per Single Roll" (= ROLL goods, IN SCOPE) vs
//        "Priced Per Yard" / "Per Panel" (= non-roll, EXEMPT). A "Priced Per
//        Single Roll" good is precisely the double-roll-MOQ case (priced per
//        single, ordered in 2s) -- it is NOT exempt.
//   OR 'mural' in title / productType / tags
//   OR in a collection whose handle/title contains 'mural'
// Drift = in-scope ROLL product whose global.v_prods_quantity_order_min/units is missing or != 2.
// NOTHING is written. Pure audit. Output: drifted-SKU list + exclusion lists + counts.
const https = require('https');
const fs = require('fs');
const STORE='designer-laboratory-sandbox.myshopify.com';
const os=require('os');
function loadToken(){const k=process.env.SHOPIFY_ADMIN_TOKEN;if(k)return k;try{const e=fs.readFileSync(os.homedir()+'/Projects/secrets-manager/.env','utf8');const m=e.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m);if(m)return m[1].trim();}catch{}throw new Error('SHOPIFY_ADMIN_TOKEN not found (export it or set in secrets-manager/.env)');}
let TOKEN; // set below via loadToken()
TOKEN=loadToken();
const OUTDIR='/Users/macstudio3/Projects/dwjs-consolidation-2026-04-23/yb-audit';
fs.mkdirSync(OUTDIR,{recursive:true});

const IN_SCOPE_VENDORS = new Set(['Jeffrey Stevens','Malibu Wallpaper']);

function gql(b, retry=0){return new Promise((res,rej)=>{const d=JSON.stringify(b);const r=https.request({hostname:STORE,path:'/admin/api/2024-10/graphql.json',method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json','Content-Length':Buffer.byteLength(d)}},R=>{let c='';R.on('data',x=>c+=x);R.on('end',()=>{try{res(JSON.parse(c))}catch{if(retry<3)setTimeout(()=>res(gql(b,retry+1)),2000);else res({error:c.slice(0,300)})}})});r.on('error',err=>{if(retry<3)setTimeout(()=>res(gql(b,retry+1)),2000);else rej(err)});r.setTimeout(60000,()=>{r.destroy();if(retry<3)res(gql(b,retry+1));else rej(new Error('t'))});r.write(d);r.end()});}

async function bulkCheck(filter){
  const q=`{ products(query:"${filter}") { edges { node { id title vendor productType tags status
    m1: metafield(namespace:"global", key:"v_prods_quantity_order_min") { value }
    m2: metafield(namespace:"global", key:"v_prods_quantity_order_units") { value }
    m3: metafield(namespace:"global", key:"unit_of_measure") { value }
    collections { edges { node { handle title } } }
    variants { edges { node { id sku title
      v1: metafield(namespace:"custom", key:"v_prod_quantity_order_min") { value }
      v2: metafield(namespace:"custom", key:"v_prods_quantity_order_units") { value }
    } } }
  } } } }`;
  for(let i=0;i<10;i++){const r=await gql({query:'{ currentBulkOperation { status } }'});const st=r?.data?.currentBulkOperation?.status;if(!st||['COMPLETED','FAILED','CANCELED','EXPIRED'].includes(st))break;await new Promise(r=>setTimeout(r,3000));}
  const start=await gql({query:`mutation{ bulkOperationRunQuery(query: """${q}""") { bulkOperation{id} userErrors{message} } }`});
  const ue=start?.data?.bulkOperationRunQuery?.userErrors;
  if(ue&&ue.length) throw new Error('bulk start: '+JSON.stringify(ue));
  for(;;){
    await new Promise(r=>setTimeout(r,4000));
    const p=await gql({query:'{ currentBulkOperation { status objectCount url } }'});
    const op=p?.data?.currentBulkOperation;
    console.log(`  bulk ${op?.status} objects=${op?.objectCount||0}`);
    if(op?.status==='COMPLETED') return op.url;
    if(['FAILED','CANCELED','EXPIRED'].includes(op?.status)) throw new Error(op.status);
  }
}
function dl(url,file){return new Promise((r,j)=>{const o=fs.createWriteStream(file);https.get(url,res=>{res.pipe(o);o.on('finish',()=>o.close(()=>r(file)))}).on('error',j)})}

function isMural(p){
  const blob = `${p.title} ${p.productType} ${p.tags}`.toLowerCase();
  if (/mural/.test(blob)) return 'title/type/tags';
  for (const c of p.collections) { if (/mural/i.test(`${c.handle} ${c.title}`)) return 'collection:'+c.handle; }
  return null;
}

(async()=>{
  const products = new Map();
  for (const filter of ['sku:DWJS*','sku:DWQW*']) {
    console.log(`\n=== bulk pull ${filter} ===`);
    const url = await bulkCheck(filter);
    const file = `${OUTDIR}/raw_${filter.replace(/\W/g,'')}.jsonl`;
    await dl(url, file);
    for (const line of fs.readFileSync(file,'utf8').split('\n').filter(Boolean)) {
      const o = JSON.parse(line);
      if (o.id?.includes('/Product/')) products.set(o.id, {
        id:o.id, title:o.title||'', vendor:o.vendor||'', productType:o.productType||'',
        tags:o.tags||'', status:o.status||'',
        m1:o.m1?.value??null, m2:o.m2?.value??null, m3:o.m3?.value??null,
        collections:[], variants:[],
      });
      else if (o.id?.includes('/Collection/') && o.__parentId) { const p=products.get(o.__parentId); if(p)p.collections.push({handle:o.handle||'',title:o.title||''}); }
      else if (o.id?.includes('/ProductVariant/') && o.__parentId) {
        const p=products.get(o.__parentId);
        if(p)p.variants.push({id:o.id,sku:o.sku||'',title:o.title||'',v1:o.v1?.value??null,v2:o.v2?.value??null});
      }
    }
  }

  const stats = {
    pulled_products: products.size,
    excluded_vendor: {}, // vendor -> count of products dropped (not in-scope vendor)
    in_scope_total: 0,
    mural_exempt: [], fabric_exempt: [],
    ok_2_2: 0,
    drifted: [],         // {dw_sku, vendor, m1, m2, reason}
    drifted_active: 0, drifted_nonactive: 0,
  };

  for (const p of products.values()) {
    if (!IN_SCOPE_VENDORS.has(p.vendor)) {
      stats.excluded_vendor[p.vendor]=(stats.excluded_vendor[p.vendor]||0)+1;
      continue;
    }
    // representative dw_sku = main (non-sample) variant sku, else first
    const main = p.variants.find(v=>!/sample/i.test(v.sku+v.title)) || p.variants[0] || {sku:''};
    const dw_sku = main.sku;
    stats.in_scope_total++;

    const muralReason = isMural(p);
    const unit = (p.m3||'').toLowerCase();
    const nonRoll = unit && !/roll/.test(unit); // 'priced per yard', 'per panel' etc. ROLL phrases stay in scope.
    if (nonRoll) { stats.fabric_exempt.push({dw_sku,vendor:p.vendor,unit:p.m3,type:p.productType}); continue; }
    if (muralReason) { stats.mural_exempt.push({dw_sku,vendor:p.vendor,reason:muralReason,title:p.title}); continue; }

    // ROLL product (or unit unset, default ROLL) -> must be 2/2 at product level
    if (p.m1==='2' && p.m2==='2') { stats.ok_2_2++; }
    else {
      const reason = (p.m1==null&&p.m2==null)?'missing-both':(p.m1!=='2'?'min!=2':'units!=2');
      stats.drifted.push({dw_sku,vendor:p.vendor,status:p.status,m1:p.m1,m2:p.m2,reason,product_id:p.id});
      if (p.status==='ACTIVE') stats.drifted_active++; else stats.drifted_nonactive++;
    }
  }

  // Also write the gated-write payload (drifted product ids) for qty_backfill consumption
  fs.writeFileSync(`${OUTDIR}/drifted_skus.json`, JSON.stringify(stats.drifted,null,2));
  fs.writeFileSync(`${OUTDIR}/mural_exempt.json`, JSON.stringify(stats.mural_exempt,null,2));
  fs.writeFileSync(`${OUTDIR}/fabric_exempt.json`, JSON.stringify(stats.fabric_exempt,null,2));
  fs.writeFileSync(`${OUTDIR}/excluded_vendor.json`, JSON.stringify(stats.excluded_vendor,null,2));

  console.log('\n========== YORK/BREWSTER DOUBLE-ROLL MOQ AUDIT ==========');
  console.log('pulled products (DWJS* ∪ DWQW*):', stats.pulled_products);
  console.log('excluded by vendor identity:', JSON.stringify(stats.excluded_vendor));
  console.log('IN-SCOPE total (Jeffrey Stevens + Malibu Wallpaper):', stats.in_scope_total);
  console.log('  fabric/non-ROLL exempt (unit != ROLL):', stats.fabric_exempt.length);
  console.log('  mural exempt:', stats.mural_exempt.length);
  console.log('  OK already 2/2:', stats.ok_2_2);
  console.log('  DRIFTED (single-roll-orderable):', stats.drifted.length,
              `(ACTIVE=${stats.drifted_active}, non-active=${stats.drifted_nonactive})`);
  console.log('files ->', OUTDIR);
})().catch(e=>{console.error(e);process.exit(1)});