← back to Carnegie Reprice

undo-tk11434.mjs

74 lines

// TK-11434 — UNDO for reprice-tk11434.mjs.  Restores each repriced variant to its exact
// pre-write price recorded in the ledger, via LIVE RE-READ + COMPARE-AND-SET: it only rolls a
// variant back IF the current live price still equals the newPrice we set (i.e. nothing else has
// touched it since). If the live price has moved on, it SKIPS — never clobbers a later change.
//
// Usage:
//   node undo-tk11434.mjs            # DRY-RUN: show what would be restored
//   node undo-tk11434.mjs --apply    # restore all applied variants to prePrice
//   node undo-tk11434.mjs <variantId>          # dry-run one variant
//   node undo-tk11434.mjs <variantId> --apply  # restore one variant
import fs from 'node:fs';

const HOME = process.env.HOME;
const ENV = `${HOME}/Projects/secrets-manager/.env`;
const env = k => { const m = fs.readFileSync(ENV,'utf8').split('\n').find(l=>l.startsWith(k+'=')); return m ? m.slice(k.length+1).trim().replace(/^["']|["']$/g,'') : ''; };
const TOKEN = env('SHOPIFY_ADMIN_TOKEN');
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API = `https://${DOMAIN}/admin/api/2024-10/graphql.json`;
const sleep = ms => new Promise(r=>setTimeout(r,ms));

const DIR = new URL('.', import.meta.url).pathname;
const LED = `${DIR}reprice-tk11434-ledger.jsonl`;
const APPLY = process.argv.includes('--apply');
const ONLY = process.argv.slice(2).find(a=>/^\d+$/.test(a));
const log = o => fs.appendFileSync(LED, JSON.stringify({ ts:new Date().toISOString(), tk:'TK-11434', ...o }) + '\n');

async function gql(query, variables={}) {
  for (let a=0;a<6;a++){
    const r = await fetch(API,{ method:'POST', headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},
      body: JSON.stringify({query,variables}), signal: AbortSignal.timeout(45000) });
    if (r.status===429 || r.status>=500){ await sleep(1500*(a+1)); continue; }
    const j = await r.json();
    if (j.errors && JSON.stringify(j.errors).includes('THROTTLED')){ await sleep(3000*(a+1)); continue; }
    return j;
  }
  throw new Error('gql retries exhausted');
}

if (!fs.existsSync(LED)) { console.log('no ledger — nothing to undo'); process.exit(0); }

// latest applied (and not later undone) price-change per variant
const state = new Map();
for (const l of fs.readFileSync(LED,'utf8').split('\n')){
  if(!l.trim())continue; let r; try{ r=JSON.parse(l); }catch{ continue; }
  if (r.phase==='applied') state.set(String(r.vid), { vid:String(r.vid), pid:r.pid, sku:r.sku, prePrice:r.prePrice, newPrice:r.newPrice });
  if (r.phase==='undone')  state.delete(String(r.vid));
}
let targets = [...state.values()];
if (ONLY) targets = targets.filter(t=>t.vid===ONLY);
console.log(`[undo] ${targets.length} variant(s) to restore${ONLY?` (filtered to ${ONLY})`:''}`);

let restored=0, skip=0, failed=0;
const M = `mutation($id:ID!,$price:Money!){ productVariantsBulkUpdate(productId:$pid, variants:[{id:$id,price:$price}]){ userErrors{ message } } }`;
for (const t of targets){
  const gvid = `gid://shopify/ProductVariant/${t.vid}`;
  const j = await gql(`query($id:ID!){ productVariant(id:$id){ id price product{ id } } }`, { id: gvid });
  const v = j.data?.productVariant;
  if (!v){ console.log('SKIP not-found', t.vid); log({phase:'undo-skip',vid:t.vid,why:'not found'}); skip++; continue; }
  const cur = +v.price;
  if (Math.abs(cur - Number(t.prePrice)) < 0.005){ console.log('ALREADY', t.sku, cur); skip++; continue; }
  if (Math.abs(cur - Number(t.newPrice)) >= 0.005){ console.log('SKIP moved-since', t.sku, 'live',cur,'expected',t.newPrice); log({phase:'undo-skip',vid:t.vid,liveNow:cur,expected:t.newPrice,why:'moved off our value'}); skip++; continue; }
  if (!APPLY){ console.log(`DRY  restore ${t.sku}  ${cur} -> ${t.prePrice}`); continue; }
  const pid = v.product.id;
  const mut = `mutation($pid:ID!,$id:ID!,$price:Money!){ productVariantsBulkUpdate(productId:$pid, variants:[{id:$id,price:$price}]){ productVariants{ id price } userErrors{ message } } }`;
  const r = await gql(mut, { pid, id: gvid, price: String(Number(t.prePrice).toFixed(2)) });
  const ue = r.data?.productVariantsBulkUpdate?.userErrors || [];
  if (ue.length || r.errors){ console.log('FAIL', t.sku, JSON.stringify(ue.length?ue:r.errors).slice(0,160)); log({phase:'undo-failed',vid:t.vid,err:ue.length?ue:r.errors}); failed++; await sleep(500); continue; }
  console.log(`OK   restore ${t.sku}  ${cur} -> ${t.prePrice}`);
  log({ phase:'undone', pid:t.pid, vid:t.vid, sku:t.sku, restoredTo:t.prePrice, fromPrice:cur });
  restored++; await sleep(500);
}
console.log(`\n== ${APPLY?'RESTORED':'DRY-RUN would-restore'}=${restored} skip=${skip} failed=${failed} ==`);
if (!APPLY) console.log('DRY-RUN — nothing written. Add --apply to restore.');