← back to Dwjs Bolt Single Roll Fix

scan-york-brewster.js

79 lines

#!/usr/bin/env node
/*
 * scan-york-brewster.js — READ-ONLY audit. Finds every live variant whose option
 * value contains "bolt" across the York/Brewster house lines, records its price,
 * and flags single-roll-priced bolts (the below-cost leak signature).
 * Writes nothing to Shopify. Output: bolt-audit.csv + a console summary.
 */
'use strict';
const fs = require('fs'); const path = require('path');
const ENV = path.join(process.env.HOME, 'Projects/Designer-Wallcoverings/shopify/.env');
const env = k => { try { const l = fs.readFileSync(ENV,'utf8').split('\n').find(x=>x.startsWith(k+'=')); return l?l.slice(k.length+1).trim():process.env[k]; } catch { return process.env[k]; } };
const TOKEN = env('SHOPIFY_ADMIN_TOKEN');
const STORE = env('SHOPIFY_STORE_DOMAIN') || 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
const sleep = ms => new Promise(r=>setTimeout(r,ms));

// York/Brewster house lines (Phillipe Romano included to SEE if it has bolt variants)
const VENDORS = ['Jeffrey Stevens','LA Walls','Apartment Wallpaper','Brewster & York','Fentucci','Phillipe Romano'];
const LEAK_MAX = 45;   // a "Per Bolt" variant priced below this is single-roll-priced => leak suspect

async function gql(query, variables){
  for (let a=0;a<6;a++){
    const r = await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',
      headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},
      body:JSON.stringify({query,variables})});
    const j = await r.json();
    if (j.errors && JSON.stringify(j.errors).includes('THROTTLED')){ await sleep(2000); continue; }
    if (j.errors) throw new Error(JSON.stringify(j.errors));
    return j.data;
  }
  throw new Error('throttled out');
}

const Q = `query($q:String!,$cursor:String){ products(first:50, query:$q, after:$cursor){
  pageInfo{ hasNextPage endCursor }
  edges{ node{ id vendor handle
    variants(first:15){ edges{ node{ sku price selectedOptions{ name value } } } } } } } }`;

(async () => {
  if (!TOKEN) { console.error('no token'); process.exit(1); }
  const rows = [];
  for (const v of VENDORS){
    let cursor=null, page=0, vendorHits=0;
    do {
      const d = await gql(Q, { q:`vendor:'${v.replace(/'/g,"")}' status:active`, cursor });
      const conn = d.products;
      for (const e of conn.edges){
        const p = e.node;
        for (const ve of p.variants.edges){
          const n = ve.node;
          const opt = n.selectedOptions.map(o=>o.value).join(' ');
          if (/bolt/i.test(opt)){
            const price = parseFloat(n.price);
            rows.push({ vendor:p.vendor, handle:p.handle, sku:n.sku||'', price:n.price,
              option:opt, leak: price < LEAK_MAX ? 'LEAK?' : 'bolt-priced' });
            vendorHits++;
          }
        }
      }
      cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
      page++; await sleep(400);
    } while (cursor);
    console.log(`  ${v}: ${vendorHits} bolt variants (${page} pages)`);
  }
  // write csv
  const cols=['vendor','handle','sku','price','option','leak'];
  fs.writeFileSync(path.join(__dirname,'bolt-audit.csv'),
    cols.join(',')+'\n'+rows.map(r=>cols.map(c=>String(r[c]).replace(/,/g,' ')).join(',')).join('\n'));
  // summary
  console.log(`\n=== ${rows.length} total "Per Bolt" variants across York/Brewster ===`);
  const byVL = {};
  for (const r of rows){ const k=`${r.vendor} | ${r.leak}`; byVL[k]=(byVL[k]||0)+1; }
  Object.entries(byVL).sort().forEach(([k,c])=>console.log(`  ${c.toString().padStart(5)}  ${k}`));
  const leaks = rows.filter(r=>r.leak==='LEAK?');
  console.log(`\nLEAK SUSPECTS (Per Bolt priced < $${LEAK_MAX}): ${leaks.length}`);
  const pc={}; leaks.forEach(r=>{pc[r.price]=(pc[r.price]||0)+1;});
  Object.entries(pc).sort((a,b)=>b[1]-a[1]).slice(0,12).forEach(([p,c])=>console.log(`  ${c.toString().padStart(4)} @ $${p}`));
})().catch(e=>{console.error('ERR',e.message);process.exit(1);});