← back to Dw Yolo Loop

scripts/google-feed/calibrate-price.mjs

66 lines

#!/usr/bin/env node
// calibrate-price.mjs — READ-ONLY. Compare dw_unified mirror price vs LIVE Shopify
// variant prices for a sample of products, to decide whether the clean Google feed
// must be driven by live Shopify data (yes if the mirror is stale).
// No writes. No status changes.
import fs from 'node:fs';

const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const VER = '2024-10';
const envTxt = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (envTxt.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
if (!TOKEN) { console.error('no token'); process.exit(1); }
const URL = `https://${SHOP}/admin/api/${VER}/graphql.json`;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

async function gql(query, variables) {
  for (let attempt = 0; attempt < 6; attempt++) {
    const res = await fetch(URL, { method: 'POST',
      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, variables }) });
    const j = await res.json();
    if (j.errors) { if (JSON.stringify(j.errors).includes('THROTTLED')) { await sleep(2000*(attempt+1)); continue; } throw new Error(JSON.stringify(j.errors)); }
    return j.data;
  }
  throw new Error('throttle');
}

const ids = fs.readFileSync('/tmp/cal_ids.csv','utf8').trim().split('\n')
  .map(l => { const [grp, gid] = l.split(','); return { grp, gid }; }).filter(x => x.gid);

const Q = `query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product {
  id title status vendor
  variants(first:20){ nodes { title sku price } } } } }`;

const isSample = (v) => /(sample|memo|swatch)/i.test([v.title, v.sku].join(' '));

(async () => {
  const data = await gql(Q, { ids: ids.map(x => x.gid) });
  const byId = new Map(ids.map(x => [x.gid, x.grp]));
  const rows = [];
  for (const p of data.nodes) {
    if (!p) continue;
    const grp = byId.get(p.id);
    const vs = p.variants.nodes;
    const roll = vs.filter(v => !isSample(v)).map(v => parseFloat(v.price)).filter(Number.isFinite);
    const samp = vs.filter(v => isSample(v)).map(v => parseFloat(v.price)).filter(Number.isFinite);
    rows.push({ grp, id: p.id.split('/').pop(), title: (p.title||'').slice(0,42), vendor: p.vendor,
      rollPrices: roll, samplePrices: samp,
      rollMax: roll.length ? Math.max(...roll) : null });
  }
  console.log('mirror_grp | live_roll_max | live_sample | vendor | title');
  console.log('-'.repeat(90));
  for (const r of rows) {
    console.log(`${r.grp.padEnd(5)} | ${String(r.rollMax ?? 'NONE').padStart(9)} | ${String(r.samplePrices[0] ?? '-').padStart(7)} | ${(r.vendor||'').slice(0,18).padEnd(18)} | ${r.title}`);
  }
  const nullGrp = rows.filter(r => r.grp === 'NULL');
  const p425Grp = rows.filter(r => r.grp === 'P425');
  const fmt = (g) => {
    const withRoll = g.filter(r => r.rollMax && r.rollMax > 10).length;
    return `${withRoll}/${g.length} have a live roll price > $10`;
  };
  console.log('-'.repeat(90));
  console.log('mirror NULL  :', fmt(nullGrp));
  console.log('mirror $4.25 :', fmt(p425Grp));
})();