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