← back to Dw Unbuyable Recovery Pilot
tk11041-innovations-reconcile/rescrape/tk11041-authoritative-table.mjs
98 lines
#!/usr/bin/env node
/**
* TK-11041 — the AUTHORITATIVE per-SKU reprice table for the 39 live PR-Innovations items.
* READ-ONLY. Writes NOTHING to dw_unified or Shopify. Emits JSON + markdown for the gated memo.
*
* Canonical chain — EXACTLY matches the production gated writer
* scripts/innovations-reprice/stage-innov-reprice.mjs line 40:
* price_trade (net) = round(list * 0.90) # 10% trade discount, Sales Order O-111475
* our_price (retail) = round(price_trade / 0.65 / 0.85) # canonical DW markup (NOT the 1.81 approx)
* Net is rounded to cents FIRST because price_trade is a real 2dp stored cost column, then retail
* is derived from that stored net — this is the DW cadence-import convention.
*
* SKYLARK is SOLD BY THE ROLL (vendor 2026-09-15). Its price_trade/our_price are computed at the
* ROLL unit (list_roll = list_per_yd * 10), NOT per-yard-then-x10 — that avoids the 12-cent
* rounding drift ($2198.28 roll-level vs $2198.40 per-yд-x10).
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const TERMS = JSON.parse(fs.readFileSync(path.join(HERE, 'data/pattern-order-terms.json'), 'utf8'));
const r2 = n => Math.round(n * 100) / 100;
// DECIMAL-SAFE money math via integer cents — matches a PostgreSQL NUMERIC round(list*0.90,2)
// write. Plain JS `Math.round(list*0.90*100)/100` hits an IEEE-754 artifact on half-cent
// boundaries (132.95*0.90 = 119.655 reads as 119.6549999 and rounds DOWN to 119.65, a silent 1c
// underprice on Flanders). Integer cents keeps 119.655 -> 11965.5 -> half-up 11966 -> $119.66.
const cents = dollars => Math.round(dollars * 100);
const netOf = list => Math.round(cents(list) * 9 / 10) / 100; // 10% off, exact
const retailOf = net => Math.round(cents(net) / 0.65 / 0.85) / 100; // canonical DW markup
// live shopify_product_id from the Mac2 mirror (READ-ONLY) for the restore-map
const psql = sql => execFileSync('psql', ['-h', '/tmp', '-At', '-F', '\t', '-d', 'dw_unified', '-c', sql], { encoding: 'utf8' }).trim();
const spidRows = psql(`SELECT mfr_sku, coalesce(shopify_product_id::text,''), coalesce(price_trade::text,''),
coalesce(our_price::text,''), coalesce(price_unit,'') FROM innovations_catalog
WHERE substring(mfr_sku from '^[A-Za-z]+') IN ('CSO','ENS','FLN','PRO','SKL','TTM','VCO') ORDER BY mfr_sku;`);
const SPID = {};
for (const l of spidRows.split('\n').filter(Boolean)) {
const [sku, spid, pt, op, pu] = l.split('\t');
SPID[sku] = { spid, old_price_trade: pt || null, old_our_price: op || null, old_price_unit: pu || null };
}
const rows = [];
for (const [sku, t] of Object.entries(TERMS).sort()) {
const listYd = t.current_list_per_yd;
const base = {
mfr_sku: sku, pattern: t.pattern, sold_by: t.sold_by,
vendor_list_per_yd: listYd, min_order_yds: t.min_order_yds,
cut_fee_per_yd: t.cut_fee_per_yd, cut_fee_below_yds: t.cut_fee_below_yds,
width: t.width, warehouse: t.warehouse,
shopify_product_id: SPID[sku]?.spid || null,
restore: { old_price_trade: SPID[sku]?.old_price_trade ?? null,
old_our_price: SPID[sku]?.old_our_price ?? null,
old_price_unit: SPID[sku]?.old_price_unit ?? null },
};
if (t.sold_by === 'roll') {
const listRoll = r2(listYd * t.roll_yds); // 134.95 * 10 = 1349.50
const netRoll = netOf(listRoll); // round(1349.50 * 0.90) = 1214.55
const retailRoll = retailOf(netRoll); // round(1214.55 / 0.65 / 0.85) = 2198.28
rows.push({ ...base, order_unit: 'roll', roll_yds: t.roll_yds,
list_roll: listRoll, net_cost_roll: netRoll, retail_roll: retailRoll,
// per-yд shown for reference only; NOT the stored price for a by-roll item
ref_net_per_yd: netOf(listYd), ref_retail_per_yd: retailOf(netOf(listYd)),
price_unit: 'roll', price_trade: netRoll, our_price: retailRoll });
} else {
const net = netOf(listYd);
rows.push({ ...base, order_unit: 'yard',
net_cost_per_yd: net, retail_per_yd: retailOf(net),
price_unit: 'yard', price_trade: net, our_price: retailOf(net) });
}
}
const now = new Date().toISOString();
const doc = {
ticket: 'TK-11041', kind: 'authoritative-reprice-table', generated_at: now,
read_only: true, no_db_write: true, no_shopify_write: true,
basis: 'LIST (Steve-confirmed; Hektor Martinez reply 2026-09-15)',
formula: 'net = round(list*0.90); retail = round(net/0.65/0.85). Matches stage-innov-reprice.mjs.',
skylark_note: 'SOLD BY THE ROLL (10yd/roll). price_trade/our_price computed at the ROLL unit.',
cohort_n: rows.length, rows,
};
fs.writeFileSync(path.join(HERE, 'data/tk11041-authoritative-table.json'), JSON.stringify(doc, null, 2));
// pattern-level summary
const money = v => '$' + Number(v).toFixed(2);
const byPat = {};
for (const r of rows) if (!byPat[r.pattern]) byPat[r.pattern] = r;
console.log('PATTERN UNIT LIST NET RETAIL min cutfee');
for (const p of Object.keys(byPat)) {
const r = byPat[p];
if (r.order_unit === 'roll')
console.log(`${p.padEnd(14)} roll ${money(r.list_roll).padEnd(10)} ${money(r.net_cost_roll).padEnd(10)} ${money(r.retail_roll).padEnd(11)} ${String(r.min_order_yds).padEnd(3)} n/a (roll) [per-yd ref retail ${money(r.ref_retail_per_yd)}]`);
else
console.log(`${p.padEnd(14)} yard ${money(r.vendor_list_per_yd).padEnd(10)} ${money(r.net_cost_per_yd).padEnd(10)} ${money(r.retail_per_yd).padEnd(11)} ${String(r.min_order_yds).padEnd(3)} +$${r.cut_fee_per_yd}/yd <${r.cut_fee_below_yds}yd`);
}
console.log(`\nrows=${rows.length} -> data/tk11041-authoritative-table.json`);