← back to Sanderson Onboard

tk10873/validate_draft.mjs

87 lines

#!/usr/bin/env node
// validate_draft.mjs — THE GATE. Asserts pricing invariants on the generated draft JSON.
// Exits nonzero on any violation. READ-ONLY (reads the generated artifact only; no DB/Shopify).
import fs from 'node:fs';
import { retailFromTrade, SAMPLE_PRICE } from './lib.mjs';

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

const EXPECT = { active: 331, joined: 330, priced: 328, missing_sellable: 1 };

function main() {
  if (!fs.existsSync(JSON_OUT)) {
    console.error(`FAIL: draft not found — run build_zoffany_optionA_draft.mjs first (${JSON_OUT})`);
    process.exit(1);
  }
  const plan = JSON.parse(fs.readFileSync(JSON_OUT, 'utf8'));
  const violations = [];

  let joined = 0, priced = 0, missingSellable = 0;
  const counts = { REPRICE: 0, SELLABLE_ADD: 0, HELD_NEEDS_PRICE: 0 };
  const seenMfrSkus = new Set();
  let previousMfrSku = null;

  for (const p of plan) {
    if (!p.mfr_sku) violations.push('row missing mfr_sku');
    if (seenMfrSkus.has(p.mfr_sku)) violations.push(`${p.mfr_sku}: duplicate mfr_sku`);
    seenMfrSkus.add(p.mfr_sku);
    if (previousMfrSku != null && String(previousMfrSku).localeCompare(String(p.mfr_sku)) > 0)
      violations.push(`${p.mfr_sku}: draft rows are not ordered by mfr_sku`);
    previousMfrSku = p.mfr_sku;

    counts[p.action] = (counts[p.action] || 0) + 1;
    if (p.joined) joined++;
    if (Number(p.price_retail) > 0) priced++;
    if (!p.has_product_variant) missingSellable++;

    // 1. sample_price always 4.25
    if (p.sample_price !== SAMPLE_PRICE) violations.push(`${p.mfr_sku}: sample_price ${p.sample_price} != 4.25`);
    if (!p.has_sample_variant) violations.push(`${p.mfr_sku}: live product is missing its Sample variant`);

    if (p.action === 'REPRICE' || p.action === 'SELLABLE_ADD') {
      // 2. proposed > 0 AND > sample
      if (!(Number(p.proposed_sellable_price) > 0)) violations.push(`${p.mfr_sku}: ${p.action} proposed not >0 (${p.proposed_sellable_price})`);
      if (!(Number(p.proposed_sellable_price) > p.sample_price)) violations.push(`${p.mfr_sku}: ${p.action} proposed !> sample`);
      // 3a. must have a real staged price to be in this bucket (no REPRICE without a staged price)
      if (!(Number(p.price_retail) > 0)) violations.push(`${p.mfr_sku}: ${p.action} has no staged price_retail>0`);
      // 3b. proposed must equal staged price_retail (Option-A: proposed = staged retail)
      if (Number(p.proposed_sellable_price) !== Math.round(Number(p.price_retail) * 100) / 100)
        violations.push(`${p.mfr_sku}: ${p.action} proposed ${p.proposed_sellable_price} != staged retail ${p.price_retail}`);
      // 3c. MARGIN GUARD — where trade present, proposed > trade AND retail≈trade/0.65/0.85 within ±0.02
      if (Number(p.price_trade) > 0) {
        if (!(Number(p.proposed_sellable_price) > Number(p.price_trade)))
          violations.push(`${p.mfr_sku}: MARGIN proposed ${p.proposed_sellable_price} <= trade ${p.price_trade}`);
        const calc = retailFromTrade(p.price_trade);
        if (calc == null || Math.abs(calc - Math.round(Number(p.price_retail) * 100) / 100) > 0.02)
          violations.push(`${p.mfr_sku}: MARGIN retail ${p.price_retail} not ≈ trade/0.65/0.85=${calc} (±0.02)`);
      }
    } else if (p.action === 'HELD_NEEDS_PRICE') {
      // held rows carry NO proposed price
      if (p.proposed_sellable_price != null) violations.push(`${p.mfr_sku}: HELD carries a proposed price ${p.proposed_sellable_price}`);
    } else {
      violations.push(`${p.mfr_sku}: unknown action ${p.action}`);
    }
  }

  // 4. counts reconcile to expected
  if (plan.length !== EXPECT.active) violations.push(`active ${plan.length} != ${EXPECT.active}`);
  if (joined !== EXPECT.joined) violations.push(`joined ${joined} != ${EXPECT.joined}`);
  if (priced !== EXPECT.priced) violations.push(`priced ${priced} != ${EXPECT.priced}`);
  if (missingSellable !== EXPECT.missing_sellable) violations.push(`missing_sellable ${missingSellable} != ${EXPECT.missing_sellable}`);

  console.log('=== validate_draft (GATE) ===');
  console.log(`active=${plan.length} joined=${joined} priced=${priced} missing_sellable=${missingSellable}`);
  console.log(`counts: REPRICE=${counts.REPRICE} SELLABLE_ADD=${counts.SELLABLE_ADD} HELD_NEEDS_PRICE=${counts.HELD_NEEDS_PRICE}`);

  if (violations.length) {
    console.error(`\nVALIDATION FAILED — ${violations.length} violation(s):`);
    for (const v of violations.slice(0, 50)) console.error(`  - ${v}`);
    process.exit(1);
  }
  console.log('\nVALIDATION PASSED — all invariants hold.');
  process.exit(0);
}

main();