← back to Re Flyer Aggregator
scripts/build-normalized-flyers.mjs
133 lines
#!/usr/bin/env node
// TK-10708 Build data/flyers-normalized.json — the NORMALIZED cross-source flyer index.
// CONSOLIDATED into re-flyer-aggregator (2026-08-25, decision B): this was the standalone
// ~/Projects/re-flyers scaffold; it only ever read THIS project's own outputs + local PG,
// so it now lives here as a script. The standalone project is retired.
//
// Reads the EXISTING, already-produced flyer/marketing-asset outputs from this project
// (does NOT re-scrape anything) and normalizes them into one common schema, deduped.
// $0 local, read-only against this project's files + local PG.
//
// Common schema (per TK-10708):
// { source_build, listing_id, title, address, flyer_url, generated_at, asset_type,
// rights_basis, tier, dedup_key }
//
// Sources unified:
// A. public/flyers-found/property-flyers.json (Tier-1 broker OMs/brochures)
// B. reflyers PG external_marketing_asset (classified deal assets)
// C. out/spotlight-*.html (Tier-2 self-generated recaps)
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { createHash } from 'node:crypto';
// This script lives in re-flyer-aggregator/scripts/, so ROOT = the aggregator project root.
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const AGG_DIR = ROOT; // sources are THIS project's own outputs (post-consolidation)
const OUT = join(ROOT, 'data', 'flyers-normalized.json');
const rows = [];
const dedup = new Set();
function push(r) {
const key = (r.flyer_url || r.title || '').trim().toLowerCase();
r.dedup_key = createHash('sha1').update(key).digest('hex').slice(0, 12);
if (dedup.has(r.dedup_key)) return false;
dedup.add(r.dedup_key);
rows.push(r);
return true;
}
// ---- Source A: broker-owned property flyers (Tier-1, real OMs/brochures) ----
try {
const p = join(AGG_DIR, 'public', 'flyers-found', 'property-flyers.json');
if (existsSync(p)) {
const arr = JSON.parse(readFileSync(p, 'utf8'));
for (const f of arr) {
const title = (f.property || '').trim();
const isOM = /\bOM\b|offering.?memo/i.test(title) || /_OM_/i.test(f.pdf || '');
push({
source_build: 'broker-site',
listing_id: null,
title: title || 'Untitled brochure',
address: null, // broker flyers carry a property name, not a normalized address
flyer_url: f.pdf || f.from || null,
generated_at: f.found_at || null,
asset_type: isOM ? 'offering_memorandum' : 'marketing_flyer',
rights_basis: 'first_party_broker',
tier: 1,
firm: f.firm || null,
});
}
}
} catch (e) { console.error('source A skipped:', e.message); }
// ---- Source B: classified deal assets from the reflyers staging DB ----
try {
const dbList = execFileSync('psql', ['-lqt'], { encoding: 'utf8' });
if (/\breflyers\b/.test(dbList)) {
const json = execFileSync('psql', ['reflyers', '-tAc',
`SELECT row_to_json(t) FROM (
SELECT asset_type, tier, rights_basis, title, source_landing_url,
document_url, last_verified_at, created_at
FROM external_marketing_asset) t`], { encoding: 'utf8' }).trim();
for (const line of json.split('\n').filter(Boolean)) {
const a = JSON.parse(line);
push({
source_build: 'usre-deal',
listing_id: null,
title: a.title || 'Deal asset',
address: null,
flyer_url: a.document_url || a.source_landing_url || null,
generated_at: a.last_verified_at || a.created_at || null,
asset_type: a.asset_type,
rights_basis: a.rights_basis || null,
tier: a.tier,
});
}
}
} catch (e) { console.error('source B skipped:', e.message); }
// ---- Source C: Tier-2 self-generated spotlight recaps (our own flyers) ----
try {
const outDir = join(AGG_DIR, 'out');
if (existsSync(outDir)) {
for (const fn of readdirSync(outDir)) {
if (!/^spotlight-.*\.html$/.test(fn)) continue;
const full = join(outDir, fn);
let title = fn.replace(/\.html$/, '');
try {
const html = readFileSync(full, 'utf8');
const m = html.match(/<title>([^<]+)<\/title>/i);
if (m) title = m[1].trim();
} catch {}
const st = statSync(full);
push({
source_build: 'rentv-spotlight',
listing_id: fn.replace(/^spotlight-|\.html$/g, ''),
title,
address: null,
flyer_url: 'file://' + full, // local Tier-2 artifact (print → PDF)
generated_at: st.mtime.toISOString(),
asset_type: 'property_spotlight',
rights_basis: 'self_generated',
tier: 2,
});
}
}
} catch (e) { console.error('source C skipped:', e.message); }
// sort newest-first (nulls last)
rows.sort((a, b) => (b.generated_at || '').localeCompare(a.generated_at || ''));
const payload = {
generated_at: new Date().toISOString(),
count: rows.length,
by_source: rows.reduce((m, r) => ((m[r.source_build] = (m[r.source_build] || 0) + 1), m), {}),
note: 'Normalized cross-source flyer index (TK-10708). Read-only aggregate of existing re-flyer-aggregator outputs; no re-scrape. Tier-3/GATED assets carried with rights_basis for viewer filtering, never auto-downloaded.',
flyers: rows,
};
writeFileSync(OUT, JSON.stringify(payload, null, 2));
console.log(`wrote ${OUT}: ${rows.length} flyers by_source=${JSON.stringify(payload.by_source)}`);