← back to Dwla Rebrand
scripts/build_dryrun.js
192 lines
#!/usr/bin/env node
// Dry-run builder for DWLA rebrand.
// Phase 1: 17 PI-PAMPAS-* registry rows + their 17 dup `-1` Shopify records.
// Output: preview/phase1.html + preview/phase1.csv + preview/phase1.json
// Reads dw_unified PG. Does NOT touch Shopify. Does NOT mutate registry.
//
// Run: node scripts/build_dryrun.js [--phase 1]
const { Client } = require('pg');
const fs = require('fs');
const path = require('path');
const PHASE = parseInt((process.argv.find(a => a.startsWith('--phase=')) || '--phase=1').split('=')[1], 10);
const ROOT = path.join(__dirname, '..');
const PREVIEW_DIR = path.join(ROOT, 'preview');
fs.mkdirSync(PREVIEW_DIR, { recursive: true });
const NAME_MAP = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'la_name_map.json'), 'utf8')).map;
const NEXT_DWLA_FILE = path.join(ROOT, 'data', 'dwla_sequence.txt');
let nextDwla = 100001;
if (fs.existsSync(NEXT_DWLA_FILE)) nextDwla = parseInt(fs.readFileSync(NEXT_DWLA_FILE, 'utf8').trim(), 10);
function allocateDwla() {
const sku = `DWLA-${String(nextDwla).padStart(6, '0')}`;
nextDwla++;
return sku;
}
function rebrandTitle(t) {
if (!t) return t;
let out = t;
// Replace "| Phillipe Romano" suffix with "| Los Angeles Fabrics"
out = out.replace(/\|\s*Phillipe Romano\s*$/i, '| Los Angeles Fabrics');
out = out.replace(/\bPhillipe Romano\b/g, 'Los Angeles Fabrics');
// Apply East-Coast → LA name map (longest-first match to avoid "Bar Harbor" matching "Bar")
const keys = Object.keys(NAME_MAP).sort((a, b) => b.length - a.length);
for (const k of keys) {
if (NAME_MAP[k] === k) continue;
const re = new RegExp(`\\b${k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
out = out.replace(re, NAME_MAP[k]);
}
return out;
}
// Option (b) — handle is derived from the new title, NOT from the old handle.
// New handle = slug(pattern) + "-" + slug(colorway) + "-los-angeles-fabrics"
// Pattern + colorway come from the title structure: "Pampas - Aqua Sustainable Bio-Based | Phillipe Romano"
function buildHandleFromTitle(title, colorway, isDup) {
if (!title) return '';
// Pattern is whatever comes before the first " - "
const pattern = (title.split(' - ')[0] || '').trim();
const slug = (s) => s.toLowerCase()
.replace(/['']/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const base = `${slug(pattern)}-${slug(colorway)}-los-angeles-fabrics`;
return isDup ? `${base}-1` : base;
}
async function main() {
const c = new Client({ host: '/tmp', database: 'dw_unified' });
await c.connect();
// Phase 1 = the 17 PI-PAMPAS-* rows + their corresponding dup Shopify products
const registryRows = (await c.query(`
SELECT id, dw_sku, vendor_prefix, vendor_name, mfr_sku, shopify_product_id
FROM dw_sku_registry
WHERE mfr_sku LIKE 'PI-PAMPAS-%'
ORDER BY mfr_sku
`)).rows;
// For each registry row, find ALL Shopify products whose title matches the colorway
const records = [];
for (const r of registryRows) {
const colorway = r.mfr_sku.replace(/^PI-PAMPAS-/, '').toLowerCase();
const colorwayPretty = colorway.charAt(0).toUpperCase() + colorway.slice(1);
const shopify = (await c.query(`
SELECT id, handle, title, sku, vendor
FROM shopify_products
WHERE vendor = 'Phillipe Romano'
AND title ILIKE '%pampas%spring%' = false -- placeholder; replaced below
AND (title ILIKE $1 OR title ILIKE $2)
ORDER BY id
`.replace("title ILIKE '%pampas%spring%' = false -- placeholder; replaced below\n AND ", ""),
[`%pampas%${colorwayPretty}%`, `%pampas%${colorway}%`])).rows;
const dwlaSku = allocateDwla();
records.push({
registry: r,
colorway: colorwayPretty,
proposed_dwla_sku: dwlaSku,
shopify_products: shopify.map(s => {
const isDup = s.handle.endsWith('-1');
const newTitle = rebrandTitle(s.title);
return {
old_id: s.id,
old_handle: s.handle,
old_sku: s.sku,
old_vendor: s.vendor,
old_title: s.title,
new_handle: buildHandleFromTitle(newTitle, colorwayPretty, isDup),
new_sku: dwlaSku,
new_vendor: 'Los Angeles Fabrics',
new_title: newTitle,
action: isDup ? 'archive_duplicate' : 'rename_canonical'
};
})
});
}
await c.end();
// Persist allocated sequence
fs.writeFileSync(NEXT_DWLA_FILE, String(nextDwla));
// Write JSON
const outJson = path.join(PREVIEW_DIR, `phase${PHASE}.json`);
fs.writeFileSync(outJson, JSON.stringify(records, null, 2));
// Write CSV
const outCsv = path.join(PREVIEW_DIR, `phase${PHASE}.csv`);
const csvLines = ['action,registry_dw_sku,registry_mfr_sku,proposed_dwla_sku,shopify_id,old_handle,new_handle,old_sku,new_sku,old_vendor,new_vendor,old_title,new_title'];
for (const rec of records) {
for (const sp of rec.shopify_products) {
csvLines.push([
sp.action, rec.registry.dw_sku, rec.registry.mfr_sku, rec.proposed_dwla_sku,
sp.old_id, sp.old_handle, sp.new_handle,
sp.old_sku, sp.new_sku, sp.old_vendor, sp.new_vendor,
JSON.stringify(sp.old_title), JSON.stringify(sp.new_title)
].join(','));
}
}
fs.writeFileSync(outCsv, csvLines.join('\n'));
// Write HTML
const outHtml = path.join(PREVIEW_DIR, `phase${PHASE}.html`);
let html = `<!doctype html>
<html><head><meta charset="utf-8"><title>DWLA Rebrand · Phase ${PHASE} preview</title>
<style>
body{font:13px/1.45 -apple-system,Segoe UI,Helvetica,Arial,sans-serif;margin:24px;background:#fafaf7;color:#222}
h1{font-weight:600;font-size:22px}
.meta{color:#666;margin-bottom:24px}
table{border-collapse:collapse;width:100%;margin:14px 0;font-size:12px}
th,td{padding:6px 8px;border-bottom:1px solid #eee;vertical-align:top;text-align:left}
th{background:#f0ede4;font-weight:600;color:#444}
.action-archive{color:#a44;font-weight:500}
.action-canonical{color:#262;font-weight:500}
.old{color:#888;text-decoration:line-through;font-family:ui-monospace,monospace;font-size:11px}
.new{color:#262;font-family:ui-monospace,monospace;font-size:11px}
.sku{font-family:ui-monospace,monospace}
.warn{background:#fffbe7;border:1px solid #f0e1a8;padding:10px 12px;margin:12px 0;border-radius:6px}
.ok{background:#e9f6ec;border:1px solid #b3dfb8;padding:10px 12px;margin:12px 0;border-radius:6px}
</style></head><body>
<h1>DWLA Rebrand · Phase ${PHASE} dry-run preview</h1>
<div class="meta">Generated ${new Date().toISOString()} · ${records.length} registry rows · ${records.reduce((a,r)=>a+r.shopify_products.length,0)} Shopify products affected</div>
<div class="warn">
<strong>NO Shopify writes have happened.</strong> This is a dry-run. Approve below to generate <code>push_phase${PHASE}.js</code>; review that before execution.
</div>`;
for (const rec of records) {
html += `<h3>${rec.registry.dw_sku} · ${rec.registry.mfr_sku} · → <span class="sku">${rec.proposed_dwla_sku}</span></h3>`;
if (rec.shopify_products.length === 0) {
html += `<p style="color:#a44">No Shopify products matched.</p>`;
continue;
}
html += `<table><tr><th>Action</th><th>Shopify ID</th><th>Old → New handle</th><th>Old → New SKU</th><th>Title</th></tr>`;
for (const sp of rec.shopify_products) {
const cls = sp.action === 'archive_duplicate' ? 'action-archive' : 'action-canonical';
html += `<tr>
<td class="${cls}">${sp.action}</td>
<td>${sp.old_id}</td>
<td><div class="old">${sp.old_handle}</div><div class="new">${sp.new_handle}</div></td>
<td><div class="old">${sp.old_sku}</div><div class="new">${sp.new_sku}</div></td>
<td><div class="old">${sp.old_title}</div><div class="new">${sp.new_title}</div></td>
</tr>`;
}
html += `</table>`;
}
html += `<div class="ok"><strong>Next step:</strong> review the diffs above. If all looks correct, reply "GO phase 1" and I'll write <code>push_phase1.js</code> for your final approval before any Shopify call.</div></body></html>`;
fs.writeFileSync(outHtml, html);
console.log(`✓ phase ${PHASE} preview written`);
console.log(` ${outJson}`);
console.log(` ${outCsv}`);
console.log(` ${outHtml}`);
console.log(` ${records.length} registry rows · ${records.reduce((a,r)=>a+r.shopify_products.length,0)} Shopify products`);
console.log(` next DWLA seq: ${nextDwla}`);
}
main().catch(e => { console.error(e); process.exit(1); });