← back to Harlequin Pilot Publish

02-add-single-roll.mjs

92 lines

// Idempotent: add a real "Single Roll" variant at computed retail to each pilot product.
// Sample variant already exists (verified) => left untouched.
// Records a restore-map (created variant ids) BEFORE completion for reversibility.
import fs from 'node:fs';
import path from 'node:path';
import { rest, getProductVariants } from './lib.mjs';

const APPLY = process.argv.includes('--apply');
const located = JSON.parse(fs.readFileSync(new URL('./data/located.json', import.meta.url)));

const RESTORE_MAP_PATH = process.env.HOME + '/.claude/yolo-queue/executed-reversible/harlequin-pilot-restore-map.json';
let restore = { ticket: 'TK-10876', agent: 'vp-dw-commerce', created_at: new Date().toISOString(),
  action: 'harlequin-pilot: add Single Roll variant to 23 Diane Hill products', entries: [] };
if (fs.existsSync(RESTORE_MAP_PATH)) {
  try { restore = JSON.parse(fs.readFileSync(RESTORE_MAP_PATH)); } catch {}
  if (!restore.entries) restore.entries = [];
}

function saveRestore() {
  fs.mkdirSync(path.dirname(RESTORE_MAP_PATH), { recursive: true });
  fs.writeFileSync(RESTORE_MAP_PATH, JSON.stringify(restore, null, 2));
}

const results = [];

for (const p of located) {
  if (!p.productGid) { results.push({ ...p, action: 'SKIP-NOT-FOUND' }); continue; }
  const pidNum = p.productGid.split('/').pop();

  // Re-read current variants FRESH (idempotency source of truth)
  const prod = await getProductVariants(p.productGid);
  const variants = prod.variants.edges.map(e => e.node);
  const wantSku = p.dw_sku;                 // Single Roll base sku
  const wantPrice = Number(p.retail).toFixed(2);

  const existingRoll = variants.find(v =>
    (v.sku || '').toUpperCase() === wantSku.toUpperCase() ||
    (v.title || '').toLowerCase() === 'single roll');

  if (existingRoll) {
    const priceOk = Number(existingRoll.price).toFixed(2) === wantPrice;
    results.push({ dw_sku: p.dw_sku, action: priceOk ? 'SKIP-EXISTS-OK' : 'EXISTS-PRICE-MISMATCH',
      variant_id: existingRoll.id, price: existingRoll.price, want: wantPrice });
    process.stderr.write(`${p.dw_sku}\t${priceOk ? 'SKIP-EXISTS-OK' : 'PRICE-MISMATCH ' + existingRoll.price + '->' + wantPrice}\n`);
    continue;
  }

  if (!APPLY) {
    results.push({ dw_sku: p.dw_sku, action: 'WOULD-ADD', price: wantPrice, option: 'Size=Single Roll' });
    process.stderr.write(`${p.dw_sku}\tWOULD-ADD Single Roll @ $${wantPrice}\n`);
    continue;
  }

  // ADD via REST — mirror the Sample variant's inventory settings (tracked + continue = always sellable)
  const body = { variant: {
    option1: 'Single Roll',
    sku: wantSku,
    price: wantPrice,
    inventory_policy: 'continue',
    inventory_management: 'shopify',
    taxable: true,
    requires_shipping: true,
    weight: 3.0, weight_unit: 'lb',
  }};
  const r = await rest(`products/${pidNum}/variants.json`, 'POST', body);
  if (r.status >= 200 && r.status < 300 && r.json.variant) {
    const v = r.json.variant;
    restore.entries.push({
      dw_sku: p.dw_sku, product_id: pidNum, product_gid: p.productGid, handle: p.handle,
      variant_id: v.id, variant_gid: `gid://shopify/ProductVariant/${v.id}`,
      change: 'ADDED_VARIANT', title: v.title, sku: v.sku, price: v.price,
      prior_value: null, undo: `DELETE /products/${pidNum}/variants/${v.id}.json`,
      created_at: new Date().toISOString(),
    });
    saveRestore(); // persist after EACH add (crash-safe reversibility)
    results.push({ dw_sku: p.dw_sku, action: 'ADDED', variant_id: v.id, price: v.price });
    process.stderr.write(`${p.dw_sku}\tADDED Single Roll id=${v.id} @ $${v.price}\n`);
  } else {
    results.push({ dw_sku: p.dw_sku, action: 'ERROR', status: r.status, body: r.json });
    process.stderr.write(`${p.dw_sku}\tERROR ${r.status} ${JSON.stringify(r.json).slice(0,200)}\n`);
  }
  // >=90s gap is for BULK pushes; single-variant REST adds are light. Pace lightly to be safe.
  await new Promise(res => setTimeout(res, 600));
}

fs.writeFileSync(new URL('./data/add-results.json', import.meta.url), JSON.stringify(results, null, 2));
saveRestore();
const added = results.filter(r => r.action === 'ADDED').length;
const skip = results.filter(r => r.action === 'SKIP-EXISTS-OK').length;
const err = results.filter(r => r.action === 'ERROR' || r.action === 'EXISTS-PRICE-MISMATCH' || r.action === 'SKIP-NOT-FOUND').length;
process.stderr.write(`\n${APPLY ? 'APPLY' : 'DRY-RUN'} DONE: added=${added} skip-ok=${skip} issues=${err}\nrestore-map: ${RESTORE_MAP_PATH}\n`);