[object Object]

← back to Homesonspec

Listings contact: capture per-community sales-office phone from Lennar feed (verified) + Community.salesPhone column + prominent Call button on home detail; Lennar exposes no public email so listings show phone + lead form

0d7bc7785309cf3bab6036a49af9433dc71cbff4 · 2026-07-22 23:24:02 -0700 · Steve

Files touched

Diff

commit 0d7bc7785309cf3bab6036a49af9433dc71cbff4
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 22 23:24:02 2026 -0700

    Listings contact: capture per-community sales-office phone from Lennar feed (verified) + Community.salesPhone column + prominent Call button on home detail; Lennar exposes no public email so listings show phone + lead form
---
 apps/web/src/app/homes/[id]/page.tsx                                | 6 ++++++
 collectors/lennar/src/index.ts                                      | 4 ++++
 .../migrations/20260723040000_community_sales_phone/migration.sql   | 1 +
 packages/database/prisma/schema.prisma                              | 1 +
 packages/publisher/src/index.ts                                     | 1 +
 packages/schemas/src/index.ts                                       | 1 +
 6 files changed, 14 insertions(+)

diff --git a/apps/web/src/app/homes/[id]/page.tsx b/apps/web/src/app/homes/[id]/page.tsx
index c215a94..23a7eef 100644
--- a/apps/web/src/app/homes/[id]/page.tsx
+++ b/apps/web/src/app/homes/[id]/page.tsx
@@ -244,6 +244,12 @@ export default async function HomeDetailPage({ params }: { params: Promise<{ id:
             <p className="mt-1 text-sm text-neutral-600">
               This builder is actively selling this home — send a note and they&rsquo;ll follow up. No obligation.
             </p>
+            {home.community.salesPhone ? (
+              <a href={`tel:${home.community.salesPhone.replace(/[^0-9+]/g, "")}`}
+                className="btn-accent mt-3 w-full justify-center" data-testid="sales-phone">
+                📞 Call {home.community.salesPhone}
+              </a>
+            ) : null}
             <div className="mt-3">
               <LeadForm homeId={home.id} communityId={home.communityId} builderName={home.builder.name} />
             </div>
diff --git a/collectors/lennar/src/index.ts b/collectors/lennar/src/index.ts
index 4e52801..1223530 100644
--- a/collectors/lennar/src/index.ts
+++ b/collectors/lennar/src/index.ts
@@ -147,6 +147,9 @@ export const lennarAdapter: SourceAdapter = {
     const errors: { url: string; reason: string }[] = [];
     try {
       const html = page.body.toString("utf8");
+      // Community sales-office phone — the "broker" contact shown on listings.
+      const phoneRaw = html.match(/"phoneNumber"\s*:\s*"([^"]+)"/)?.[1] ?? null;
+      const salesPhone = phoneRaw && /\d{3}[-.\s]?\d{3}[-.\s]?\d{4}/.test(phoneRaw) ? phoneRaw : null;
       const match = html.match(/<script id="__NEXT_DATA__" type="application\/json"[^>]*>([\s\S]*?)<\/script>/);
       if (!match) return { records: [], errors: [{ url: page.url, reason: "no __NEXT_DATA__" }] };
       const nextData = JSON.parse(match[1]!) as {
@@ -188,6 +191,7 @@ export const lennarAdapter: SourceAdapter = {
           hoaFeeMonthly: fv(hoaMonthly, rollup?.monthlyHoaFee === undefined ? null : String(rollup?.monthlyHoaFee), page.url),
           schoolDistrict: fv<string>(null, null, page.url),
           ageRestricted: fv<boolean>(null, null, page.url),
+          salesPhone: fv(salesPhone, salesPhone, page.url, salesPhone ? "community sales phone" : null),
         },
       });
 
diff --git a/packages/database/prisma/migrations/20260723040000_community_sales_phone/migration.sql b/packages/database/prisma/migrations/20260723040000_community_sales_phone/migration.sql
new file mode 100644
index 0000000..582369a
--- /dev/null
+++ b/packages/database/prisma/migrations/20260723040000_community_sales_phone/migration.sql
@@ -0,0 +1 @@
+ALTER TABLE "Community" ADD COLUMN "salesPhone" TEXT;
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index 99759e0..6a3336c 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -77,6 +77,7 @@ model Community {
   hoaRequired    Boolean?
   schoolDistrict String?  // objective district name only — no subjective ratings
   ageRestricted  Boolean  @default(false)
+  salesPhone     String?  // community sales-office phone (the "broker" contact on listings)
   amenities      Json?
   searchText     String?  // app-maintained: "name city state zip metro county" for trgm
   priceMin       Decimal? @db.Decimal(12, 2) // cached rollup from active homes
diff --git a/packages/publisher/src/index.ts b/packages/publisher/src/index.ts
index 2502f84..fbbb9c5 100644
--- a/packages/publisher/src/index.ts
+++ b/packages/publisher/src/index.ts
@@ -96,6 +96,7 @@ export async function publishStagedRecord(stagedRecordId: string) {
       hoaRequired: value<number>("hoaFeeMonthly") !== null ? true : null,
       schoolDistrict: value<string>("schoolDistrict"),
       ageRestricted: value<boolean>("ageRestricted") ?? false,
+      salesPhone: value<string>("salesPhone") ?? undefined,
       searchText: [name, city, state, zip, value<string>("metro"), value<string>("county")]
         .filter(Boolean)
         .join(" "),
diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts
index 6812f6e..baa8eb0 100644
--- a/packages/schemas/src/index.ts
+++ b/packages/schemas/src/index.ts
@@ -97,6 +97,7 @@ export const communityPayloadSchema = z.object({
   hoaFeeMonthly: fieldValueSchema(z.number()),
   schoolDistrict: fieldValueSchema(z.string()),
   ageRestricted: fieldValueSchema(z.boolean()),
+  salesPhone: fieldValueSchema(z.string()).optional(),
 });
 export type CommunityPayload = z.infer<typeof communityPayloadSchema>;
 

← c642140 Lennar adapter: LENNAR_STATE_FILTER for targeted per-state c  ·  back to Homesonspec  ·  Add D.R. Horton adapter (2nd builder) — Sitecore var-model b d27cab5 →