← back to Dw Unbuyable Recovery Pilot
tk11041-innovations-reconcile/rescrape/tk11041-dryrun-reprice-plan.mjs
214 lines
#!/usr/bin/env node
/**
* tk11041-dryrun-reprice-plan.mjs — READ-ONLY dry-run reprice PLAN for the 39 live
* PR-Innovations items (prefixes CSO/ENS/FLN/PRO/SKL/TTM/VCO). WHL/Whistler EXCLUDED.
*
* Emits a JSON + human-readable .md that wire the DW reprice formula end-to-end with the
* per-pattern LIST price as the ONE free variable. Writes NOTHING to dw_unified or Shopify.
*
* net_cost = LIST * 0.90 (discount SETTLED 10%, HIGH conf — Sales Order O-111475)
* retail = net_cost / 0.65 / 0.85 (DW standard markup)
* round = Math.round(x*100)/100 (nearest cent) — the SAME convention as the existing
* add-yard/reprice script scripts/innovations-reprice/stage-innov-reprice.mjs
* (RETAIL = c => Math.round((c/0.65/0.85)*100)/100; "same formula as cadence-import").
*
* The gated staging write (price_trade / price_unit / our_price / price_source / price_updated_at)
* fires ONLY after portal creds land (innovationsusa.com acct 58315) AND per-pattern LIST verified.
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
const OUTDIR = path.dirname(new URL(import.meta.url).pathname);
const PREFIXES = ['CSO', 'ENS', 'FLN', 'PRO', 'SKL', 'TTM', 'VCO'];
// ---- settled inputs (from prior TK-11041 sessions; not re-litigated here) -------------------
const DISCOUNT = 0.90; // net_cost = LIST * 0.90 (10% off)
const M1 = 0.65, M2 = 0.85; // retail = net_cost / 0.65 / 0.85
const round2 = x => Math.round(x * 100) / 100; // DW convention (nearest cent)
const netCost = list => round2(list * DISCOUNT);
const retail = list => round2(netCost(list) / M1 / M2);
// ---- read the cohort from the Mac2-local mirror (READ-ONLY) ---------------------------------
const psql = sql => execFileSync('psql',
['-h', '/tmp', '-At', '-F', '\t', '-d', 'dw_unified', '-c', sql], { encoding: 'utf8' }).trim();
const rows = psql(`
SELECT product_url, pattern_name, mfr_sku, color_name,
coalesce(dw_sku,'') , coalesce(width,'') , coalesce(roll_length,'') ,
coalesce(price_unit,'') ,
(price_trade IS NOT NULL OR our_price IS NOT NULL OR price_retail IS NOT NULL) AS has_price
FROM innovations_catalog
WHERE substring(mfr_sku from '^[A-Za-z]+') IN (${PREFIXES.map(p => `'${p}'`).join(',')})
ORDER BY product_url, mfr_sku;`)
.split('\n').filter(Boolean).map(l => {
const [product_url, pattern_name, mfr_sku, color_name, dw_sku, width, roll_length, price_unit, has_price] = l.split('\t');
return { product_url, pattern_name, mfr_sku, color_name, dw_sku, width, roll_length, price_unit,
has_price: has_price === 't' };
});
// ---- integrity checks (flag anything that contradicts the brief) ----------------------------
const flags = [];
if (rows.length !== 39) flags.push(`COHORT COUNT IS ${rows.length}, EXPECTED 39`);
const priced = rows.filter(r => r.has_price);
if (priced.length !== 0) flags.push(`${priced.length}/39 ALREADY CARRY A PRICE: ${priced.map(r => r.mfr_sku).join(', ')}`);
// ---- group by the unique pattern page -------------------------------------------------------
const pagesMap = new Map();
for (const r of rows) {
if (!pagesMap.has(r.product_url)) pagesMap.set(r.product_url, []);
pagesMap.get(r.product_url).push(r);
}
const pages = [...pagesMap.entries()].map(([product_url, items]) => ({
product_url,
pattern_name: items[0].pattern_name,
prefix: items[0].mfr_sku.match(/^[A-Za-z]+/)[0],
width: [...new Set(items.map(i => i.width))].join(' | '),
roll_length: [...new Set(items.map(i => i.roll_length))].join(' | '),
item_count: items.length,
colorways: items.map(i => ({ mfr_sku: i.mfr_sku, color_name: i.color_name })),
}));
// ---- worked PLACEHOLDER example -------------------------------------------------------------
const PLACEHOLDER_LIST = 84.50; // NOT a real Innovations price — illustrative only
const example = {
label: 'PLACEHOLDER — arithmetic demonstration only; NOT a real Innovations price',
applies_to: 'e.g. CSO-001 Radiant (Costine) — any of the 39, formula is identical',
LIST_per_yard: PLACEHOLDER_LIST,
step1_net_cost: { formula: 'round(LIST * 0.90, 2)', value: netCost(PLACEHOLDER_LIST), writes_to: 'price_trade' },
step2_retail: { formula: 'round(net_cost / 0.65 / 0.85, 2)', value: retail(PLACEHOLDER_LIST), writes_to: 'our_price' },
price_unit: 'yard',
};
// ---- assemble the plan ----------------------------------------------------------------------
const now = new Date().toISOString();
const plan = {
ticket: 'TK-11041',
kind: 'dry-run-reprice-plan',
generated_at: now,
read_only: true, no_db_write: true, no_shopify_write: true, no_email: true, no_deploy: true,
source_of_truth: 'Mac2-local dw_unified.innovations_catalog (host=/tmp socket)',
cohort: {
n: rows.length, expected: 39, pattern_pages: pages.length, priced_now: priced.length,
prefixes: PREFIXES,
excluded: '12 WHL/Whistler items (404, no portal page, priced separately)',
},
integrity_flags: flags.length ? flags : ['NONE — 39 items, 7 pages, 0 priced (matches brief)'],
formula: {
settled_inputs: {
discount_pct: 10,
discount_confidence: 'HIGH — Sales Order O-111475',
portal_basis: 'LIST (portal shows list price; discount applies on top)',
},
chain: 'net_cost = LIST * 0.90 ; retail = net_cost / 0.65 / 0.85',
rounding: 'Math.round(x * 100) / 100 (nearest cent, 2 decimals)',
rounding_source: 'scripts/innovations-reprice/stage-innov-reprice.mjs RETAIL() — "same formula as cadence-import"',
free_variable: 'LIST — per-pattern list price, per linear yard, from innovationsusa.com trade portal (acct 58315)',
price_unit: 'yard (Innovations sold PER LINEAR YARD)',
},
one_remaining_input: 'per-pattern LIST price from the innovationsusa.com trade portal (acct 58315). '
+ 'Creds NOT yet pasted, so LIST is unknown — this plan is fully parameterized on it.',
gated_write_fires_only_after: [
'portal creds land (Steve pastes innovationsusa.com acct 58315 login)',
'per-pattern LIST verified on the authenticated page (basis=LIST confirmed, not NET)',
'then: stage price cols, then run the add-yard/reprice (stage-innov-reprice.mjs) — Steve-gated (customer-facing money op)',
],
target_columns: {
price_trade: 'net_cost = round(LIST * 0.90, 2)',
price_unit: "'yard'",
our_price: 'retail = round(price_trade / 0.65 / 0.85, 2)',
price_source: "'innovations portal <YYYY-MM-DD>'",
price_updated_at: 'now()',
},
retail_computed: false,
retail_blocked_reason:
'Per-pattern LIST price not yet available (portal creds unpasted). The two unknowns the pull '
+ 'script guards (DISCOUNT + BASIS) are SETTLED for this plan — discount 10% (O-111475), basis LIST '
+ '— so the ONLY remaining input is the LIST number itself. Once LIST lands per pattern: '
+ 'price_trade = round(LIST*0.90,2); our_price = round(price_trade/0.65/0.85,2).',
worked_example: example,
pattern_pages: pages,
items: rows.map(r => ({
mfr_sku: r.mfr_sku,
pattern_name: r.pattern_name,
color_name: r.color_name,
product_url: r.product_url,
dw_sku: r.dw_sku || null,
width: r.width,
roll_length: r.roll_length,
price_unit_now: r.price_unit || null,
// formula wired symbolically — LIST is the free variable, filled in only after the portal pull
LIST_per_yard: null,
net_cost_formula: 'round(LIST * 0.90, 2) -> price_trade',
retail_formula: 'round(net_cost / 0.65 / 0.85, 2) -> our_price',
retail_computed: false,
retail_blocked_reason: 'awaiting per-pattern LIST from portal (creds unpasted)',
})),
};
fs.writeFileSync(path.join(OUTDIR, 'tk11041-dryrun-reprice-plan.json'), JSON.stringify(plan, null, 2));
// ---- human-readable .md ---------------------------------------------------------------------
const money = v => '$' + v.toFixed(2);
let md = `# TK-11041 — Innovations dry-run reprice plan (READ-ONLY)
_Generated ${now} · Mac2-local dw_unified.innovations_catalog · NO db write · NO Shopify write · NO email · NO deploy_
## Cohort
- **${plan.cohort.n} items** across **${plan.cohort.pattern_pages} unique pattern pages** (prefixes ${PREFIXES.join('/')}).
- **${plan.cohort.priced_now}/39 currently carry a price** (price_trade / our_price / price_retail all NULL).
- Excluded: ${plan.cohort.excluded}.
- Integrity: ${plan.integrity_flags.join('; ')}.
## Formula (LIST is the one free variable)
\`\`\`
net_cost = LIST * 0.90 # discount SETTLED 10%, HIGH conf — Sales Order O-111475
retail = net_cost / 0.65 / 0.85 # DW standard markup
round = Math.round(x*100)/100 # nearest cent (2dp) — matches stage-innov-reprice.mjs / cadence-import
\`\`\`
- Portal basis = **LIST** (discount applies on top). Price unit = **yard** (sold per linear yard).
- Rounding source: \`scripts/innovations-reprice/stage-innov-reprice.mjs\` → \`RETAIL = c => Math.round((c/0.65/0.85)*100)/100\`.
## The ONE remaining input
**Per-pattern LIST price** from the innovationsusa.com trade portal (acct 58315). Creds not yet pasted.
The gated write (stage price cols → then the add-yard/reprice) fires **only after** creds land **and** LIST is verified per pattern.
## Worked EXAMPLE (PLACEHOLDER — not a real Innovations price)
LIST = ${money(PLACEHOLDER_LIST)}/yard →
net_cost = round(${money(PLACEHOLDER_LIST)} × 0.90) = **${money(example.step1_net_cost.value)}** (→ price_trade) →
retail = round(${money(example.step1_net_cost.value)} / 0.65 / 0.85) = **${money(example.step2_retail.value)}** (→ our_price)
## Target columns (staged only after LIST verified — GATED)
| column | value |
|---|---|
| price_trade | round(LIST × 0.90, 2) |
| price_unit | 'yard' |
| our_price | round(price_trade / 0.65 / 0.85, 2) |
| price_source | 'innovations portal <YYYY-MM-DD>' |
| price_updated_at | now() |
\`retail_computed: false\` carried on every row until LIST lands (matches the pull script's safety contract).
## The 7 pattern pages
| # | pattern | prefix | items | width | roll_length | product_url |
|---|---|---|---|---|---|---|
`;
pages.forEach((p, i) => {
md += `| ${i + 1} | ${p.pattern_name} | ${p.prefix} | ${p.item_count} | ${p.width} | ${p.roll_length} | ${p.product_url} |\n`;
});
md += `\n## All 39 items (LIST column blank — filled only after the portal pull)\n`;
md += `| mfr_sku | pattern | colorway | width | roll_length | LIST | net_cost=LIST×0.90 | retail=net/0.65/0.85 |\n`;
md += `|---|---|---|---|---|---|---|---|\n`;
for (const r of rows) {
md += `| ${r.mfr_sku} | ${r.pattern_name} | ${r.color_name} | ${r.width} | ${r.roll_length} | _(pending)_ | round(LIST×0.90) | round(net/0.65/0.85) |\n`;
}
md += `\n_39 rows. dw_sku is NULL in the local mirror for all 39 (identity/spec present; dw_sku not yet stamped here) — noted, not blocking the reprice math._\n`;
fs.writeFileSync(path.join(OUTDIR, 'tk11041-dryrun-reprice-plan.md'), md);
console.log(`cohort=${rows.length} pages=${pages.length} priced=${priced.length}`);
console.log(`flags: ${plan.integrity_flags.join('; ')}`);
console.log(`example: LIST ${money(PLACEHOLDER_LIST)} -> net ${money(example.step1_net_cost.value)} -> retail ${money(example.step2_retail.value)}`);
console.log(`wrote tk11041-dryrun-reprice-plan.json + .md`);