← back to Dw Retail Halving Preview
apply-halving-LIVE.js
89 lines
#!/usr/bin/env node
/*
* apply-halving-LIVE.js — GATED live-write executor. *** INERT BY DEFAULT ***
*
* This script is a SCAFFOLD. It will NOT write to Shopify or to any database
* unless invoked with the explicit flags below AND it has a vetted preview CSV.
* It is committed for review only; do NOT run it until Steve has answered the
* three open questions in the pending-approval memo and explicitly says "go".
*
* Safety design (in order):
* 1. INPUT = a vetted preview CSV produced by halve-preview.js (eligible rows only).
* 2. SNAPSHOT-FIRST: before ANY update, write a rollback CSV capturing every
* targeted variant's CURRENT live online price (read live from Shopify, the
* ground truth at write-time — NOT the possibly-stale local mirror).
* Columns: dw_sku, variant_id, inventory_item_id, current_price, intended_halved_price.
* The rollback CSV is the instant-restore artifact.
* 3. RE-VERIFY each row at write-time: vendor still non-MAP, halved>0, and the
* LIVE current price still matches what the preview halved (drift guard).
* 4. BATCH writes via productVariantsBulkUpdate (Shopify 2024-10), batch size
* <= 100 variants, >= 90s gap between batches (DW bulk-push rule), respect
* the 1k variant/day cap until the store passes 50K+ variants.
* 5. VARIANT BUDGET: price updates do NOT create variants, so no upload-budget
* take is required; if a future variant is created, take()+refund() per the
* DW budget.cjs refund rule.
*
* ROLLBACK (separate verb): --rollback <rollback.csv> restores each variant's
* current_price from the snapshot, same batch/gap rules.
*
* Usage (all gated — refuses without --i-have-steve-go):
* node apply-halving-LIVE.js --preview <vetted.csv> --snapshot-out <rollback.csv> --dry
* node apply-halving-LIVE.js --preview <vetted.csv> --snapshot-out <rollback.csv> --execute --i-have-steve-go
* node apply-halving-LIVE.js --rollback <rollback.csv> --execute --i-have-steve-go
*
* Cost: Shopify Admin API calls are free (no per-call charge); $0 paid-API.
*/
'use strict';
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const API_VERSION = '2024-10';
const BATCH_SIZE = 100; // variants per productVariantsBulkUpdate
const BATCH_GAP_MS = 90_000; // >= 90s between batches (DW rule)
const DAILY_VARIANT_CAP = 1000; // until store passes 50K+ variants
function die(msg) { console.error('REFUSING: ' + msg); process.exit(3); }
const args = process.argv.slice(2);
const has = (f) => args.includes(f);
const val = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };
// -------------------------------------------------------------------------
// HARD GATE: this scaffold will not perform a live write in this build.
// Removing this block is a deliberate, Steve-approved action — not a default.
// -------------------------------------------------------------------------
if (has('--execute')) {
if (!has('--i-have-steve-go')) {
die('--execute requires --i-have-steve-go (explicit Steve approval token). None supplied.');
}
die('Live-write path is intentionally not implemented in this committed scaffold. '
+ 'Implement Shopify productVariantsBulkUpdate + snapshot here ONLY after the '
+ 'pending-approval memo is signed and Steve says go.');
}
console.log(`apply-halving-LIVE.js scaffold (INERT). Shop=${SHOP} api=${API_VERSION}`);
console.log(`Batch=${BATCH_SIZE} gap=${BATCH_GAP_MS}ms cap=${DAILY_VARIANT_CAP}/day`);
console.log('Run with --dry once implemented to preview the snapshot+plan; live writes stay gated.');
console.log('cost: $0 (no API calls in scaffold)');
/*
* --- IMPLEMENTATION NOTES for the gated build (left as comments on purpose) ---
*
* snapshot(previewRows):
* for each dw_sku -> resolve variant_id + inventoryItem.id via products query
* read LIVE current variant price (ground truth)
* append {dw_sku, variant_id, inventory_item_id, current_price, intended_halved_price}
* to snapshot-out BEFORE any mutation; flush to disk per batch.
*
* applyBatch(batch):
* mutation productVariantsBulkUpdate(productId, variants:[{id, price}]) — 2024-10.
* verify userErrors empty; on any error, STOP the whole run (fail-closed).
*
* rollback(snapshotCsv):
* for each row, productVariantsBulkUpdate price = current_price (the saved value).
*
* token: SHOPIFY_ADMIN_TOKEN via secrets (read_products + write_products). Never
* hardcode. PostgreSQL-before-Shopify: also patch dw_unified.shopify_products.price
* in the same transaction window so the mirror does not drift.
*/