[object Object]

← back to Commercialrealestate

fetch-redfin-agents (condo): record ALL license numbers per listing (multi-agent, role listing/co-listing) — mirrors the SFR + FHA-condo fix; completes multi-agent capture across all 3 Redfin listing scrapers

1e8ea8f5000e491d2a6b2a0ecf9e59a0af6745de · 2026-08-03 11:42:08 -0700 · Steve Abrams

Files touched

Diff

commit 1e8ea8f5000e491d2a6b2a0ecf9e59a0af6745de
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 11:42:08 2026 -0700

    fetch-redfin-agents (condo): record ALL license numbers per listing (multi-agent, role listing/co-listing) — mirrors the SFR + FHA-condo fix; completes multi-agent capture across all 3 Redfin listing scrapers
---
 scripts/fetch-redfin-agents.js | 75 ++++++++++++++++++++++++++----------------
 1 file changed, 47 insertions(+), 28 deletions(-)

diff --git a/scripts/fetch-redfin-agents.js b/scripts/fetch-redfin-agents.js
index 99dfe3c..6af668d 100644
--- a/scripts/fetch-redfin-agents.js
+++ b/scripts/fetch-redfin-agents.js
@@ -37,23 +37,33 @@ const strip = s => s.replace(/^[)\]}'&\s]*\{\}&&/, '').replace(/^[)\]}'\s]+/, ''
 const pn = v => (v && typeof v === 'object' ? v.phoneNumber : v) || null;   // phone object -> string
 
 // Pull the residential listing agent out of a mainHouseInfo payload. Returns null when suppressed.
-function extractAgent(payloadText) {
-  let j; try { j = JSON.parse(strip(payloadText)); } catch { return { parseErr: true }; }
+// Return EVERY listing agent (Steve 2026-08-03: record all license numbers), not just [0] — the
+// primary AND any co-listing agent, each with its own license. Deduped by license||name.
+function extractAgents(payloadText) {
+  let j; try { j = JSON.parse(strip(payloadText)); } catch { return { parseErr: true, agents: [] }; }
   const mh = (j.payload && j.payload.mainHouseInfo) || (j.mainHouseInfo) || null;
-  if (!mh) return { noPayload: true };
-  const la = Array.isArray(mh.listingAgents) ? mh.listingAgents[0] : null;
-  if (!la) return { suppressed: true };
-  const ai = la.agentInfo || {};
-  const name = (ai.agentName || '').trim();
-  if (!name || ai.isAgentNameBlank) return { suppressed: true };
-  return {
-    name,
-    brokerage: (la.brokerName || '').trim() || null,
-    license: (la.license || '').trim() || null,
-    phone: pn(la.agentPhoneNumber) || pn(la.brokerPhoneNumber) || null,
-    email: (la.agentEmailAddress || la.brokerEmailAddress || '').trim() || null,
-    isRedfinAgent: !!ai.isRedfinAgent
-  };
+  if (!mh) return { noPayload: true, agents: [] };
+  const list = Array.isArray(mh.listingAgents) ? mh.listingAgents : [];
+  if (!list.length) return { suppressed: true, agents: [] };
+  const agents = [], seen = new Set();
+  for (const la of list) {
+    if (!la) continue;
+    const ai = la.agentInfo || {};
+    const name = (ai.agentName || '').trim();
+    if (!name || ai.isAgentNameBlank) continue;
+    const a = {
+      name,
+      brokerage: (la.brokerName || '').trim() || null,
+      license: (la.license || '').trim() || null,
+      phone: pn(la.agentPhoneNumber) || pn(la.brokerPhoneNumber) || null,
+      email: (la.agentEmailAddress || la.brokerEmailAddress || '').trim() || null,
+      isRedfinAgent: !!ai.isRedfinAgent
+    };
+    const k = a.license || a.name;
+    if (!seen.has(k)) { seen.add(k); agents.push(a); }
+  }
+  if (!agents.length) return { suppressed: true, agents: [] };
+  return { agents };
 }
 
 async function loadTargets() {
@@ -70,7 +80,7 @@ async function loadTargets() {
   return r.rows.filter(x => x.pid && x.pid !== x.source);
 }
 
-async function persistAgent(condo, a, sourceUrl) {
+async function persistAgent(condo, a, sourceUrl, role = 'listing') {
   const firmId = a.brokerage ? await brokerdb.upsertFirm(a.brokerage) : null;
   // Insert/refresh the broker as a RESIDENTIAL agent. upsertBroker keys on (name, firm_id); we set
   // agent_type/license/website directly so commercial rows are untouched.
@@ -87,12 +97,15 @@ async function persistAgent(condo, a, sourceUrl) {
   const brokerId = r.rows[0].id;
 
   await brokerdb.pool.query(
-    `INSERT INTO broker_condo(broker_id, condo_id, role) VALUES($1,$2,'listing')
-     ON CONFLICT DO NOTHING`, [brokerId, condo.id]);
+    `INSERT INTO broker_condo(broker_id, condo_id, role) VALUES($1,$2,$3)
+     ON CONFLICT DO NOTHING`, [brokerId, condo.id, role]);
 
-  await brokerdb.pool.query(
-    `UPDATE condo SET broker_name=$2, firm_name=$3, firm_id=$4 WHERE id=$1`,
-    [condo.id, a.name, a.brokerage, firmId]);
+  // Only the PRIMARY listing agent writes the flat condo.broker_name (a co-agent must not clobber it).
+  if (role === 'listing') {
+    await brokerdb.pool.query(
+      `UPDATE condo SET broker_name=$2, firm_name=$3, firm_id=$4 WHERE id=$1`,
+      [condo.id, a.name, a.brokerage, firmId]);
+  }
 
   // Per-field provenance (CCPA audit trail), tier 'redfin-detail'.
   const prov = [];
@@ -148,19 +161,25 @@ async function persistAgent(condo, a, sourceUrl) {
             return { status: r.status, text: await r.text() };
           }, u);
           if (res.status !== 200 || !res.text) { summary.errors++; results.push({ condo: t.id, pid: t.pid, status: res.status, error: 'non-200' }); continue; }
-          const a = extractAgent(res.text);
-          if (a.suppressed || a.noPayload || a.parseErr) {
+          const ex = extractAgents(res.text);
+          if (ex.suppressed || ex.noPayload || ex.parseErr || !ex.agents.length) {
             summary.suppressed++;
             results.push({ condo: t.id, pid: t.pid, agent: null, label: 'no public agent (Redfin-listed / suppressed)' });
             continue;
           }
-          await persistAgent(t, a, t.source);
+          // Persist EVERY agent (record all licenses): primary = 'listing', the rest = 'co-listing'.
+          for (let i = 0; i < ex.agents.length; i++) {
+            await persistAgent(t, ex.agents[i], t.source, i === 0 ? 'listing' : 'co-listing');
+          }
+          const a = ex.agents[0];
+          const coAgents = ex.agents.length - 1;
           summary.captured++;
+          summary.coAgents = (summary.coAgents || 0) + coAgents;
           if (a.phone)   summary.withPhone++;
           if (a.email)   summary.withEmail++;
-          if (a.license) summary.withLicense++;
-          results.push({ condo: t.id, pid: t.pid, agent: a.name, brokerage: a.brokerage, phone: !!a.phone, email: !!a.email });
-          process.stderr.write(`  ${t.address}, ${t.city}: ${a.name} / ${a.brokerage || '?'}${a.phone ? ' ☎' : ''}${a.email ? ' ✉' : ''}\n`);
+          if (ex.agents.some(x => x.license)) summary.withLicense++;
+          results.push({ condo: t.id, pid: t.pid, agent: a.name, brokerage: a.brokerage, phone: !!a.phone, email: !!a.email, agents: ex.agents.length, licenses: ex.agents.filter(x => x.license).map(x => x.license) });
+          process.stderr.write(`  ${t.address}, ${t.city}: ${a.name} / ${a.brokerage || '?'}${a.phone ? ' ☎' : ''}${a.email ? ' ✉' : ''}${coAgents ? ` +${coAgents} co-agent` : ''}\n`);
           await page.waitForTimeout(500);
         } catch (e) { summary.errors++; results.push({ condo: t.id, pid: t.pid, error: String(e.message).slice(0, 80) }); }
       }

← 5bb6bfb fetch-sfr-agents: record ALL license numbers per SFR listing  ·  back to Commercialrealestate  ·  auto-save: 2026-08-03T11:53:46 (3 files) — data/condo-broker 92e42fc →