← 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();