← back to Harlequin Pilot Publish

04-untrack-samples.mjs

87 lines

// PART A: Flip the 23 pilot Sample variants to inventory-UNTRACKED (DW rule: samples untracked).
// IDEMPOTENT (skip any already untracked). REVERSIBLE — records prior tracked state into a restore-map.
// Uses productVariantsBulkUpdate on inventoryItem.tracked=false (the 2024-10 way to untrack).
import fs from 'node:fs';
import path from 'node:path';
import { gql, getProductVariants } from './lib.mjs';

const APPLY = process.argv.includes('--apply');
const verify = JSON.parse(fs.readFileSync(new URL('./data/verify.json', import.meta.url)));
const located = JSON.parse(fs.readFileSync(new URL('./data/located.json', import.meta.url)));
const byDw = Object.fromEntries(located.map(l => [l.dw_sku, l]));

const RESTORE = process.env.HOME + '/.claude/yolo-queue/executed-reversible/harlequin-sample-untrack-restore-map.json';
const LEDGER  = process.env.HOME + '/.claude/yolo-queue/executed-reversible/ledger.jsonl';

let restore = { ticket: 'TK-10876', agent: 'vp-dw-commerce', created_at: new Date().toISOString(),
  action: 'Part A: untrack 23 Harlequin pilot Sample variants (inventory_management shopify -> null)', entries: [] };
if (fs.existsSync(RESTORE)) { try { restore = JSON.parse(fs.readFileSync(RESTORE)); if (!restore.entries) restore.entries = []; } catch {} }
function save() { fs.mkdirSync(path.dirname(RESTORE), { recursive: true }); fs.writeFileSync(RESTORE, JSON.stringify(restore, null, 2)); }

// GraphQL: read current tracked state fresh, then flip via productVariantsBulkUpdate
const BULK = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){
  productVariantsBulkUpdate(productId:$pid, variants:$variants){
    productVariants { id inventoryItem { id tracked } }
    userErrors { field message }
  }
}`;

const results = [];
for (const row of verify) {
  const dw = row.dw_sku;
  const loc = byDw[dw];
  const prod = await getProductVariants(loc.productGid);          // fresh
  const vs = prod.variants.edges.map(e => e.node);
  const sample = vs.find(v => (v.sku || '').toUpperCase() === (dw + '-SAMPLE').toUpperCase());
  if (!sample) { results.push({ dw_sku: dw, action: 'SKIP-NO-SAMPLE' }); process.stderr.write(`${dw}\tSKIP-NO-SAMPLE\n`); continue; }

  const priorTracked = sample.inventoryItem?.tracked;             // true = currently tracked
  if (priorTracked === false) {
    results.push({ dw_sku: dw, action: 'SKIP-ALREADY-UNTRACKED', variant_id: sample.id });
    process.stderr.write(`${dw}\tSKIP-ALREADY-UNTRACKED\n`); continue;
  }

  if (!APPLY) {
    results.push({ dw_sku: dw, action: 'WOULD-UNTRACK', variant_id: sample.id, prior_tracked: priorTracked });
    process.stderr.write(`${dw}\tWOULD-UNTRACK (prior tracked=${priorTracked})\n`); continue;
  }

  const d = await gql(BULK, { pid: loc.productGid, variants: [{ id: sample.id, inventoryItem: { tracked: false } }] });
  const errs = d.productVariantsBulkUpdate.userErrors;
  if (errs && errs.length) {
    results.push({ dw_sku: dw, action: 'ERROR', errors: errs });
    process.stderr.write(`${dw}\tERROR ${JSON.stringify(errs)}\n`); continue;
  }
  const upd = d.productVariantsBulkUpdate.productVariants[0];
  restore.entries.push({
    dw_sku: dw, product_gid: loc.productGid, handle: loc.handle,
    variant_id: sample.id, inventory_item_id: sample.inventoryItem?.id,
    change: 'UNTRACKED_SAMPLE', field: 'inventoryItem.tracked',
    prior_value: true, new_value: false,
    undo: `productVariantsBulkUpdate ${loc.productGid} [{id:${sample.id}, inventoryItem:{tracked:true}}]`,
    now_tracked: upd?.inventoryItem?.tracked, at: new Date().toISOString(),
  });
  save();
  results.push({ dw_sku: dw, action: 'UNTRACKED', variant_id: sample.id, now_tracked: upd?.inventoryItem?.tracked });
  process.stderr.write(`${dw}\tUNTRACKED now_tracked=${upd?.inventoryItem?.tracked}\n`);
  await new Promise(r => setTimeout(r, 400));
}

fs.writeFileSync(new URL('./data/untrack-results.json', import.meta.url), JSON.stringify(results, null, 2));
save();
if (APPLY && restore.entries.length) {
  fs.mkdirSync(path.dirname(LEDGER), { recursive: true });
  fs.appendFileSync(LEDGER, JSON.stringify({
    ts: new Date().toISOString(), agent: 'vp-dw-commerce', ticket: 'TK-10876',
    action: 'Part A: untrack Harlequin pilot Sample variants',
    blast_radius: restore.entries.length,
    undo_cmd: `node ~/Projects/harlequin-pilot-publish/rollback-untrack.mjs --apply`,
    verify: 'GET each sample variant; inventoryItem.tracked must be false, price 4.25',
    restore_map: RESTORE,
  }) + '\n');
}
const u = results.filter(r => r.action === 'UNTRACKED').length;
const s = results.filter(r => r.action === 'SKIP-ALREADY-UNTRACKED').length;
const e = results.filter(r => r.action === 'ERROR').length;
process.stderr.write(`\n${APPLY ? 'APPLY' : 'DRY-RUN'} DONE: untracked=${u} already=${s} errors=${e}\nrestore-map: ${RESTORE}\n`);