deep research addressement
This commit is contained in:
@@ -28,6 +28,18 @@ import {
|
||||
type SubWithEffectiveTier,
|
||||
} from "~/server/lib/tier";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tier limits for property monitoring
|
||||
// Guard (basic) – 1 property
|
||||
// Fortress (plus) – 3 properties
|
||||
// Family (premium) – 5 properties
|
||||
// ---------------------------------------------------------------------------
|
||||
const MAX_PROPERTIES: Record<string, number> = {
|
||||
basic: 1,
|
||||
plus: 3,
|
||||
premium: 5,
|
||||
};
|
||||
|
||||
async function getSubscription(userId: string): Promise<SubWithEffectiveTier> {
|
||||
const [sub] = await db
|
||||
.select()
|
||||
@@ -81,6 +93,25 @@ export async function addProperty(
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "HomeTitle requires a Plus subscription or active feature trial" });
|
||||
}
|
||||
|
||||
// Enforce tier property count limits
|
||||
const maxAllowed = MAX_PROPERTIES[sub.effectiveTier] ?? 1;
|
||||
const [{ count: activeCount }] = await db
|
||||
.select({ count: count() })
|
||||
.from(propertyWatchlistItems)
|
||||
.where(
|
||||
and(
|
||||
eq(propertyWatchlistItems.subscriptionId, sub.id),
|
||||
eq(propertyWatchlistItems.isActive, true),
|
||||
),
|
||||
);
|
||||
|
||||
if (activeCount >= maxAllowed) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: `Property limit reached: ${maxAllowed} active properties for ${sub.effectiveTier} tier`,
|
||||
});
|
||||
}
|
||||
|
||||
const parsed = parseAddress(address);
|
||||
const coords = await geocodeAddress(address);
|
||||
|
||||
@@ -100,17 +131,60 @@ export async function addProperty(
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Try to fetch real property data from Attom for the initial snapshot
|
||||
let initialOwnerName = ownerName ?? "Unknown Owner";
|
||||
let initialDeedDate: string | null = null;
|
||||
let initialTaxAmount: number | null = null;
|
||||
let initialLienCount = 0;
|
||||
let initialPropertyType = "residential";
|
||||
let initialAddress: Record<string, string> = { full: address, ...parsed };
|
||||
let initialTaxId: string | null = null;
|
||||
|
||||
try {
|
||||
const apiKey = process.env.ATTOM_API_KEY;
|
||||
if (apiKey && !ownerName) {
|
||||
const record = await fetchCountyRecords(
|
||||
parcelId ?? null,
|
||||
parsed.state,
|
||||
parsed.state,
|
||||
inserted.id,
|
||||
sub.id,
|
||||
userId,
|
||||
);
|
||||
if (record) {
|
||||
initialOwnerName = record.ownerName;
|
||||
initialDeedDate = record.deedDate;
|
||||
initialTaxAmount = record.taxAmount ?? null;
|
||||
initialLienCount = record.lienCount;
|
||||
initialPropertyType = record.propertyType ?? "residential";
|
||||
initialAddress = record.address;
|
||||
initialTaxId = record.taxId;
|
||||
|
||||
// Update parcel ID from Attom lookup
|
||||
if (record.taxId && !parcelId) {
|
||||
db.update(propertyWatchlistItems)
|
||||
.set({ ownerName: record.ownerName, parcelId: record.taxId, updatedAt: new Date() })
|
||||
.where(eq(propertyWatchlistItems.id, inserted.id))
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Initial fetch is best-effort; log but proceed with defaults
|
||||
console.warn("[hometitle] Failed to fetch initial Attom data for property:", err);
|
||||
}
|
||||
|
||||
await db.insert(propertySnapshots).values({
|
||||
propertyWatchlistItemId: inserted.id,
|
||||
subscriptionId: sub.id,
|
||||
capturedAt: new Date(),
|
||||
ownerName: ownerName ?? "Unknown",
|
||||
address: { full: address, ...parsed },
|
||||
deedDate: null,
|
||||
taxId: null,
|
||||
propertyType: "residential",
|
||||
taxAmount: null,
|
||||
lienCount: 0,
|
||||
ownerName: initialOwnerName,
|
||||
address: initialAddress,
|
||||
deedDate: initialDeedDate,
|
||||
taxId: initialTaxId,
|
||||
propertyType: initialPropertyType,
|
||||
taxAmount: initialTaxAmount,
|
||||
lienCount: initialLienCount,
|
||||
});
|
||||
|
||||
return inserted;
|
||||
@@ -286,16 +360,18 @@ async function checkTierLimits(userId: string): Promise<{ allowed: boolean; reas
|
||||
const sub = await getSubscription(userId);
|
||||
const tier = sub.effectiveTier;
|
||||
|
||||
// Premium has no scan limit
|
||||
if (tier === "premium") {
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
const maxScans: Record<string, number> = {
|
||||
basic: 1,
|
||||
plus: 4,
|
||||
plus: 3,
|
||||
};
|
||||
|
||||
const maxScanCount = maxScans[tier] ?? 1;
|
||||
// Basic = monthly window, Plus = weekly window
|
||||
const periodStart =
|
||||
tier === "plus"
|
||||
? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||||
@@ -350,9 +426,19 @@ export async function runScan(userId: string): Promise<{ scanId: string }> {
|
||||
try {
|
||||
const lastSnapshot = await getLastSnapshot(item.id);
|
||||
const county = item.state || "Unknown";
|
||||
const currentRecord = await fetchCountyRecords(item.parcelId, county, item.state ?? "");
|
||||
const currentRecord = await fetchCountyRecords(
|
||||
item.parcelId,
|
||||
county,
|
||||
item.state ?? "",
|
||||
item.id,
|
||||
sub.id,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (!currentRecord) continue;
|
||||
if (!currentRecord) {
|
||||
console.warn(`[hometitle] No Attom data returned for property ${item.id} — monitoring gap may exist`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newData: SnapshotData = {
|
||||
ownerName: currentRecord.ownerName,
|
||||
|
||||
Reference in New Issue
Block a user