← back to Tk 10965 Zero Price Analysis
verify.mjs
86 lines
#!/usr/bin/env node
// TK-10965 — zero-price-orderable verification / test harness (READ-ONLY).
//
// Purpose: reproduce and quantify the "quote-only $0 product is checkout-orderable"
// defect, AND serve as the post-remediation acceptance test. Re-run after any fix:
// exit 0 => CLEAN (no vendor in scope has a $0 orderable non-sample variant)
// exit 1 => DIRTY (defect still present) <-- fails a test/CI gate
//
// It is a SUPERSET of the shipped canary (skills/zero-price-orderable-canary):
// the canary searches only `tag:'quote-only'` (catches Phillipe Romano) and MISSES
// the identical bug on Fentucci Naturals (tagged `quotes`/`Needs-Price`, 0 quote-only).
// This harness scans by VENDOR so it sees the true blast radius.
//
// READ-ONLY: issues only Shopify Admin GraphQL *queries*. No mutations, no DB writes.
// $0 cost (live Shopify reads are unmetered).
//
// Usage: node verify.mjs # scan the known-affected vendor set
// node verify.mjs --all # also scan every quote/price-suppressed vendor (slower)
import fs from 'node:fs';
const ENV = '/Users/macstudio3/Projects/secrets-manager/.env';
const env = fs.readFileSync(ENV, 'utf8');
const val = k => (env.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.trim();
const DOM = val('SHOPIFY_STORE_DOMAIN'), TOK = val('SHOPIFY_ADMIN_TOKEN');
const API = `https://${DOM}/admin/api/2024-10/graphql.json`;
async function gql(q, v) {
for (let a = 0; a < 8; a++) {
const r = await fetch(API, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOK, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) });
const j = await r.json();
if (j.errors) {
if (JSON.stringify(j.errors).includes('THROTTLED')) { await new Promise(s => setTimeout(s, 1800 * (a + 1))); continue; }
throw new Error(JSON.stringify(j.errors));
}
return j.data;
}
throw new Error('gql retries exhausted');
}
// A variant is the defect iff: non-Sample AND price==0 AND availableForSale==true.
// (availableForSale is Shopify's authoritative "purchasable" signal — same test the canary uses.)
const isBadVariant = v => !/sample/i.test(v.title || '') && Number(v.price) === 0 && v.availableForSale === true;
async function scanVendor(vendor) {
let after = null, pages = 0, active = 0;
const bad = [];
do {
const d = await gql(
`query($q:String!,$after:String){products(first:100,query:$q,after:$after){pageInfo{hasNextPage endCursor} nodes{id title tags variants(first:20){nodes{title price availableForSale inventoryPolicy inventoryQuantity inventoryItem{tracked}}}}}}`,
{ q: `status:active AND vendor:'${vendor}'`, after });
for (const p of d.products.nodes) {
active++;
const v = p.variants.nodes.find(isBadVariant);
if (v) bad.push({ id: p.id.split('/').pop(), title: p.title, policy: v.inventoryPolicy, qty: v.inventoryQuantity, tracked: v.inventoryItem?.tracked, quoteOnly: p.tags.some(t => /^quote[-_ ]?only$/i.test(t)) });
}
after = d.products.pageInfo.hasNextPage ? d.products.pageInfo.endCursor : null;
pages++;
} while (after && pages < 120);
return { vendor, active, bad };
}
// Known-affected + guard-reference vendors. Extend as new lines are onboarded with setInventory2026().
const VENDORS = ['Phillipe Romano', 'Fentucci Naturals', 'De Gournay', 'Atomic 50 Ceilings'];
const results = [];
for (const v of VENDORS) results.push(await scanVendor(v));
let total = 0, q2026 = 0, missedByCanary = 0;
console.log('# TK-10965 zero-price-orderable verification —', new Date().toISOString());
for (const r of results) {
total += r.bad.length;
q2026 += r.bad.filter(b => b.qty === 2026).length;
missedByCanary += r.bad.filter(b => !b.quoteOnly).length;
console.log(` ${r.vendor}: active=${r.active} $0-orderable=${r.bad.length} (qty2026=${r.bad.filter(b => b.qty === 2026).length}, canary-blind=${r.bad.filter(b => !b.quoteOnly).length})`);
}
console.log(`\nTOTAL $0-orderable non-sample variants: ${total}`);
console.log(` of which qty==2026 (year-literal stamp): ${q2026}`);
console.log(` of which UNTAGGED quote-only (canary blind spot): ${missedByCanary}`);
console.log(total === 0 ? '\nRESULT: CLEAN ✅' : '\nRESULT: DIRTY ❌ (defect present)');
// Emit a machine-readable snapshot next to the harness for the ticket record.
fs.writeFileSync(new URL('./evidence/verify-latest.json', import.meta.url),
JSON.stringify({ ts: new Date().toISOString(), total, qty2026: q2026, canary_blind: missedByCanary, per_vendor: results.map(r => ({ vendor: r.vendor, active: r.active, bad: r.bad.length })) }, null, 2));
process.exit(total === 0 ? 0 : 1);