← back to Hollywood Import
push-1007-shopify.mjs
58 lines
// Push retail to the 1,007 ACTIVE Hollywood products (Momentum+Innovations) — officer-approved
// (vp-dw-commerce). Group-1 method + blocking guardrails:
// GATE: hw>4.25, exactly ONE non-sample variant, never target the $4.25 Sample.
// CONFIRM-FROM-RESPONSE: done only if API echoes new price on the yard variant AND sample still 4.25.
// Audit JSONL (reversible), resumable (skip done pids), batch-paced (≥90s gap / 100).
// PUT-only on an existing variant → cannot trigger the productVariantsBulkCreate sample-overwrite bug.
import fs from 'node:fs';
const envTxt = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (envTxt.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1].trim();
const STORE = (envTxt.match(/^SHOPIFY_STORE=(.+)$/m) || [])[1].trim();
const API = `https://${STORE}/admin/api/2024-01`;
const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
const AUDIT = new URL('push-1007-audit.jsonl', import.meta.url).pathname;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const targets = fs.readFileSync('/tmp/push1007.tsv', 'utf8').trim().split('\n').map(l => {
const [pid, dw, hw] = l.split('\t'); return { pid, dw, hw: parseFloat(hw) };
});
// resume: pids already finalized (UPDATED or noop) in the audit
const done = new Set();
if (fs.existsSync(AUDIT)) for (const l of fs.readFileSync(AUDIT, 'utf8').trim().split('\n').filter(Boolean)) {
try { const r = JSON.parse(l); if (['UPDATED', 'noop'].includes(r.action)) done.add(r.pid); } catch {}
}
const out = fs.createWriteStream(AUDIT, { flags: 'a' });
const isSample = v => /sample/i.test(v.sku || '') || /sample/i.test(v.title || '');
let pushed = 0, noop = 0, skipped = 0, errors = 0, n = 0;
for (const t of targets) {
n++;
if (done.has(t.pid)) { noop++; continue; }
const rec = { pid: t.pid, dw: t.dw, hw: t.hw, ts: n };
try {
if (!(t.hw > 4.25)) { rec.action = 'skip'; rec.why = 'hw<=4.25'; skipped++; out.write(JSON.stringify(rec) + '\n'); continue; }
const r = await fetch(`${API}/products/${t.pid}.json?fields=id,title,variants`, { headers: H });
if (!r.ok) { rec.action = 'ERR'; rec.why = 'GET ' + r.status; errors++; out.write(JSON.stringify(rec) + '\n'); await sleep(600); continue; }
const p = (await r.json()).product;
const sample = p.variants.find(isSample);
const nonSample = p.variants.filter(v => !isSample(v));
if (nonSample.length !== 1) { rec.action = 'skip'; rec.why = `non-sample variants=${nonSample.length}`; rec.variants = p.variants.map(v => v.sku); skipped++; out.write(JSON.stringify(rec) + '\n'); await sleep(300); continue; }
const yard = nonSample[0];
rec.title = p.title; rec.yard_sku = yard.sku; rec.yard_vid = yard.id;
rec.old_price = yard.price; rec.sample_price = sample ? sample.price : '(none)';
if (Math.abs(parseFloat(yard.price) - t.hw) < 0.005) { rec.action = 'noop'; noop++; out.write(JSON.stringify(rec) + '\n'); await sleep(250); continue; }
const u = await fetch(`${API}/variants/${yard.id}.json`, { method: 'PUT', headers: H, body: JSON.stringify({ variant: { id: yard.id, price: t.hw.toFixed(2) } }) });
if (!u.ok) { rec.action = 'ERR'; rec.why = 'PUT ' + u.status; errors++; out.write(JSON.stringify(rec) + '\n'); await sleep(700); continue; }
const got = (await u.json()).variant?.price;
rec.confirmed = got;
if (parseFloat(got) !== t.hw) { rec.action = 'MISMATCH'; errors++; out.write(JSON.stringify(rec) + '\n'); await sleep(550); continue; }
rec.action = 'UPDATED'; pushed++; out.write(JSON.stringify(rec) + '\n');
await sleep(550); // ~1.8/s, under Shopify REST bucket
} catch (e) { rec.action = 'ERR'; rec.why = String(e).slice(0, 80); errors++; out.write(JSON.stringify(rec) + '\n'); }
if (n % 100 === 0) { console.log(`[${n}/${targets.length}] pushed=${pushed} noop=${noop} skip=${skipped} err=${errors} — batch gap 90s`); await sleep(90000); }
}
out.end();
console.log(`DONE: ${targets.length} targets | pushed=${pushed} noop=${noop} skipped=${skipped} errors=${errors}`);
console.log('audit:', AUDIT);