← back to Sanderson Onboard

tk10873/reprice_runbook.mjs

76 lines

#!/usr/bin/env node
// reprice_runbook.mjs — TK-10873 Zoffany Option-A REPRICE runbook.
//
// DRY-RUN ONLY. NO SHOPIFY/DB WRITES. `--apply` is intentionally NOT implemented —
// the live run is a separate Steve-gated tool. This script contains NO http/fetch/
// graphql-send call of ANY kind; it only READS the local draft JSON and WRITES a
// local plan artifact. It NEVER reads live Shopify (variant ids are emitted as a
// placeholder the future gated tool must resolve).
//
// What it does:
//   1. Reads zoffany_optionA_draft.json (built by build_zoffany_optionA_draft.mjs).
//   2. Filters to action=REPRICE (the 328).
//   3. For each, emits a plan record:
//        { dw_sku, mfr_sku, proposed_sellable_price, sample_price:4.25,
//          would_run: <productVariantsBulkUpdate mutation shape, VARIANT_ID placeholder>,
//          restore_map_schema: <shape the future live tool MUST snapshot before writing> }
//   4. Writes reprice_runbook_dryrun.json (all 328) + prints a summary.
//
// The live reprice (a customer-facing Shopify write) stays GATED and is a SEPARATE
// tool. If someone passes --apply here, we print a gate notice and exit 0.
import fs from 'node:fs';
import { SAMPLE_PRICE } from './lib.mjs';
import { buildWouldRun, restoreMapSchema, VARIANT_ID_PLACEHOLDER } from './reprice_lib.mjs';

const DIR = new URL('.', import.meta.url).pathname;
const DRAFT_IN = `${DIR}zoffany_optionA_draft.json`;
const OUT = `${DIR}reprice_runbook_dryrun.json`;

// --apply is deliberately a dead branch: this file has NO send path to implement.
if (process.argv.includes('--apply')) {
  console.log('live run is gated — not implemented here');
  process.exit(0);
}

function main() {
  const draft = JSON.parse(fs.readFileSync(DRAFT_IN, 'utf8'));
  const reprice = draft.filter(p => p.action === 'REPRICE');

  const records = reprice.map(p => {
    // dw_sku: the live sellable SKU is the reprice target; fall back to the staged
    // dw_sku (matches the CSV builder's dw_sku fallback). Neither is fabricated.
    const dw_sku = p.live_dw_sku || p.staged_dw_sku || null;
    const price = p.proposed_sellable_price;
    return {
      dw_sku,
      mfr_sku: p.mfr_sku,
      proposed_sellable_price: price,
      sample_price: SAMPLE_PRICE,
      // The mutation the gated live tool WOULD send, with a VARIANT_ID placeholder
      // because we intentionally do not read live variant ids in a dry run.
      would_run: buildWouldRun(dw_sku, price),
      // The pre-write snapshot the live tool MUST capture for a reversible rollback.
      restore_map_schema: restoreMapSchema(),
    };
  });

  fs.writeFileSync(OUT, JSON.stringify(records, null, 2));

  // Summary
  const prices = records.map(r => r.proposed_sellable_price).sort((a, b) => a - b);
  console.log('=== Zoffany Option-A REPRICE runbook (DRY-RUN, NO writes) ===');
  console.log(`plan_records=${records.length}  (expected 328 REPRICE)`);
  console.log(`sample_price=$${SAMPLE_PRICE} on every product (unchanged)`);
  console.log(`proposed price  min=$${prices[0]}  median=$${prices[Math.floor(prices.length / 2)]}  max=$${prices[prices.length - 1]}`);
  console.log(`variant_id in every would_run = ${VARIANT_ID_PLACEHOLDER} (live tool resolves it)`);
  console.log(`mutation = productVariantsBulkUpdate (variant id + inventoryItem.sku unchanged; only price)`);
  console.log('NO http/fetch/graphql-send path exists in this file. --apply prints a gate notice and exits.');
  console.log(`\nartifact:\n  ${OUT}`);

  // Show one sample record so the shape is visible in stdout.
  console.log('\nsample plan record:');
  console.log(JSON.stringify(records[0], null, 2));
}

main();