← back to Tk10895 GroupB
rollback.mjs
58 lines
#!/usr/bin/env node
// TK-10895 Group B reprice — ROLLBACK. Restores every price_before recorded by reprice.mjs.
// Dry-run by default. --apply to write.
// Safety: refuses to touch a variant whose CURRENT price is not the price_after we recorded
// (i.e. something else changed it since) unless --force.
import fs from 'node:fs';
const TOK = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8')
.split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN='))
?.split('=').slice(1).join('=').trim();
if (!TOK) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
const APPLY = process.argv.includes('--apply');
const FORCE = process.argv.includes('--force');
// Pace between writes — stay well under Shopify's REST rate limit (matches reprice.mjs).
const PACE_MS = 600;
const MAP = process.env.HOME + '/.claude/yolo-queue/evidence/TK-10895/groupB-reprice-rollback-20260910.json';
if (!fs.existsSync(MAP)) { console.error('no rollback map at ' + MAP); process.exit(1); }
const map = JSON.parse(fs.readFileSync(MAP, 'utf8'));
if (!map.length) { console.log('map is empty — nothing to roll back.'); process.exit(0); }
async function api(path, opt = {}) {
const r = await fetch(`https://${SHOP}/admin/api/${API}/${path}`, {
...opt,
headers: { 'X-Shopify-Access-Token': TOK, 'Content-Type': 'application/json', ...(opt.headers || {}) },
});
if (!r.ok) throw new Error(`${r.status} ${path} :: ${(await r.text()).slice(0, 200)}`);
return r.json();
}
let ok = 0, skip = 0, fail = 0;
for (const m of map) {
try {
const v = (await api(`variants/${m.variant_id}.json`)).variant;
if (v.price === m.price_before) { console.log(` ${m.mfr} ${m.handle} already $${m.price_before} — skip`); skip++; continue; }
if (v.price !== m.price_after && !FORCE) {
console.log(` ${m.mfr} ${m.handle} DRIFTED (live $${v.price}, expected $${m.price_after}) — refusing without --force`);
skip++; continue;
}
if (!APPLY) { console.log(` ${m.mfr} ${m.handle} would restore $${v.price} -> $${m.price_before}`); continue; }
const r = await api(`variants/${m.variant_id}.json`, {
method: 'PUT',
body: JSON.stringify({ variant: { id: m.variant_id, price: m.price_before } }),
});
console.log(` OK ${m.mfr} ${m.handle} restored to $${r.variant.price}`);
ok++;
} catch (e) {
console.log(` FAIL ${m.mfr} ${m.handle} :: ${e.message}`);
fail++;
}
await new Promise(r => setTimeout(r, PACE_MS));
}
if (fail) process.exitCode = 1; // a partial failure must not read as success to a caller/canary
console.log(APPLY ? `\nrestored=${ok} skipped=${skip} failed=${fail}` : '\nDRY RUN — nothing written. Re-run with --apply.');