← back to Hollywood Import
generate-stage-report.mjs
69 lines
// Build staged price updates + dry-run report from crawl + vendor_catalog export. READ-ONLY
// (writes only local files). hw_price = round(list × 1.448, 2). Flags anomalies for review.
import fs from 'node:fs';
const MULT = 1.448;
const r2 = (n) => Math.round(n * 100) / 100;
const map = JSON.parse(fs.readFileSync(new URL('pattern-price-map.json', import.meta.url)));
const rows = fs.readFileSync(new URL('vc_hollywood.tsv', import.meta.url), 'utf8').trim().split('\n').map(l => l.split('\t'));
// pattern_slugs with >1 price across cards = ambiguous → exclude from auto-apply
const COLLISION = new Set(['capri']);
const updates = [], discontinued = [], noSlug = [], flags = [];
let unchanged = 0;
for (const [id, dw, mfr, pat, col, slug, list, hw, uom] of rows) {
if (!slug) { noSlug.push({ id, dw, mfr, pat, col }); continue; }
const ps = slug.split('-')[0];
const hit = map[ps];
if (!hit) { discontinued.push({ id, dw, mfr, pat, col, slug, oldList: list }); continue; }
const newList = hit.price;
const newHw = r2(newList * MULT);
const oldList = list ? parseFloat(list) : null;
const oldHw = hw ? parseFloat(hw) : null;
const listChanged = oldList == null || Math.abs(oldList - newList) > 0.005;
const hwChanged = oldHw == null || Math.abs(oldHw - newHw) > 0.005;
if (!listChanged && !hwChanged) { unchanged++; continue; }
const rec = { id: +id, dw_sku: dw, mfr_sku: mfr, slug, pattern_slug: ps,
oldList, newList, oldHw, newHw, unit: hit.unit, oldUom: uom, momentumPattern: hit.pattern };
// anomaly flags
if (COLLISION.has(ps)) { rec.flag = 'ambiguous_pattern_slug_multi_price'; flags.push(rec); continue; }
if (oldList != null && oldList > 0 && Math.abs(newList - oldList) / oldList > 0.25)
rec.flag = 'big_swing_' + Math.round((newList - oldList) / oldList * 100) + '%';
if (hit.unit && hit.unit !== 'YD') rec.flag = (rec.flag ? rec.flag + ';' : '') + 'unit_' + hit.unit;
if (rec.flag) flags.push(rec);
updates.push(rec);
}
// write staged updates (auto-appliable) + SQL for Kamatera
fs.writeFileSync(new URL('stage-updates.jsonl', import.meta.url), updates.map(u => JSON.stringify(u)).join('\n'));
const sql = updates.map(u =>
`UPDATE vendor_catalog SET specs = jsonb_set(jsonb_set(jsonb_set(specs,'{list_price}','"${u.newList}"'),'{hw_price}','"${u.newHw}"'),'{retail_price}','"${u.newHw}"') WHERE id=${u.id};`
).join('\n');
fs.writeFileSync(new URL('apply-vendor-catalog.sql', import.meta.url),
'-- RUN ON KAMATERA (canonical). vendor_catalog is replicated FROM Kamatera → Mac2.\n' +
'-- Refreshes list_price + hw_price(=list×1.448) + retail_price for ' + updates.length + ' rows.\nBEGIN;\n' + sql + '\nCOMMIT;\n');
const report = {
generated: 'dry-run (no writes)',
multiplier: MULT,
totals: {
vendor_catalog_rows: rows.length,
matched_live_momentum: updates.length + unchanged + flags.filter(f=>f.flag?.startsWith('ambig')).length,
to_update: updates.length,
unchanged_confirmed: unchanged,
discontinued_on_momentum: discontinued.length,
no_momentum_slug_unmatched: noSlug.length,
},
flags_count: flags.length,
unit_breakdown: updates.reduce((a,u)=>{a[u.unit]=(a[u.unit]||0)+1;return a;},{}),
sample_changes: updates.filter(u=>!u.flag).slice(0, 10),
flagged_samples: flags.slice(0, 15),
};
fs.writeFileSync(new URL('momentum-price-report.json', import.meta.url), JSON.stringify(report, null, 2));
// discontinued + noSlug lists for Steve
fs.writeFileSync(new URL('discontinued-on-momentum.tsv', import.meta.url),
'dw_sku\tmfr_sku\tpattern\tcolor\tslug\toldList\n' + discontinued.map(d=>[d.dw,d.mfr,d.pat,d.col,d.slug,d.oldList].join('\t')).join('\n'));
fs.writeFileSync(new URL('unmatched-no-slug.tsv', import.meta.url),
'dw_sku\tmfr_sku\tpattern\tcolor\n' + noSlug.map(d=>[d.dw,d.mfr,d.pat,d.col].join('\t')).join('\n'));
console.log(JSON.stringify(report, null, 2));