← back to Harlequin Sample Price Analysis

scripts/tk10870-action1-reprice.mjs

87 lines

#!/usr/bin/env node
// TK-10870 ACTION 1 — reprice the ROLL variant of 27 candidates to target_retail.
// Idempotent, restore-map-backed, read-back-verified. Steve-APPROVED (2026-08-30).
// Reads the write plan from tk10870-live-read.json (built read-only just before).
import { readFileSync, writeFileSync } from 'node:fs';

const ENV = readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].trim();
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
const APPLY = process.argv.includes('--apply');

async function gql(query, variables) {
  const r = await fetch(`https://${DOMAIN}/admin/api/${API}/graphql.json`, {
    method: 'POST',
    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  const j = await r.json();
  if (j.errors) throw new Error('GQL transport errors: ' + JSON.stringify(j.errors));
  return j.data;
}

const live = JSON.parse(readFileSync('/Users/macstudio3/Projects/harlequin-sample-price-analysis/artifacts/tk10870-live-read.json', 'utf8'));
const writes = live.writes;

const M = `mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
  productVariantsBulkUpdate(productId: $productId, variants: $variants) {
    productVariants { id price }
    userErrors { field message }
  }
}`;

const Q = `query($id: ID!){ productVariant(id:$id){ id price sku } }`;

const restoreMap = { ticket: 'TK-10870', action: 'ACTION1-reprice', generated: new Date().toISOString(), domain: DOMAIN, api: API, entries: [] };
const verified = [];
const errors = [];

console.log(APPLY ? '=== APPLY MODE ===' : '=== DRY RUN (pass --apply to write) ===');
for (const w of writes) {
  const rv = w.roll_variant;
  const entry = { dwhq_sku: w.dwhq_sku, product_gid: w.gid, variant_id: rv.id, variant_sku: rv.sku, old_price: rv.price, target_price: String(w.target_retail) };

  // Re-read live right before write (defense-in-depth idempotency): if already at target, skip.
  let preRead;
  try { preRead = await gql(Q, { id: rv.id }); } catch (e) { errors.push({ ...entry, stage: 'preread', error: String(e) }); continue; }
  const curPrice = parseFloat(preRead.productVariant.price);
  if (Math.abs(curPrice - w.target_retail) < 0.005) {
    console.log(`SKIP ${w.dwhq_sku} — already at ${curPrice}`);
    entry.result = 'skip_already_at_target'; entry.old_price = preRead.productVariant.price;
    restoreMap.entries.push(entry);
    continue;
  }
  entry.old_price = preRead.productVariant.price; // authoritative pre-write value

  if (!APPLY) { console.log(`WOULD reprice ${w.dwhq_sku} ${entry.old_price} -> ${w.target_retail}`); restoreMap.entries.push(entry); continue; }

  let res;
  try {
    res = await gql(M, { productId: w.gid, variants: [{ id: rv.id, price: String(w.target_retail) }] });
  } catch (e) { errors.push({ ...entry, stage: 'mutation', error: String(e) }); continue; }
  const ue = res.productVariantsBulkUpdate.userErrors;
  if (ue && ue.length) { errors.push({ ...entry, stage: 'userErrors', userErrors: ue }); console.log(`ERR  ${w.dwhq_sku}`, JSON.stringify(ue)); continue; }

  // Read-back verify
  const post = await gql(Q, { id: rv.id });
  const newPrice = parseFloat(post.productVariant.price);
  const ok = Math.abs(newPrice - w.target_retail) < 0.005;
  entry.result = ok ? 'written_verified' : 'written_MISMATCH';
  entry.verified_price = post.productVariant.price;
  restoreMap.entries.push(entry);
  verified.push({ dwhq_sku: w.dwhq_sku, price: post.productVariant.price, ok });
  console.log(`${ok ? 'OK  ' : 'BAD '} ${w.dwhq_sku} ${entry.old_price} -> ${post.productVariant.price}`);
}

const RM_PATH = '/Users/macstudio3/.claude/yolo-queue/executed-reversible/harlequin-tk10870-action1-reprice-restore-map.json';
if (APPLY) writeFileSync(RM_PATH, JSON.stringify(restoreMap, null, 2));
writeFileSync('/Users/macstudio3/Projects/harlequin-sample-price-analysis/artifacts/tk10870-action1-result.json', JSON.stringify({ restoreMap, verified, errors }, null, 2));

console.log('\n=== SUMMARY ===');
console.log('Repriced+verified:', verified.filter(v => v.ok).length, '/ attempted', writes.length);
console.log('Skipped (already at target):', restoreMap.entries.filter(e => e.result === 'skip_already_at_target').length);
console.log('Errors:', errors.length);
if (APPLY) console.log('Restore-map:', RM_PATH);
if (errors.length) console.log(JSON.stringify(errors, null, 2));