← back to Commercialrealestate

scripts/backfill-broker-contacts.js

79 lines

#!/usr/bin/env node
// backfill-broker-contacts.js — FREE ($0) broker-contact backfill for the NON-Showcase broker-direct
// feeds, because "if you're on their site, grab the contact" (Steve 2026-08-19). These sites aren't
// Akamai-blocked, so plain curl works — no browser, no browserbase, no cost.
//   • WESTMAC   → each detail page has a structured .agent-box: agent name + DIRECT phone + email. Fetch per listing.
//   • Lyon Stahl→ every listing shows the same office line (310) 425-9838 + leadership@ — firm-level, set instantly (no fetch).
//   • BuildOut  → the feed already carries broker name+email; add the firm's known phone.
// Resumable (_contactBackfilled). Writes back into each data/<firm>-listings.json.
// Usage: node scripts/backfill-broker-contacts.js
'use strict';
const fs = require('fs');
const path = require('path');
const DATA = path.join(__dirname, '..', 'data');
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36';
const clean = s => String(s == null ? '' : s).replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n)).replace(/&amp;/g, '&').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
const fmtPhone = p => { const d = String(p || '').replace(/\D/g, '').replace(/^1(?=\d{10}$)/, ''); return d.length === 10 ? `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}` : ''; };
const load = f => { try { return JSON.parse(fs.readFileSync(path.join(DATA, f), 'utf8')); } catch (_) { return null; } };
const save = (f, d) => fs.writeFileSync(path.join(DATA, f), JSON.stringify(d));

// ── Lyon Stahl: firm-level office line on every listing (no fetch needed) ──────────────
function backfillLyonStahl() {
  const doc = load('lyonstahl-listings.json'); if (!doc) return;
  let n = 0;
  for (const r of doc.listings) {
    if (r.broker_phone) continue;
    r.broker_phone = '(310) 425-9838';                  // Lyon Stahl office (on every property page)
    r.broker_email = r.broker_email || 'leadership@lyonstahl.com';
    r.broker_firm = r.broker_firm || 'Lyon Stahl Investment Real Estate';
    r._contactBackfilled = true; n++;
  }
  save('lyonstahl-listings.json', doc);
  console.log(`Lyon Stahl: ${n} listings set to firm office line ($0, no fetch).`);
}

// ── BuildOut: feed has name+email; add the firm phone (CBC Blair) ──────────────────────
function backfillBuildout() {
  const doc = load('buildout-listings.json'); if (!doc) return;
  const FIRM_PHONE = { 'Coldwell Banker Commercial BLAIR Westmac': '(562) 495-6070' };  // CBC Blair Long Beach office
  let n = 0;
  for (const r of doc.listings) {
    if (r.broker_phone) continue;
    const ph = FIRM_PHONE[r.broker_firm]; if (ph) { r.broker_phone = ph; r._contactBackfilled = true; n++; }
  }
  save('buildout-listings.json', doc);
  console.log(`BuildOut: ${n} listings set to firm phone ($0).`);
}

// ── WESTMAC: per-listing agent name + direct phone + email from the .agent-box on each detail page ──
async function backfillWestmac() {
  const doc = load('westmac-listings.json'); if (!doc) return;
  const todo = doc.listings.filter(r => r.source && !r.broker_phone && !r._contactBackfilled);
  let n = 0, hit = 0;
  for (const r of todo) {
    try {
      const html = await fetch(r.source, { headers: { 'User-Agent': UA } }).then(x => x.ok ? x.text() : '');
      // first .agent-box block on the page = the listing's lead agent
      const box = (html.match(/agent-box__agent__name[\s\S]{0,600}/) || [''])[0];
      const name = clean((box.match(/agent-box__agent__name"><a[^>]*>([^<]+)</) || [])[1] || '');
      const email = (box.match(/mailto:([^"?]+)/) || [])[1] || '';
      const phone = fmtPhone((box.match(/tel:([()0-9 .\-]{10,18})/) || [])[1] || '');
      r._contactBackfilled = true;
      if (name) { r.broker_agent = name; r.broker_agents = [name]; }
      if (email) r.broker_email = email;
      if (phone) { r.broker_phone = phone; hit++; }
    } catch (_) { r._contactBackfilled = true; }
    n++;
    if (n % 10 === 0) { save('westmac-listings.json', doc); process.stdout.write(`  westmac ${n}/${todo.length} (${hit} phones)\r`); }
  }
  save('westmac-listings.json', doc);
  console.log(`\nWESTMAC: ${n} detail pages, ${hit} direct agent phones ($0 curl).`);
}

(async () => {
  backfillLyonStahl();
  backfillBuildout();
  await backfillWestmac();
  console.log('broker-contact backfill done.');
})().catch(e => { console.error('backfill-broker-contacts FAILED:', e.message); process.exit(1); });