← back to Harlequin Sample Price Analysis
scripts/tk10870-read-live.mjs
125 lines
#!/usr/bin/env node
// TK-10870 Action 1 — READ live Shopify variants for the 27 reprice candidates
// + the 2 collision products. Produces a plan of writes/skips + restore-map.
// READ-ONLY. No mutations.
import { readFileSync, writeFileSync } from 'node:fs';
const ENV = readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].trim();
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
async function gql(query, variables) {
const r = await fetch(`https://${DOMAIN}/admin/api/${API}/graphql.json`, {
method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const j = await r.json();
if (j.errors) throw new Error('GQL errors: ' + JSON.stringify(j.errors));
return j.data;
}
const plan = JSON.parse(readFileSync('/Users/macstudio3/Projects/harlequin-sample-price-analysis/artifacts/reprice-plan.json', 'utf8'));
// Candidates: 27 reprice_now, but EXCLUDE the two collisions per instructions.
const EXCLUDE = new Set(['DWHQ-335002', 'DWHQ-335024']);
const candidates = plan.reprice_now.filter(c => !EXCLUDE.has(c.dwhq_sku));
// Also read the 2 collision products for Action 3 context.
const collisionProductIds = ['7787420680243']; // Cranes shared product (335001/335002)
// DWHQ-335024 Locronan/Demoiselle — need to find its shopify_product_id; not in reprice_now. Read separately later.
const Q = `query($id: ID!) {
product(id: $id) {
id
title
vendor
status
variants(first: 50) {
nodes { id title price sku inventoryItem { id } }
}
}
}`;
const results = [];
const writes = [];
const skips = [];
const restoreMap = [];
for (const c of candidates) {
const gid = `gid://shopify/Product/${c.shopify_product_id}`;
let data;
try {
data = await gql(Q, { id: gid });
} catch (e) {
results.push({ dwhq_sku: c.dwhq_sku, error: String(e) });
continue;
}
const p = data.product;
if (!p) { results.push({ dwhq_sku: c.dwhq_sku, error: 'product not found', gid }); continue; }
const variants = p.variants.nodes;
// Sample variant = title/sku contains -Sample; roll variant = the non-sample sellable one.
const sampleV = variants.find(v => /sample/i.test(v.sku || '') || /sample/i.test(v.title || ''));
const rollVs = variants.filter(v => v !== sampleV);
const rec = {
dwhq_sku: c.dwhq_sku, pattern: c.pattern, color: c.color, gid,
title: p.title, status: p.status,
target_retail: c.target_retail,
variant_count: variants.length,
variants: variants.map(v => ({ id: v.id, title: v.title, price: v.price, sku: v.sku })),
roll_variant_count: rollVs.length,
};
if (rollVs.length === 0) {
rec.disposition = 'NO_ROLL_VARIANT — sample-only live, cannot reprice (would need new variant, out of scope)';
skips.push(rec);
} else if (rollVs.length > 1) {
rec.disposition = 'MULTIPLE_ROLL_VARIANTS — ambiguous, STOP/draft';
skips.push(rec);
} else {
const rv = rollVs[0];
rec.roll_variant = { id: rv.id, title: rv.title, price: rv.price, sku: rv.sku };
const cur = parseFloat(rv.price);
const tgt = c.target_retail;
if (Math.abs(cur - tgt) < 0.005) {
rec.disposition = `ALREADY_AT_TARGET (${cur} == ${tgt}) — SKIP`;
skips.push(rec);
} else {
rec.disposition = `REPRICE ${cur} -> ${tgt}`;
writes.push(rec);
restoreMap.push({ dwhq_sku: c.dwhq_sku, gid, variant_id: rv.id, variant_sku: rv.sku, old_price: rv.price, new_price: String(tgt) });
}
}
results.push(rec);
}
// Read collision product(s) too for context (read-only).
const collisions = [];
for (const pid of collisionProductIds) {
const gid = `gid://shopify/Product/${pid}`;
try {
const data = await gql(Q, { id: gid });
collisions.push(data.product);
} catch (e) { collisions.push({ error: String(e), pid }); }
}
const out = {
ticket: 'TK-10870', generated: new Date().toISOString(),
excluded: [...EXCLUDE],
summary: { candidates: candidates.length, writes: writes.length, skips: skips.length },
writes, skips, restoreMap, collisions,
};
const OUT = '/Users/macstudio3/Projects/harlequin-sample-price-analysis/artifacts/tk10870-live-read.json';
writeFileSync(OUT, JSON.stringify(out, null, 2));
console.log('Candidates:', candidates.length, '| Writes needed:', writes.length, '| Skips:', skips.length);
console.log('---WRITES---');
for (const w of writes) console.log(` ${w.dwhq_sku} ${w.roll_variant.price} -> ${w.target_retail} [${w.roll_variant.title}]`);
console.log('---SKIPS---');
for (const s of skips) console.log(` ${s.dwhq_sku} ${s.disposition}`);
console.log('---COLLISIONS (read-only context)---');
for (const c of collisions) {
if (c.error) { console.log(' ERROR', c.pid, c.error); continue; }
console.log(` ${c.title} [${c.status}] variants: ${c.variants.nodes.map(v=>`${v.sku}=${v.price}`).join(', ')}`);
}
console.log('\nWrote', OUT);