← 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.
 */