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