← back to Dw Yolo Loop
gmc-425-count.js
136 lines
#!/usr/bin/env node
// READ-ONLY GMC $4.25 feed-price blast-radius count. No writes anywhere except local result json.
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 token'); process.exit(1); }
const URL = `https://${SHOP}/admin/api/${VERSION}/graphql.json`;
const SAMPLE_PRICE = 4.25;
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(URL, {
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) {
const throttled = JSON.stringify(json.errors).includes('THROTTLED');
if (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 {
title
vendor
hasOnlyDefaultVariant
variants(first: 60) {
nodes { title price }
}
}
}
}`;
(async () => {
let cursor = null;
let total = 0, pages = 0;
let bucketA = 0; // sample-only
let bucketB = 0; // multi-variant, min/feed is 4.25 but a real roll exists
let bucketC = 0; // real sellable feed price
let bucketD = 0; // no price / $0 only
let feedMin425 = 0;
let feedFirst425 = 0;
const vendorCount = {}; // a+b by MIN model
const vendorCountFirst = {}; // first-variant == 4.25
const is425 = (x) => Math.abs(x - SAMPLE_PRICE) < 0.005;
const startedAt = Date.now();
while (true) {
const data = await gql(QUERY, { cursor });
const conn = data.products;
pages++;
for (const p of conn.nodes) {
total++;
const variants = p.variants.nodes || [];
const prices = variants.map(v => parseFloat(v.price)).filter(x => !isNaN(x));
if (prices.length === 0) { bucketD++; continue; }
const firstPrice = prices[0];
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
const hasRealRoll = prices.some(x => x > SAMPLE_PRICE + 0.005);
const minIs425 = is425(minPrice);
const firstIs425 = is425(firstPrice);
if (minIs425) feedMin425++;
if (firstIs425) feedFirst425++;
if (prices.every(x => x === 0)) {
bucketD++;
} else if (is425(maxPrice) && !hasRealRoll) {
bucketA++;
vendorCount[p.vendor] = (vendorCount[p.vendor] || 0) + 1;
} else if (minIs425 && hasRealRoll) {
bucketB++;
vendorCount[p.vendor] = (vendorCount[p.vendor] || 0) + 1;
} else if (minPrice === 0 && hasRealRoll) {
bucketD++;
} else {
bucketC++;
}
if (firstIs425) vendorCountFirst[p.vendor] = (vendorCountFirst[p.vendor] || 0) + 1;
}
if (!conn.pageInfo.hasNextPage) break;
cursor = conn.pageInfo.endCursor;
if (pages % 10 === 0) {
process.stderr.write(` ...${total} products, ${pages} pages, ${((Date.now()-startedAt)/1000).toFixed(0)}s\n`);
}
}
const result = {
generated_at: new Date().toISOString(),
total_active: total,
pages,
elapsed_s: ((Date.now() - startedAt) / 1000).toFixed(1),
buckets_min_model: {
a_sample_only: bucketA,
b_has_real_roll_but_feeds_425: bucketB,
c_real_feed_price_ok: bucketC,
d_no_price_or_zero: bucketD,
},
feed_425_min_model: feedMin425,
feed_425_first_model: feedFirst425,
would_feed_425_a_plus_b: bucketA + bucketB,
vendor_breakdown_a_plus_b: Object.entries(vendorCount).sort((x,y)=>y[1]-x[1]),
vendor_breakdown_first_model: Object.entries(vendorCountFirst).sort((x,y)=>y[1]-x[1]),
};
console.log(JSON.stringify(result, null, 2));
fs.writeFileSync('/Users/macstudio3/Projects/designerwallcoverings/gmc-425-result.json', JSON.stringify(result, null, 2));
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });