← back to Homesonspec
packages/database/prisma/seed.ts
466 lines
import { prisma } from "../src/index.js";
import { canonicalKey, normalizeAddress } from "../../shared/src/canonical.js";
/**
* Synthetic demonstration inventory — 3 fake-but-realistic markets.
* Every row is isDemo: true with verificationLabel DEMONSTRATION.
* Real zip prefixes / coordinates are used so the validators PASS —
* but no builder, community, address, or home here is real.
*
* Meridian's Cedar Bend inventory homes are NOT seeded here — they arrive
* through the fixture pipeline (pnpm pipeline) so their evidence rows are
* pipeline-real. This seed creates the FK targets (builder, community,
* source registry) the pipeline needs.
*/
// Deterministic LCG so re-seeding is stable.
let seedState = 42;
function rand(): number {
seedState = (seedState * 1664525 + 1013904223) % 2 ** 32;
return seedState / 2 ** 32;
}
function pick<T>(items: T[]): T {
return items[Math.floor(rand() * items.length)]!;
}
function between(min: number, max: number): number {
return min + rand() * (max - min);
}
const DEMO_SOURCE_URL = "https://demo.homesonspec.local/synthetic";
interface MarketSpec {
metro: string;
state: string;
county: string;
schoolDistricts: string[];
cities: { name: string; zip: string; lat: number; lon: number }[];
priceBand: [number, number];
}
const MARKETS: Record<string, MarketSpec> = {
austin: {
metro: "Austin-Round Rock",
state: "TX",
county: "Williamson County",
schoolDistricts: ["Leander ISD", "Round Rock ISD", "Georgetown ISD"],
cities: [
{ name: "Leander", zip: "78641", lat: 30.5788, lon: -97.8531 },
{ name: "Cedar Park", zip: "78613", lat: 30.5052, lon: -97.8203 },
{ name: "Georgetown", zip: "78628", lat: 30.6333, lon: -97.6772 },
{ name: "Round Rock", zip: "78665", lat: 30.5488, lon: -97.6259 },
],
priceBand: [330_000, 640_000],
},
phoenix: {
metro: "Phoenix-Mesa",
state: "AZ",
county: "Maricopa County",
schoolDistricts: ["Gilbert Public Schools", "Queen Creek USD", "Peoria USD"],
cities: [
{ name: "Mesa", zip: "85212", lat: 33.3125, lon: -111.6354 },
{ name: "Queen Creek", zip: "85142", lat: 33.2487, lon: -111.6343 },
{ name: "Buckeye", zip: "85326", lat: 33.3703, lon: -112.5838 },
{ name: "Peoria", zip: "85383", lat: 33.7134, lon: -112.2765 },
],
priceBand: [360_000, 720_000],
},
raleigh: {
metro: "Raleigh-Cary",
state: "NC",
county: "Wake County",
schoolDistricts: ["Wake County Public Schools"],
cities: [
{ name: "Cary", zip: "27513", lat: 35.7915, lon: -78.7811 },
{ name: "Apex", zip: "27502", lat: 35.7327, lon: -78.8503 },
{ name: "Fuquay-Varina", zip: "27526", lat: 35.5843, lon: -78.8 },
{ name: "Wake Forest", zip: "27587", lat: 35.9799, lon: -78.5097 },
],
priceBand: [340_000, 780_000],
},
};
const PLAN_NAMES = [
"The Juniper", "The Silverleaf", "The Bracken", "The Laurel", "The Cypress",
"The Marigold", "The Saguaro", "The Ocotillo", "The Palo Verde", "The Dogwood",
"The Magnolia", "The Longleaf",
];
const STREET_NAMES = [
"Juniper Draw", "Silverleaf Pass", "Brackens Crossing", "Laurel Bend", "Cypress Hollow",
"Marigold Way", "Saguaro Vista", "Ocotillo Trail", "Palo Verde Lane", "Dogwood Court",
"Magnolia Grove", "Longleaf Circle", "Quarry Ridge", "Summit Draw", "Prairie Clover",
];
async function main() {
console.log("Seeding synthetic demonstration inventory…");
// ── Source registry ──────────────────────────────────────────────
const syntheticSource = await prisma.sourceRegistry.upsert({
where: { key: "synthetic-demo" },
update: {},
create: {
key: "synthetic-demo",
name: "Synthetic demonstration data",
collectionMethod: "SYNTHETIC",
mediaRights: "NONE",
notes: "Generated demo inventory. Not real homes, builders, or offers.",
},
});
// ── Builders ─────────────────────────────────────────────────────
const builderSpecs = [
{ slug: "meridian-homes", name: "Meridian Homes", coverage: "TX, AZ, NC" },
{ slug: "bluebonnet-builders", name: "Bluebonnet Builders", coverage: "Central Texas" },
{ slug: "sunridge-communities", name: "SunRidge Communities", coverage: "AZ desert metros" },
{ slug: "harborline-homes", name: "Harborline Homes", coverage: "Southeast" },
];
const builders: Record<string, { id: string }> = {};
for (const spec of builderSpecs) {
builders[spec.slug] = await prisma.builder.upsert({
where: { slug: spec.slug },
update: {},
create: {
slug: spec.slug,
name: spec.name,
legalName: `${spec.name} LLC (fictional)`,
websiteUrl: `https://fixtures.${spec.slug.replace(/-/g, "")}.example`,
coverageArea: spec.coverage,
logoRightsStatus: "none",
isDemo: true,
},
});
}
// Fixture source for the Meridian pipeline (FK target for pnpm pipeline).
await prisma.sourceRegistry.upsert({
where: { key: "meridian-homes-fixtures" },
update: {},
create: {
key: "meridian-homes-fixtures",
name: "Meridian Homes (fixture pages)",
builderId: builders["meridian-homes"]!.id,
baseUrl: "https://fixtures.meridianhomes.example",
collectionMethod: "FIXTURE",
mediaRights: "NONE",
freshIntervalHours: 24,
agingIntervalHours: 72,
notes: "Local fixture HTML — demonstration pipeline source. No live fetching.",
},
});
// ── Divisions ────────────────────────────────────────────────────
const divisionSpecs: [string, string, string, string[]][] = [
["meridian-homes", "Meridian Texas", "Central Texas", ["TX"]],
["meridian-homes", "Meridian Arizona", "Phoenix Valley", ["AZ"]],
["meridian-homes", "Meridian Carolinas", "Research Triangle", ["NC"]],
["bluebonnet-builders", "Bluebonnet Central", "Central Texas", ["TX"]],
["sunridge-communities", "SunRidge Desert", "Phoenix Valley", ["AZ"]],
["harborline-homes", "Harborline Southeast", "Carolinas", ["NC", "TX", "AZ"]],
];
const divisions: Record<string, string> = {};
for (const [builderSlug, name, region, states] of divisionSpecs) {
const existing = await prisma.division.findFirst({
where: { name, builderId: builders[builderSlug]!.id },
});
const division =
existing ??
(await prisma.division.create({
data: { builderId: builders[builderSlug]!.id, name, region, states },
}));
divisions[name] = division.id;
}
// ── Communities (12 = 4 per market) ──────────────────────────────
interface CommunitySpec {
slug: string;
name: string;
builderSlug: string;
division: string;
market: keyof typeof MARKETS;
cityIndex: number;
ageRestricted?: boolean;
hoa?: number | null;
}
const communitySpecs: CommunitySpec[] = [
// Austin
{ slug: "cedar-bend", name: "Cedar Bend", builderSlug: "meridian-homes", division: "Meridian Texas", market: "austin", cityIndex: 0, hoa: 65 },
{ slug: "bluebonnet-ridge", name: "Bluebonnet Ridge", builderSlug: "bluebonnet-builders", division: "Bluebonnet Central", market: "austin", cityIndex: 1, hoa: 45 },
{ slug: "mesquite-flats", name: "Mesquite Flats", builderSlug: "bluebonnet-builders", division: "Bluebonnet Central", market: "austin", cityIndex: 2, hoa: null },
{ slug: "harpers-crossing", name: "Harper's Crossing", builderSlug: "harborline-homes", division: "Harborline Southeast", market: "austin", cityIndex: 3, hoa: 80 },
// Phoenix
{ slug: "sol-mesa-vista", name: "Sol Mesa Vista", builderSlug: "sunridge-communities", division: "SunRidge Desert", market: "phoenix", cityIndex: 0, hoa: 95 },
{ slug: "agave-trails", name: "Agave Trails", builderSlug: "sunridge-communities", division: "SunRidge Desert", market: "phoenix", cityIndex: 1, ageRestricted: true, hoa: 210 },
{ slug: "copper-sky", name: "Copper Sky", builderSlug: "meridian-homes", division: "Meridian Arizona", market: "phoenix", cityIndex: 2, hoa: 70 },
{ slug: "dune-crest", name: "Dune Crest", builderSlug: "harborline-homes", division: "Harborline Southeast", market: "phoenix", cityIndex: 3, hoa: null },
// Raleigh
{ slug: "longleaf-preserve", name: "Longleaf Preserve", builderSlug: "harborline-homes", division: "Harborline Southeast", market: "raleigh", cityIndex: 0, hoa: 110 },
{ slug: "pinehollow", name: "Pinehollow", builderSlug: "meridian-homes", division: "Meridian Carolinas", market: "raleigh", cityIndex: 1, hoa: 85 },
{ slug: "carolina-fern", name: "Carolina Fern", builderSlug: "bluebonnet-builders", division: "Bluebonnet Central", market: "raleigh", cityIndex: 2, hoa: 60 },
{ slug: "wakefield-commons", name: "Wakefield Commons", builderSlug: "sunridge-communities", division: "SunRidge Desert", market: "raleigh", cityIndex: 3, hoa: 75 },
];
const communities: { id: string; spec: CommunitySpec; city: MarketSpec["cities"][number]; market: MarketSpec }[] = [];
for (const spec of communitySpecs) {
const market = MARKETS[spec.market]!;
const city = market.cities[spec.cityIndex]!;
const lat = Number((city.lat + between(-0.02, 0.02)).toFixed(6));
const lon = Number((city.lon + between(-0.02, 0.02)).toFixed(6));
const community = await prisma.community.upsert({
where: { slug: spec.slug },
update: {},
create: {
builderId: builders[spec.builderSlug]!.id,
divisionId: divisions[spec.division],
slug: spec.slug,
name: spec.name,
city: city.name,
state: market.state,
zip: city.zip,
county: market.county,
metro: market.metro,
lat,
lon,
status: "ACTIVE",
hoaFeeMonthly: spec.hoa ?? null,
hoaRequired: spec.hoa !== null && spec.hoa !== undefined,
schoolDistrict: pick(market.schoolDistricts),
ageRestricted: spec.ageRestricted ?? false,
amenities: ["Community pool", "Trail network", "Neighborhood park"].slice(0, 1 + Math.floor(rand() * 3)),
searchText: `${spec.name} ${city.name} ${market.state} ${city.zip} ${market.metro} ${market.county}`,
sourceUrl: DEMO_SOURCE_URL,
isDemo: true,
},
});
communities.push({ id: community.id, spec, city, market });
}
// ── Floor plans (3 per community) ────────────────────────────────
const planIdsByCommunity: Record<string, { id: string; name: string; beds: number; baths: number; sqft: number; stories: number }[]> = {};
let planCursor = 0;
for (const { id: communityId, spec } of communities) {
planIdsByCommunity[communityId] = [];
for (let i = 0; i < 3; i++) {
const name = PLAN_NAMES[(planCursor + i) % PLAN_NAMES.length]!;
const beds = 2 + ((planCursor + i) % 4); // 2–5
const baths = beds - 1 + (rand() > 0.5 ? 0.5 : 0);
const sqft = Math.round(between(1400, 3800) / 10) * 10;
const stories = sqft > 2600 ? 2 : rand() > 0.6 ? 2 : 1;
const existing = await prisma.floorPlan.findFirst({ where: { communityId, name } });
const plan =
existing ??
(await prisma.floorPlan.create({
data: {
communityId,
builderId: builders[spec.builderSlug]!.id,
name,
builderPlanId: `${spec.slug.toUpperCase().slice(0, 3)}-${100 + i}`,
beds,
bathsFull: Math.floor(baths),
bathsHalf: baths % 1 ? 1 : 0,
sqft,
stories,
garageSpaces: beds >= 4 ? 3 : 2,
basePrice: Math.round(between(...MARKETS[spec.market]!.priceBand) / 1000) * 1000,
homeType: rand() > 0.85 ? "TOWNHOME" : "SINGLE_FAMILY",
planMediaRights: "none",
sourceUrl: DEMO_SOURCE_URL,
isDemo: true,
},
}));
planIdsByCommunity[communityId]!.push({ id: plan.id, name, beds, baths, sqft, stories });
}
planCursor += 3;
}
// ── Inventory homes (10 per community, EXCEPT Cedar Bend = pipeline) ──
const statuses = ["MOVE_IN_READY", "UNDER_CONSTRUCTION", "UNDER_CONSTRUCTION", "PLANNED"] as const;
let homeCount = 0;
for (const { id: communityId, spec, city, market } of communities) {
if (spec.slug === "cedar-bend") continue; // arrives via the fixture pipeline
for (let i = 0; i < 10; i++) {
const plan = pick(planIdsByCommunity[communityId]!);
const streetNumber = 100 + i * 7 + Math.floor(rand() * 5);
const street = `${streetNumber} ${pick(STREET_NAMES)}`;
const lotNumber = String(streetNumber);
const constructionStatus = pick([...statuses]);
// ~1 in 9 homes has no published price — proves null-not-guessed rendering.
const price = i % 9 === 8 ? null : Math.round(between(...market.priceBand) / 990) * 990;
const lat = Number((city.lat + between(-0.015, 0.015)).toFixed(6));
const lon = Number((city.lon + between(-0.015, 0.015)).toFixed(6));
const inventoryId = `${spec.slug.toUpperCase().slice(0, 3)}-${1000 + i}`;
const key = canonicalKey({
builderSlug: spec.builderSlug,
communityName: spec.name,
address: street,
lotNumber,
builderInventoryId: inventoryId,
lat,
lon,
planName: plan.name,
});
const estCompletion =
constructionStatus === "MOVE_IN_READY"
? null
: new Date(Date.UTC(2026, 8 + Math.floor(rand() * 6), 1));
const home = await prisma.inventoryHome.upsert({
where: { canonicalKey: key },
update: {},
create: {
canonicalKey: key,
communityId,
builderId: builders[spec.builderSlug]!.id,
floorPlanId: plan.id,
builderInventoryId: inventoryId,
street,
city: city.name,
state: market.state,
zip: city.zip,
normalizedAddress: normalizeAddress(street),
lotNumber,
lat,
lon,
price,
beds: plan.beds,
bathsTotal: plan.baths,
sqft: plan.sqft + Math.floor(between(-80, 120)),
stories: plan.stories,
garageSpaces: plan.beds >= 4 ? 3 : 2,
homeType: "SINGLE_FAMILY",
constructionStatus,
estCompletionDate: estCompletion,
availabilityStatus: constructionStatus === "MOVE_IN_READY" ? "Available now" : "Available at completion",
status: "PUBLISHED",
freshness: "FRESH",
verificationLabel: "DEMONSTRATION",
lastVerifiedAt: new Date(),
sourceId: syntheticSource.id,
sourceUrl: DEMO_SOURCE_URL,
isDemo: true,
},
});
homeCount++;
// Synthetic evidence rows so every published fact remains traceable.
const evidenceFields: [string, string | null][] = [
["price", price === null ? null : `$${price.toLocaleString("en-US")}`],
["beds", `${plan.beds} Beds`],
["sqft", `${plan.sqft.toLocaleString("en-US")} sq ft`],
];
await prisma.sourceEvidence.createMany({
data: evidenceFields.map(([field, raw]) => ({
entityType: "INVENTORY_HOME" as const,
entityId: home.id,
field,
sourceUrl: DEMO_SOURCE_URL,
retrievedAt: new Date(),
extractorVersion: "seed-1",
rawValue: raw,
normalizedValue: raw === null ? null : String(field === "price" ? price : field === "beds" ? plan.beds : plan.sqft),
evidenceText: raw,
confidence: raw === null ? 0 : 1,
})),
});
}
}
// ── Community price rollups ──────────────────────────────────────
for (const { id: communityId } of communities) {
const agg = await prisma.inventoryHome.aggregate({
where: { communityId, status: "PUBLISHED", price: { not: null } },
_min: { price: true },
_max: { price: true },
});
await prisma.community.update({
where: { id: communityId },
data: { priceMin: agg._min.price, priceMax: agg._max.price },
});
}
// ── Incentives (18), sales offices (12), lots (~30) ─────────────
const incentiveTitles = [
["$10,000 Closing Cost Credit", 10_000],
["Rate Buydown Available", null],
["Appliance Package Included", 4_500],
["$5,000 Toward Options", 5_000],
["Reduced Earnest Money", null],
["Landscaping Package", 3_000],
] as const;
let incentiveCount = 0;
for (const [index, { id: communityId, spec }] of communities.entries()) {
const howMany = index % 2 === 0 ? 2 : 1; // 12×1.5 = 18
for (let i = 0; i < howMany; i++) {
const [title, valueUsd] = incentiveTitles[(index + i) % incentiveTitles.length]!;
const hasExpiry = i % 2 === 0;
const existing = await prisma.incentive.findFirst({ where: { communityId, title } });
if (existing) continue;
await prisma.incentive.create({
data: {
scope: "COMMUNITY",
builderId: builders[spec.builderSlug]!.id,
communityId,
title,
description: `${title} on select homes at ${spec.name}. Demonstration offer — not a real promotion.`,
valueUsd,
expiresAt: hasExpiry ? new Date(Date.UTC(2026, 8 + (i % 3), 30)) : null,
evergreenLabel: hasExpiry ? null : "expiration not provided",
sourceId: syntheticSource.id,
sourceUrl: DEMO_SOURCE_URL,
isDemo: true,
},
});
incentiveCount++;
}
}
for (const { id: communityId, spec, city, market } of communities) {
const existing = await prisma.salesOffice.findFirst({ where: { communityId } });
if (existing) continue;
await prisma.salesOffice.create({
data: {
communityId,
street: `1 ${spec.name} Welcome Center`,
city: city.name,
state: market.state,
zip: city.zip,
phone: "(555) 010-" + String(1000 + communities.findIndex((c) => c.id === communityId)).slice(1),
hours: { "mon-sat": "10:00-18:00", sun: "12:00-17:00" },
isDemo: true,
},
});
}
let lotCount = 0;
for (const { id: communityId } of communities.slice(0, 6)) {
for (let i = 0; i < 5; i++) {
const lotNumber = `L-${200 + i}`;
await prisma.lot.upsert({
where: { communityId_lotNumber: { communityId, lotNumber } },
update: {},
create: {
communityId,
lotNumber,
sizeSqft: Math.round(between(5500, 12_000) / 100) * 100,
status: pick(["available", "available", "reserved", "sold"]),
price: rand() > 0.4 ? Math.round(between(80_000, 180_000) / 1000) * 1000 : null,
isDemo: true,
},
});
lotCount++;
}
}
console.log(
`Seeded: ${builderSpecs.length} builders, ${divisionSpecs.length} divisions, ${communities.length} communities, ` +
`${communities.length * 3} plans, ${homeCount} direct homes (+ Cedar Bend via pipeline), ${incentiveCount} incentives, ` +
`12 sales offices, ${lotCount} lots. All isDemo=true, label=DEMONSTRATION.`,
);
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());