← back to Commercialrealestate

scripts/broker-of-record-recorder.js

104 lines

// broker-of-record-recorder.js — append-only recorder for the broker/firm of record per address.
//
// Steve's ask (TK-10139): "start recording every update to any address" for the broker + firm of record,
// and surface the "Last Broker of Record" / "Last Firm of Record" per property. This module owns the
// history store (data/broker-of-record.sqlite, table broker_of_record_history) and the read helpers.
//
// Behaviour of recordBrokerOfRecord(obs):
//   1. compute address_key = normalize(address + ", " + city)
//   2. look up the MOST RECENT row for that address_key
//   3. if NONE exists, OR broker_firm/broker_agent differs from that latest row -> INSERT a new row
//      (observed_at = now). Otherwise NO-OP (unchanged observations are not recorded).
//   4. observations with BOTH broker_firm and broker_agent empty/null are skipped.
// This is what makes the log record every *update* (a change), not every observation.
//
// $0, local, additive. better-sqlite3 (already a dep, used by serve.js for the assessor roll).
'use strict';
const path = require('path');
const fs = require('fs');
const ROOT = path.join(__dirname, '..');
const DB_PATH = path.join(ROOT, 'data', 'broker-of-record.sqlite');
const SCHEMA_PATH = path.join(__dirname, 'db', 'broker-of-record-history.sql');

// Lazily-opened writable handle (read-write, created on first use). Shared across calls in a process.
let _db = null;
function db() {
  if (_db) return _db;
  const Database = require('better-sqlite3');
  _db = new Database(DB_PATH);                 // read-write; file created if absent
  _db.pragma('journal_mode = WAL');
  const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
  _db.exec(schema);                            // idempotent (CREATE TABLE/INDEX IF NOT EXISTS)
  return _db;
}

// Normalize an address into a stable key: "<address>, <city>" lowercased, trimmed, whitespace collapsed.
function addressKey(address, city) {
  const a = String(address == null ? '' : address).trim();
  const c = String(city == null ? '' : city).trim();
  const joined = c ? `${a}, ${c}` : a;
  return joined.toLowerCase().replace(/\s+/g, ' ').trim();
}

const _clean = (v) => { const s = v == null ? '' : String(v).trim(); return s === '' ? null : s; };
const _eq = (a, b) => (_clean(a) || '') === (_clean(b) || '');

// Explicit column list (no SELECT *) — satisfies the deploy PII-leak lint gate.
const COLS = 'id, address_key, address, city, ain, broker_firm, broker_agent, source_of_truth, source_host, observed_at';

// Newest row for an address_key, or null.
function latestRow(address_key) {
  return db().prepare(
    `SELECT ${COLS} FROM broker_of_record_history WHERE address_key = ? ORDER BY observed_at DESC, id DESC LIMIT 1`
  ).get(address_key) || null;
}

// Record an observation IFF it's new or a change. Returns { recorded:bool, reason, id?, address_key }.
function recordBrokerOfRecord(obs) {
  const o = obs || {};
  const broker_firm = _clean(o.broker_firm);
  const broker_agent = _clean(o.broker_agent);
  // Skip empty observations — no broker of record to record.
  if (!broker_firm && !broker_agent) return { recorded: false, reason: 'empty', address_key: null };

  const address_key = addressKey(o.address, o.city);
  if (!address_key) return { recorded: false, reason: 'no-address', address_key: null };

  const prev = latestRow(address_key);
  if (prev && _eq(prev.broker_firm, broker_firm) && _eq(prev.broker_agent, broker_agent)) {
    return { recorded: false, reason: 'unchanged', address_key };   // no-op: same broker/firm as latest
  }

  const observed_at = _clean(o.observed_at) || new Date().toISOString();
  const info = db().prepare(
    `INSERT INTO broker_of_record_history
       (address_key, address, city, ain, broker_firm, broker_agent, source_of_truth, source_host, observed_at)
     VALUES (@address_key, @address, @city, @ain, @broker_firm, @broker_agent, @source_of_truth, @source_host, @observed_at)`
  ).run({
    address_key,
    address: _clean(o.address),
    city: _clean(o.city),
    ain: _clean(o.ain),
    broker_firm,
    broker_agent,
    source_of_truth: _clean(o.source_of_truth),
    source_host: _clean(o.source_host),
    observed_at,
  });
  return { recorded: true, reason: prev ? 'changed' : 'new', id: info.lastInsertRowid, address_key };
}

// Newest history row for an address_key (the "Last" broker/firm of record). Null if none.
function lastBrokerOfRecord(address_key) {
  return latestRow(address_key);
}

// All history rows for an address_key, newest first.
function historyFor(address_key) {
  return db().prepare(
    `SELECT ${COLS} FROM broker_of_record_history WHERE address_key = ? ORDER BY observed_at DESC, id DESC`
  ).all(address_key);
}

module.exports = { recordBrokerOfRecord, lastBrokerOfRecord, historyFor, addressKey, DB_PATH };