← 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
M apps/web/src/app/homes/[id]/page.tsxM collectors/lennar/src/index.tsA packages/database/prisma/migrations/20260723040000_community_sales_phone/migration.sqlM packages/database/prisma/schema.prismaM packages/publisher/src/index.tsM packages/schemas/src/index.ts
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’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 →