← back to Commercialrealestate
scripts/backfill-broker-of-record.js
99 lines
// backfill-broker-of-record.js — seed the broker-of-record history from current data (TK-10139).
//
// One-time (idempotent, re-run safe) seed of the first observation per address, from two sources:
// 1. data/broker-history-full.json — per-property history (current_broker = "Agent | Firm",
// last_deed_date as the observed_at anchor when present).
// 2. data/ranked.json — the enriched deals with broker_firm / broker_agent /
// source_of_truth / source_host (the broker-of-record + firm-direct rows).
//
// Because the recorder only INSERTs on a change vs the latest row, re-running is safe (unchanged
// observations no-op). Also writes data/last-broker-of-record.json (address_key -> last broker/firm)
// so the mls.html grid can merge the two new columns without a live server round-trip.
//
// Usage: node scripts/backfill-broker-of-record.js
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const { recordBrokerOfRecord, addressKey, historyFor, DB_PATH } = require('./broker-of-record-recorder');
const readJSON = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } };
// broker-history-full "current_broker" is EITHER a string "Agent | Firm" (agent first) OR an
// object { name, firm, ... }. Normalize both to { agent, firm }.
function splitCurrentBroker(s) {
if (s && typeof s === 'object') {
const agent = s.name != null ? String(s.name).trim() : '';
const firm = s.firm != null ? String(s.firm).trim() : '';
return { agent: agent || null, firm: firm || null };
}
const str = String(s == null ? '' : s).trim();
if (!str) return { agent: null, firm: null };
const parts = str.split('|').map(x => x.trim()).filter(Boolean);
if (parts.length >= 2) return { agent: parts[0], firm: parts.slice(1).join(' | ') };
return { agent: parts[0] || null, firm: null };
}
(function main() {
const stat = { seenHistory: 0, seenRanked: 0, recorded: 0, unchanged: 0, skipped: 0 };
const seen = new Set(); // address_keys touched, for the static map
// ── 1) broker-history-full.json (object keyed by property id, OR array) ──────────────────────
const bh = readJSON(path.join(ROOT, 'data', 'broker-history-full.json'));
const bhRows = Array.isArray(bh) ? bh
: (bh && Array.isArray(bh.properties)) ? bh.properties // { meta, properties:[...] } shape
: (bh && Array.isArray(bh.history)) ? bh.history
: (bh && typeof bh === 'object') ? Object.values(bh).filter(v => v && typeof v === 'object' && (v.address || v.current_broker))
: [];
for (const row of bhRows) {
if (!row || typeof row !== 'object') continue;
stat.seenHistory++;
const { agent, firm } = splitCurrentBroker(row.current_broker);
if (!agent && !firm) { stat.skipped++; continue; }
// observed_at anchor: last_deed_date if present (as the "first observation" time), else now.
const observed_at = row.last_deed_date ? new Date(row.last_deed_date + 'T00:00:00Z').toISOString() : undefined;
const r = recordBrokerOfRecord({
address: row.address, city: row.city, ain: row.ain || null,
broker_firm: firm, broker_agent: agent,
source_of_truth: 'broker-of-record', source_host: row.broker_source || null, observed_at,
});
if (r.recorded) stat.recorded++; else if (r.reason === 'unchanged') stat.unchanged++; else stat.skipped++;
if (r.address_key) seen.add(r.address_key);
}
// ── 2) ranked.json (enriched deals with a real broker-of-record) ─────────────────────────────
const ranked = readJSON(path.join(ROOT, 'data', 'ranked.json'));
const deals = (ranked && ranked.ranked) || [];
for (const d of deals) {
if (!d || (!d.broker_firm && !d.broker_agent)) { if (d) stat.skipped++; continue; }
stat.seenRanked++;
const r = recordBrokerOfRecord({
address: d.address, city: d.city, ain: d.ain || null,
broker_firm: d.broker_firm || null, broker_agent: d.broker_agent || null,
source_of_truth: d.source_of_truth || null, source_host: d.source_host || null,
});
if (r.recorded) stat.recorded++; else if (r.reason === 'unchanged') stat.unchanged++; else stat.skipped++;
if (r.address_key) seen.add(r.address_key);
}
// ── 3) static map for the grid: address_key -> { last_broker_of_record, last_firm_of_record, observed_at } ──
const map = {};
for (const key of seen) {
const rows = historyFor(key); // newest first
if (!rows.length) continue;
const top = rows[0];
map[key] = {
last_broker_of_record: top.broker_agent || null, // agent = the broker of record
last_firm_of_record: top.broker_firm || null, // firm = the firm of record
observed_at: top.observed_at || null,
source_of_truth: top.source_of_truth || null,
history_count: rows.length,
};
}
const mapPath = path.join(ROOT, 'data', 'last-broker-of-record.json');
fs.writeFileSync(mapPath, JSON.stringify({ generated_at: new Date().toISOString(), by_address_key: map }, null, 0));
const total = require('better-sqlite3')(DB_PATH).prepare('SELECT count(*) n FROM broker_of_record_history').get().n;
console.log(JSON.stringify({ ...stat, addresses_in_map: Object.keys(map).length, table_rows: total, db: DB_PATH, map: mapPath }, null, 2));
})();