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