[object Object]

← back to Commercialrealestate

auto-save: 2026-06-27T05:37:39 (1 files) — scripts/sources/firms.js

e133dd48631d164e716cef62b23fb9b0885267fb · 2026-06-27 05:37:44 -0700 · Steve Abrams

Files touched

Diff

commit e133dd48631d164e716cef62b23fb9b0885267fb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jun 27 05:37:44 2026 -0700

    auto-save: 2026-06-27T05:37:39 (1 files) — scripts/sources/firms.js
---
 scripts/sources/firms.js | 44 ++++++++++++++++++++++++++++----------------
 1 file changed, 28 insertions(+), 16 deletions(-)

diff --git a/scripts/sources/firms.js b/scripts/sources/firms.js
index 02fc8cd..4430987 100644
--- a/scripts/sources/firms.js
+++ b/scripts/sources/firms.js
@@ -52,14 +52,22 @@ const today = () => new Date().toISOString().slice(0, 10);
 // Given any parsed JSON body from a firm's API, dig out an array of "asset-like" objects and map
 // each to the common intermediate shape { id, price, city, address, cap, types, desc, urlSlug }.
 // Heuristic but conservative: it only keeps objects that have BOTH a plausible price and a city.
+const ARRAY_KEYS = ['data', 'results', 'assets', 'items', 'listings', 'properties', 'hits', 'records', 'docs'];
 const pickArray = (j) => {
   if (Array.isArray(j)) return j;
-  for (const k of ['data', 'results', 'assets', 'items', 'listings', 'properties', 'hits', 'records', 'docs']) {
-    if (Array.isArray(j?.[k])) return j[k];
-    if (Array.isArray(j?.[k]?.items)) return j[k].items;     // {data:{items:[...]}}
+  if (!j || typeof j !== 'object') return [];
+  // Case-insensitive match against known container keys (M&M's .NET API uses "Results", etc.).
+  const lower = {}; for (const k of Object.keys(j)) lower[k.toLowerCase()] = k;
+  for (const want of ARRAY_KEYS) {
+    const k = lower[want]; if (k == null) continue;
+    const v = j[k];
+    if (Array.isArray(v)) return v;
+    if (v && typeof v === 'object') {                         // {Results:{Items:[...]}} / {data:{items:[...]}}
+      const inner = pickArray(v); if (inner.length) return inner;
+    }
   }
-  // Last resort: first array-of-objects value on the top level.
-  for (const v of Object.values(j || {})) {
+  // Last resort: first non-empty array-of-objects value on the top level.
+  for (const v of Object.values(j)) {
     if (Array.isArray(v) && v.length && typeof v[0] === 'object') return v;
   }
   return [];
@@ -67,14 +75,18 @@ const pickArray = (j) => {
 
 const num = (v) => { const n = +String(v == null ? '' : v).replace(/[^0-9.]/g, ''); return Number.isFinite(n) && n > 0 ? n : null; };
 
-const firstStr = (obj, keys) => { for (const k of keys) { const v = obj?.[k]; if (typeof v === 'string' && v.trim()) return v.trim(); } return ''; };
+// Case-insensitive field access — many firm APIs (M&M's .NET, etc.) capitalize keys (ListPrice, City).
+const ciIndex = (o) => { const m = {}; if (o && typeof o === 'object') for (const k of Object.keys(o)) m[k.toLowerCase()] = o[k]; return m; };
+const ciStr = (o, names) => { const m = ciIndex(o); for (const n of names) { const v = m[n.toLowerCase()]; if (typeof v === 'string' && v.trim()) return v.trim(); } return ''; };
+const ciVal = (o, names) => { const m = ciIndex(o); for (const n of names) { const v = m[n.toLowerCase()]; if (v != null) return v; } return null; };
 
 // Pull a {city,address} out of an asset that may nest them under locations[0] / address{} / fields.
 const locOf = (a) => {
-  const loc = (Array.isArray(a.locations) && a.locations[0]) || a.location || a.address || a.geo || a;
-  const city = firstStr(loc, ['city', 'cityName', 'town']) || firstStr(a, ['city', 'cityName']);
-  const address = firstStr(loc, ['address', 'addressLine1', 'street', 'line1', 'fullAddress', 'name'])
-    || firstStr(a, ['address', 'name', 'title', 'displayAddress']);
+  const m = ciIndex(a);
+  const loc = (Array.isArray(m.locations) && m.locations[0]) || m.location || (m.address && typeof m.address === 'object' ? m.address : null) || m.geo || a;
+  const city = ciStr(loc, ['city', 'cityName', 'town']) || ciStr(a, ['city', 'cityName', 'town']);
+  const address = ciStr(loc, ['address', 'addressLine1', 'street', 'line1', 'fullAddress', 'streetAddress', 'name'])
+    || ciStr(a, ['address', 'streetAddress', 'addressLine1', 'name', 'title', 'displayAddress']);
   return { city, address };
 };
 
@@ -82,17 +94,17 @@ function genericExtract(json) {
   const out = [];
   for (const a of pickArray(json)) {
     if (!a || typeof a !== 'object') continue;
-    const price = num(a.askingPrice ?? a.price ?? a.listPrice ?? a.salePrice ?? a.priceValue ?? a.amount);
+    const price = num(ciVal(a, ['askingPrice', 'price', 'listPrice', 'salePrice', 'priceValue', 'amount']));
     const { city, address } = locOf(a);
     if (!price || !city || !address) continue;
-    const id = String(a.id ?? a.assetId ?? a.listingId ?? a.propertyId ?? a.slug ?? address).slice(0, 64);
-    const capRaw = a.capRate ?? a.cap ?? a.capRateValue ?? null;
+    const id = String(ciVal(a, ['id', 'assetId', 'listingId', 'propertyId', 'slug']) ?? address).slice(0, 64);
+    const capRaw = ciVal(a, ['capRate', 'cap', 'capRateValue']);
     out.push({
       id, price, city, address,
       cap: typeof capRaw === 'number' ? capRaw : num(capRaw),
-      urlSlug: firstStr(a, ['urlSlug', 'slug', 'url', 'detailUrl', 'permalink']),
-      desc: firstStr(a, ['description', 'summary', 'remarks', 'subtitle']),
-      types: a.types || a.propertyTypes || (a.propertyType ? [a.propertyType] : [])
+      urlSlug: ciStr(a, ['urlSlug', 'slug', 'url', 'detailUrl', 'permalink']),
+      desc: ciStr(a, ['description', 'summary', 'remarks', 'subtitle']),
+      types: ciVal(a, ['types', 'propertyTypes']) || (ciVal(a, ['propertyType']) ? [ciVal(a, ['propertyType'])] : [])
     });
   }
   return out;

← 6594d54 CRE: qwen narratives backfilled for all 1028 properties (the  ·  back to Commercialrealestate  ·  CRE: case-insensitive generic sniffer (handles M&M/.NET capi c70d4a6 →