← back to Dw Yolo Loop

showroom-lines-scan.js

103 lines

'use strict';
/**
 * Showroom Lines scan (READ-ONLY, $0).
 * Steve's directive 2026-06-16: products whose ONLY variant is the $4.25 memo
 * sample are a deliberate category — "SHOWROOM LINES" — kept live, $4.25 is the
 * accurate price. This identifies that exact set and characterizes it so we can
 * tag + metafield them and surface a Showroom section.
 *
 * Showroom-line predicate: status:active AND exactly 1 variant AND that variant
 * price <= $4.255 (the $4.25 sample floor, with epsilon).
 *
 * Writes a worklist + a characterization report. Zero writes to Shopify.
 */
const fs = require('fs');
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const VERSION = '2024-10';
const ENV = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1];
if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const ENDPOINT = `https://${SHOP}/admin/api/${VERSION}/graphql.json`;
const SAMPLE = 4.255;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

async function gql(query, variables) {
  for (let attempt = 0; attempt < 7; attempt++) {
    let res;
    try {
      res = await fetch(ENDPOINT, { method: 'POST',
        headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
        body: JSON.stringify({ query, variables }) });
    } catch (e) { await sleep(2000 * (attempt + 1)); continue; }
    if (res.status === 429) { await sleep(2000 * (attempt + 1)); continue; }
    const json = await res.json();
    if (json.errors) {
      if (JSON.stringify(json.errors).includes('THROTTLED')) { await sleep(2000 * (attempt + 1)); continue; }
      throw new Error(JSON.stringify(json.errors));
    }
    const avail = json.extensions?.cost?.throttleStatus?.currentlyAvailable ?? 4000;
    if (avail < 400) await sleep(1500);
    return json.data;
  }
  throw new Error('exhausted retries');
}

const QUERY = `
query($cursor: String) {
  products(first: 100, after: $cursor, query: "status:active") {
    pageInfo { hasNextPage endCursor }
    nodes {
      id title vendor handle productType
      tags
      variants(first: 10) { nodes { title price } }
    }
  }
}`;

// junk heuristics — things we'd want to eyeball before auto-classing as showroom
const JUNK_RE = /\b(test|placeholder|do not use|sample card|deleted)\b/i;

(async () => {
  let cursor = null, pages = 0, total = 0;
  const showroom = [];
  const vendors = {};
  let junk = [], priceOther = 0;
  const t0 = Date.now();
  do {
    const d = await gql(QUERY, { cursor });
    const conn = d.products;
    for (const p of conn.nodes) {
      total++;
      const vs = p.variants.nodes;
      if (vs.length !== 1) continue;                 // must be single-variant
      const price = parseFloat(vs[0].price);
      if (!(price <= SAMPLE)) { priceOther++; continue; } // single variant but not $4.25
      const rec = { id: p.id, title: p.title, vendor: p.vendor || '(none)', handle: p.handle,
                    price, alreadyTagged: (p.tags || []).includes('Showroom Line') };
      showroom.push(rec);
      vendors[rec.vendor] = (vendors[rec.vendor] || 0) + 1;
      if (JUNK_RE.test(p.title)) junk.push(rec);
    }
    cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
    if (++pages % 25 === 0) process.stderr.write(`  ...${pages} pages, ${total} active, showroom=${showroom.length}\n`);
  } while (cursor);

  const topVendors = Object.entries(vendors).sort((a, b) => b[1] - a[1]).slice(0, 20);
  const alreadyTagged = showroom.filter(r => r.alreadyTagged).length;
  const report = {
    scannedActive: total,
    showroomCount: showroom.length,
    singleVariantNon425: priceOther,
    alreadyTagged,
    needTag: showroom.length - alreadyTagged,
    junkSuspects: junk.length,
    topVendors,
    elapsedSec: Math.round((Date.now() - t0) / 1000),
  };
  fs.writeFileSync(__dirname + '/showroom-lines-worklist.json',
    JSON.stringify({ generatedAt: new Date().toISOString(), predicate: 'active & 1 variant & price<=4.255', report, ids: showroom.map(r => r.id) }, null, 2));
  fs.writeFileSync(__dirname + '/showroom-lines-report.json', JSON.stringify({ report, junkSuspects: junk.slice(0, 50), sample: showroom.slice(0, 25) }, null, 2));
  console.log(JSON.stringify(report, null, 2));
  console.log('\nWorklist → showroom-lines-worklist.json   Report → showroom-lines-report.json');
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });