← back to Designerwallcoverings
scripts/buyability-audit/buyability-audit.mjs
54 lines
// buyability-audit — READ-ONLY, $0. Cycle 89. The sharpest customer-facing inventory defect:
// an ACTIVE product NO shopper can purchase — every variant availableForSale=false (out-of-stock +
// inventoryPolicy=DENY) = a dead-end "out of stock / can't add to cart" PDP that BLOCKS the sale.
// Exhaustive via Admin GraphQL (variant availableForSale + inventoryQuantity + inventoryPolicy). Also
// measures drift from the standing inventory=2026 both-variants convention. NEGATIVE CONTROL: if the
// BULK is buyable, an unbuyable-active product is a true anomaly. EXCLUDES PJ.
import fs from 'fs';
const ENV=fs.readFileSync('/Users/stevestudio2/Projects/secrets-manager/.env','utf8');
const TOK=((ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]||'').replace(/['"\r]/g,'').trim();
const GQL='https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const PJ=/phillip[- ]?jeffries/i;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v){for(let a=0;a<6;a++){const r=await fetch(GQL,{method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});if(r.status===429||r.status>=500){await sleep(2000*(a+1));continue;}const j=await r.json();if(j.errors&&JSON.stringify(j.errors).includes('Throttled')){await sleep(2500*(a+1));continue;}return j;}return null;}
const Q=`query($cursor:String){ products(first:50, after:$cursor, query:"status:active"){ pageInfo{hasNextPage endCursor}
edges{node{ title vendor productType totalInventory
variants(first:12){ edges{ node{ availableForSale inventoryQuantity inventoryPolicy } } } } } } }`;
let cursor=null,pages=0,scanned=0,complete=true;
let buyable=0, unbuyable=0, noVariants=0;
let any2026=0, both2026=0, multiVar=0;
const unbuyByVendor={}, unbuyEx=[];
while(true){ const j=await gql(Q,{cursor}); if(!j?.data){complete=false;break;} const c=j.data.products;
for(const e of c.edges){ const n=e.node; if(PJ.test(n.vendor||''))continue; scanned++;
const vs=(n.variants?.edges||[]).map(x=>x.node);
if(vs.length===0){ noVariants++; continue; }
if(vs.length>1) multiVar++;
const anyBuyable=vs.some(v=>v.availableForSale===true);
if(anyBuyable) buyable++;
else { unbuyable++; unbuyByVendor[n.vendor]=(unbuyByVendor[n.vendor]||0)+1;
if(unbuyEx.length<30){ const why=vs.map(v=>`${v.inventoryPolicy}/${v.inventoryQuantity}`).join(','); unbuyEx.push({t:(n.title||'').slice(0,42),vendor:n.vendor,type:n.productType,vars:vs.length,why:why.slice(0,60)}); } }
// inventory=2026 convention drift
const q2026=vs.filter(v=>v.inventoryQuantity===2026).length;
if(q2026>=1) any2026++;
if(vs.length>=2 && q2026>=2) both2026++;
}
pages++; if(!c.pageInfo.hasNextPage)break; cursor=c.pageInfo.endCursor;
const cost=j.extensions?.cost?.throttleStatus; if(cost&&cost.currentlyAvailable<500) await sleep(1500); else await sleep(250);
if(pages%100===0) console.log(` ${pages}p scanned=${scanned} unbuyable=${unbuyable}`);
}
const pct=x=>scanned?(100*x/scanned).toFixed(2):'0';
console.log(`\n=== buyability-audit (READ-ONLY, $0) ===`);
console.log(`active scanned: ${scanned} (complete=${complete}, ${pages}p) [PJ excluded]`);
console.log(`\nBUYABILITY (can a shopper actually purchase?):`);
console.log(` BUYABLE (>=1 variant availableForSale): ${buyable} (${pct(buyable)}%)`);
console.log(` UNBUYABLE (NO variant purchasable - dead-end PDP, the defect): ${unbuyable} (${pct(unbuyable)}%)`);
console.log(` no-variants (data oddity): ${noVariants}`);
console.log(` NEGATIVE CONTROL: buyable bulk ${pct(buyable)}% -> an unbuyable-active product is ${unbuyable>0?'a true anomaly':'(none found)'}.`);
console.log(`\nINVENTORY=2026 convention (standing rule: activated items set 2026 on BOTH variants):`);
console.log(` active products with >=1 variant at qty 2026: ${any2026} (${pct(any2026)}%)`);
console.log(` multi-variant products: ${multiVar}; of those with BOTH variants at 2026: ${both2026}`);
console.log(`\nUNBUYABLE by vendor (top 15):`); Object.entries(unbuyByVendor).sort((a,b)=>b[1]-a[1]).slice(0,15).forEach(([v,n])=>console.log(` ${v}: ${n}`));
console.log(`\nUNBUYABLE examples (why = inventoryPolicy/qty per variant):`); unbuyEx.forEach(x=>console.log(` "${x.t}" [${x.vendor}] type=${x.type} vars=${x.vars} why=${x.why}`));
fs.writeFileSync('/tmp/buyability-audit.json',JSON.stringify({ts:new Date().toISOString(),scanned,complete,buyable,unbuyable,noVariants,any2026,both2026,multiVar,unbuyByVendor,unbuyEx},null,2));
console.log(`\nwrote /tmp/buyability-audit.json`);