@land-catalyst/batch-data-sdk 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core/types.d.ts
CHANGED
|
@@ -1257,14 +1257,103 @@ export interface Owner {
|
|
|
1257
1257
|
dnc?: OwnerDNC;
|
|
1258
1258
|
}
|
|
1259
1259
|
/**
|
|
1260
|
-
* Property owner profile
|
|
1260
|
+
* Property owner profile - information about the owner's property portfolio
|
|
1261
1261
|
*/
|
|
1262
1262
|
export interface PropertyOwnerProfile {
|
|
1263
|
+
/** Average assessed value of properties owned by the property owner */
|
|
1264
|
+
averageAssessedValue?: number;
|
|
1265
|
+
/** Average purchase price of properties owned by the property owner */
|
|
1266
|
+
averagePurchasePrice?: number;
|
|
1267
|
+
/** Average year built of properties owned by the property owner */
|
|
1263
1268
|
averageYearBuilt?: number;
|
|
1269
|
+
/** Total number of properties owned by the property owner */
|
|
1264
1270
|
propertiesCount?: number;
|
|
1271
|
+
/** Total equity in all properties owned */
|
|
1272
|
+
propertiesTotalEquity?: number;
|
|
1273
|
+
/** Total estimated value of all properties owned */
|
|
1274
|
+
propertiesTotalEstimatedValue?: number;
|
|
1275
|
+
/** Total balance of all mortgages */
|
|
1265
1276
|
mortgagesTotalBalance?: number;
|
|
1277
|
+
/** Total number of open liens (mortgages) held by the property owner */
|
|
1266
1278
|
mortgagesCount?: number;
|
|
1279
|
+
/** Average mortgage balance across all properties */
|
|
1267
1280
|
mortgagesAverageBalance?: number;
|
|
1281
|
+
/** Total purchase price of all properties owned */
|
|
1282
|
+
totalPurchasePrice?: number;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Permit tag values for allTags array
|
|
1286
|
+
*/
|
|
1287
|
+
export type PermitTag = "roofing" | "remodel" | "electrical" | "plumbing" | "hvac" | "solar" | "addition" | "kitchen" | "bathroom";
|
|
1288
|
+
/**
|
|
1289
|
+
* Permit tags - boolean flags for each permit type
|
|
1290
|
+
*/
|
|
1291
|
+
export interface PermitTags {
|
|
1292
|
+
/** Property has at least one permit that adds new square feet */
|
|
1293
|
+
addition?: boolean;
|
|
1294
|
+
/** Property has at least one permit involving a secondary unit, cottage, in-law suite, or ADU */
|
|
1295
|
+
adu?: boolean;
|
|
1296
|
+
/** Property has at least one permit that involved a bathroom */
|
|
1297
|
+
bathroom?: boolean;
|
|
1298
|
+
/** Property has at least one permit that involved a battery */
|
|
1299
|
+
battery?: boolean;
|
|
1300
|
+
/** Property has at least one permit that involved any demolition */
|
|
1301
|
+
demolition?: boolean;
|
|
1302
|
+
/** Property has at least one permit that involved an electrical meter */
|
|
1303
|
+
electricMeter?: boolean;
|
|
1304
|
+
/** Property has at least one permit that involved electrical work */
|
|
1305
|
+
electrical?: boolean;
|
|
1306
|
+
/** Property has at least one permit that involved an EV charger */
|
|
1307
|
+
evCharger?: boolean;
|
|
1308
|
+
/** Property has at least one permit that involved a fire sprinkler */
|
|
1309
|
+
fireSprinkler?: boolean;
|
|
1310
|
+
/** Property has at least one permit that involved gas */
|
|
1311
|
+
gas?: boolean;
|
|
1312
|
+
/** Property has at least one permit that involved a generator */
|
|
1313
|
+
generator?: boolean;
|
|
1314
|
+
/** Property has at least one permit that involved grading */
|
|
1315
|
+
grading?: boolean;
|
|
1316
|
+
/** Property has at least one permit involving a mini-split, heat pump, or related technology */
|
|
1317
|
+
heatPump?: boolean;
|
|
1318
|
+
/** Property has at least one permit that involved heating, ventilation, or air conditioning */
|
|
1319
|
+
hvac?: boolean;
|
|
1320
|
+
/** Property has inspections that have passed */
|
|
1321
|
+
inspectionPassed?: boolean;
|
|
1322
|
+
/** Property has at least one permit that involved a kitchen */
|
|
1323
|
+
kitchen?: boolean;
|
|
1324
|
+
/** Property has at least one permit that included any new construction */
|
|
1325
|
+
newConstruction?: boolean;
|
|
1326
|
+
/** Property has at least one permit that involved any type of plumbing */
|
|
1327
|
+
plumbing?: boolean;
|
|
1328
|
+
/** Property has at least one permit that involved a pool or hot tub */
|
|
1329
|
+
poolAndHotTub?: boolean;
|
|
1330
|
+
/** Property has at least one permit that involved a remodel */
|
|
1331
|
+
remodel?: boolean;
|
|
1332
|
+
/** Property has at least one permit that involved roofing */
|
|
1333
|
+
roofing?: boolean;
|
|
1334
|
+
/** Property has at least one permit that involved solar */
|
|
1335
|
+
solar?: boolean;
|
|
1336
|
+
/** Property has at least one permit that involved a water heater */
|
|
1337
|
+
waterHeater?: boolean;
|
|
1338
|
+
/** Property has at least one permit that involved windows or doors */
|
|
1339
|
+
windowDoor?: boolean;
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Permit information for a property
|
|
1343
|
+
*/
|
|
1344
|
+
export interface Permit {
|
|
1345
|
+
/** Total number of permits registered to the property */
|
|
1346
|
+
permitCount?: number;
|
|
1347
|
+
/** Date of the most recent permit */
|
|
1348
|
+
latestDate?: string;
|
|
1349
|
+
/** Date of the oldest permit */
|
|
1350
|
+
earliestDate?: string;
|
|
1351
|
+
/** Total job value for all permits */
|
|
1352
|
+
totalJobValue?: number;
|
|
1353
|
+
/** Array of all permit types that have been taken out on the property */
|
|
1354
|
+
allTags?: PermitTag[];
|
|
1355
|
+
/** Permit tags - boolean flags for each permit type */
|
|
1356
|
+
tags?: PermitTags;
|
|
1268
1357
|
}
|
|
1269
1358
|
/**
|
|
1270
1359
|
* Quick lists flags
|
|
@@ -1411,7 +1500,7 @@ export interface Property {
|
|
|
1411
1500
|
mortgageHistory?: MortgageHistoryEntry[];
|
|
1412
1501
|
openLien?: OpenLien;
|
|
1413
1502
|
owner?: Owner;
|
|
1414
|
-
permit?:
|
|
1503
|
+
permit?: Permit;
|
|
1415
1504
|
propertyOwnerProfile?: PropertyOwnerProfile;
|
|
1416
1505
|
quickLists?: QuickLists;
|
|
1417
1506
|
sale?: Sale;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./property-field/display";
|
|
|
16
16
|
export * from "./property-field/search-criteria-filter-context";
|
|
17
17
|
export type { SearchCriteriaFilterContext, SearchCriteriaFilterType, } from "./property-field/search-criteria-filter-context";
|
|
18
18
|
export * from "./property-field/search-criteria-ai-context";
|
|
19
|
+
export * from "./property-field/property-data-ai-context";
|
|
19
20
|
export { type PropertyFieldPathType, type PropertyFieldValueType, type FieldMetadataForPath, } from "./property-field/types";
|
|
20
21
|
export type { SearchCriteriaFieldMapping, SearchCriteriaFieldMetadata, } from "./property-field/utils";
|
|
21
22
|
export type { RequestMiddleware, ResponseMiddleware, ErrorMiddleware, HttpMiddleware, } from "./client/client";
|
package/dist/index.js
CHANGED
|
@@ -31,3 +31,4 @@ __exportStar(require("./property-field/utils"), exports);
|
|
|
31
31
|
__exportStar(require("./property-field/display"), exports);
|
|
32
32
|
__exportStar(require("./property-field/search-criteria-filter-context"), exports);
|
|
33
33
|
__exportStar(require("./property-field/search-criteria-ai-context"), exports);
|
|
34
|
+
__exportStar(require("./property-field/property-data-ai-context"), exports);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property Data AI Context Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds context for AI services to analyze and discuss property data.
|
|
5
|
+
* Provides field descriptions, interpretations, and investment insights.
|
|
6
|
+
*
|
|
7
|
+
* This module focuses on Property RESPONSE fields (data returned from searches),
|
|
8
|
+
* not SearchCriteria filter fields. Use search-criteria-ai-context.ts for search filters.
|
|
9
|
+
*/
|
|
10
|
+
import type { Property } from "../core/types";
|
|
11
|
+
/**
|
|
12
|
+
* Property data domain with description and investment relevance
|
|
13
|
+
*/
|
|
14
|
+
export interface PropertyDataDomain {
|
|
15
|
+
/** Domain name (e.g., "valuation", "permit", "owner") */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Human-readable description */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Why this domain matters for investment analysis */
|
|
20
|
+
investmentRelevance: string;
|
|
21
|
+
/** Key fields to highlight in this domain */
|
|
22
|
+
keyFields?: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* All property data domains with descriptions and investment relevance
|
|
26
|
+
*/
|
|
27
|
+
export declare const PROPERTY_DATA_DOMAINS: PropertyDataDomain[];
|
|
28
|
+
/**
|
|
29
|
+
* Options for building property data AI context
|
|
30
|
+
*/
|
|
31
|
+
export interface PropertyDataAIContextOptions {
|
|
32
|
+
/** Domains to include (defaults to all) */
|
|
33
|
+
domains?: string[];
|
|
34
|
+
/** Whether to include raw field values */
|
|
35
|
+
includeRawValues?: boolean;
|
|
36
|
+
/** Whether to include field metadata descriptions */
|
|
37
|
+
includeFieldDescriptions?: boolean;
|
|
38
|
+
/** Maximum fields per domain to include */
|
|
39
|
+
maxFieldsPerDomain?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extracted property data with AI-friendly formatting
|
|
43
|
+
*/
|
|
44
|
+
export interface PropertyDataContext {
|
|
45
|
+
/** The property address formatted for display */
|
|
46
|
+
address: string;
|
|
47
|
+
/** Key property metrics for quick reference */
|
|
48
|
+
summary: {
|
|
49
|
+
estimatedValue?: number;
|
|
50
|
+
equity?: number;
|
|
51
|
+
equityPercent?: number;
|
|
52
|
+
ltv?: number;
|
|
53
|
+
salePropensity?: number;
|
|
54
|
+
salePropensityCategory?: string;
|
|
55
|
+
yearBuilt?: number;
|
|
56
|
+
sqft?: number;
|
|
57
|
+
bedrooms?: number;
|
|
58
|
+
bathrooms?: number;
|
|
59
|
+
propertyType?: string;
|
|
60
|
+
ownerName?: string;
|
|
61
|
+
ownerType?: string;
|
|
62
|
+
permitCount?: number;
|
|
63
|
+
ownerPortfolioSize?: number;
|
|
64
|
+
};
|
|
65
|
+
/** Motivation signals detected */
|
|
66
|
+
motivationSignals: string[];
|
|
67
|
+
/** Investment considerations */
|
|
68
|
+
considerations: string[];
|
|
69
|
+
/** Domain-specific data */
|
|
70
|
+
domains: Record<string, Record<string, unknown>>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build comprehensive property data context for AI
|
|
74
|
+
* @param property The property object to analyze
|
|
75
|
+
* @param options Configuration options
|
|
76
|
+
* @returns Structured context for AI consumption
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const context = buildPropertyDataContext(property);
|
|
80
|
+
* // Use context.summary for quick metrics
|
|
81
|
+
* // Use context.motivationSignals for seller motivation
|
|
82
|
+
* // Use context.considerations for investment analysis
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildPropertyDataContext(property: Property, options?: PropertyDataAIContextOptions): PropertyDataContext;
|
|
86
|
+
/**
|
|
87
|
+
* Build AI system prompt section for property data context
|
|
88
|
+
* @param context The property data context
|
|
89
|
+
* @returns Formatted string for AI system prompt
|
|
90
|
+
*/
|
|
91
|
+
export declare function formatPropertyContextForAI(context: PropertyDataContext): string;
|
|
92
|
+
/**
|
|
93
|
+
* Get domain descriptions for AI context
|
|
94
|
+
* @param domainNames Optional list of domains to include
|
|
95
|
+
* @returns Formatted domain descriptions
|
|
96
|
+
*/
|
|
97
|
+
export declare function getPropertyDomainDescriptions(domainNames?: string[]): string;
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Property Data AI Context Builder
|
|
4
|
+
*
|
|
5
|
+
* Builds context for AI services to analyze and discuss property data.
|
|
6
|
+
* Provides field descriptions, interpretations, and investment insights.
|
|
7
|
+
*
|
|
8
|
+
* This module focuses on Property RESPONSE fields (data returned from searches),
|
|
9
|
+
* not SearchCriteria filter fields. Use search-criteria-ai-context.ts for search filters.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PROPERTY_DATA_DOMAINS = void 0;
|
|
13
|
+
exports.buildPropertyDataContext = buildPropertyDataContext;
|
|
14
|
+
exports.formatPropertyContextForAI = formatPropertyContextForAI;
|
|
15
|
+
exports.getPropertyDomainDescriptions = getPropertyDomainDescriptions;
|
|
16
|
+
const utils_1 = require("./utils");
|
|
17
|
+
/**
|
|
18
|
+
* All property data domains with descriptions and investment relevance
|
|
19
|
+
*/
|
|
20
|
+
exports.PROPERTY_DATA_DOMAINS = [
|
|
21
|
+
{
|
|
22
|
+
name: "address",
|
|
23
|
+
description: "Property location and address details",
|
|
24
|
+
investmentRelevance: "Location drives value appreciation, rental rates, and exit strategy options. Neighborhood, school districts, and proximity to amenities affect marketability.",
|
|
25
|
+
keyFields: ["street", "city", "state", "zip", "county"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "building",
|
|
29
|
+
description: "Building characteristics, size, and features",
|
|
30
|
+
investmentRelevance: "Building specs determine renovation costs, rental potential, and buyer appeal. Year built affects maintenance costs; square footage and bedroom/bath count drive value.",
|
|
31
|
+
keyFields: [
|
|
32
|
+
"yearBuilt",
|
|
33
|
+
"size",
|
|
34
|
+
"bedroomCount",
|
|
35
|
+
"bathCount",
|
|
36
|
+
"generalPropertyType",
|
|
37
|
+
"buildingCondition",
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "valuation",
|
|
42
|
+
description: "Property value estimates including AVM, equity, and loan-to-value",
|
|
43
|
+
investmentRelevance: "AVM provides baseline value for offer calculations. Equity indicates owner flexibility; high equity owners may accept creative deals. LTV over 80% suggests potential distress.",
|
|
44
|
+
keyFields: [
|
|
45
|
+
"estimatedValue",
|
|
46
|
+
"equityCurrentEstimatedBalance",
|
|
47
|
+
"equityPercent",
|
|
48
|
+
"ltv",
|
|
49
|
+
"confidenceScore",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "permit",
|
|
54
|
+
description: "Building permit history and renovation activity",
|
|
55
|
+
investmentRelevance: "Recent permits indicate property condition and improvements. High permit activity may signal deferred maintenance being addressed or active renovation. Solar/HVAC permits suggest energy efficiency upgrades.",
|
|
56
|
+
keyFields: [
|
|
57
|
+
"permitCount",
|
|
58
|
+
"totalJobValue",
|
|
59
|
+
"latestDate",
|
|
60
|
+
"allTags",
|
|
61
|
+
"tags",
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "propertyOwnerProfile",
|
|
66
|
+
description: "Owner portfolio size and financial position across all owned properties",
|
|
67
|
+
investmentRelevance: "Portfolio owners may be more sophisticated sellers but also more motivated if over-leveraged. High mortgage balance across portfolio suggests potential cash flow pressure. Multiple properties indicate experienced investor who may want quick, clean deals.",
|
|
68
|
+
keyFields: [
|
|
69
|
+
"propertiesCount",
|
|
70
|
+
"propertiesTotalEquity",
|
|
71
|
+
"propertiesTotalEstimatedValue",
|
|
72
|
+
"mortgagesTotalBalance",
|
|
73
|
+
"mortgagesCount",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "owner",
|
|
78
|
+
description: "Property owner information and contact details",
|
|
79
|
+
investmentRelevance: "Owner type (individual vs corporate vs trust) affects negotiation approach. Absentee owners may be more motivated. Length of ownership indicates attachment level.",
|
|
80
|
+
keyFields: ["fullName", "type", "mailingAddress", "phoneNumbers", "emails"],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "intel",
|
|
84
|
+
description: "Predictive analytics including sale propensity",
|
|
85
|
+
investmentRelevance: "Sale propensity score (0-100) predicts likelihood to sell. Scores above 70 indicate high motivation. Combined with other signals (length of residence, life events), helps prioritize outreach.",
|
|
86
|
+
keyFields: [
|
|
87
|
+
"salePropensity",
|
|
88
|
+
"salePropensityCategory",
|
|
89
|
+
"lastSoldDate",
|
|
90
|
+
"lastSoldPrice",
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "openLien",
|
|
95
|
+
description: "Current mortgages and liens on the property",
|
|
96
|
+
investmentRelevance: "Mortgage balance vs value determines equity and seller flexibility. Multiple liens may indicate financial stress. High interest rates on existing mortgages may motivate seller.",
|
|
97
|
+
keyFields: [
|
|
98
|
+
"totalOpenLienCount",
|
|
99
|
+
"totalOpenLienBalance",
|
|
100
|
+
"mortgages",
|
|
101
|
+
"firstLoanAmount",
|
|
102
|
+
"firstLoanInterestRate",
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "assessment",
|
|
107
|
+
description: "Tax assessment values and market value estimates",
|
|
108
|
+
investmentRelevance: "Assessed value vs market value gap indicates potential appreciation not yet taxed. Assessment increases may pressure cash-strapped owners. Useful for estimating property taxes.",
|
|
109
|
+
keyFields: [
|
|
110
|
+
"totalAssessedValue",
|
|
111
|
+
"totalMarketValue",
|
|
112
|
+
"assessmentYear",
|
|
113
|
+
"landValue",
|
|
114
|
+
"improvementValue",
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "quickLists",
|
|
119
|
+
description: "Pre-computed property flags and characteristics",
|
|
120
|
+
investmentRelevance: "Quick indicators of motivated seller situations. Vacant, pre-foreclosure, tax delinquent, and inherited properties often have motivated sellers. Free-and-clear properties have maximum flexibility.",
|
|
121
|
+
keyFields: [
|
|
122
|
+
"vacant",
|
|
123
|
+
"preforeclosure",
|
|
124
|
+
"taxDefault",
|
|
125
|
+
"inherited",
|
|
126
|
+
"freeAndClear",
|
|
127
|
+
"highEquity",
|
|
128
|
+
"absenteeOwner",
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "foreclosure",
|
|
133
|
+
description: "Foreclosure status and auction information",
|
|
134
|
+
investmentRelevance: "Active foreclosure indicates highly motivated seller with timeline pressure. Auction dates create urgency. Pre-foreclosure is often best window for negotiation.",
|
|
135
|
+
keyFields: ["status", "auctionDate", "defaultAmount", "filingDate"],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "sale",
|
|
139
|
+
description: "Sale history and transaction records",
|
|
140
|
+
investmentRelevance: "Recent sale price establishes baseline. Rapid appreciation or depreciation indicates market trends. Multiple sales in short period may indicate flipper activity or problem property.",
|
|
141
|
+
keyFields: [
|
|
142
|
+
"lastSaleDate",
|
|
143
|
+
"lastSalePrice",
|
|
144
|
+
"lastSaleType",
|
|
145
|
+
"priorSaleDate",
|
|
146
|
+
"priorSalePrice",
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "demographics",
|
|
151
|
+
description: "Owner demographic information",
|
|
152
|
+
investmentRelevance: "Demographics inform communication approach. Senior owners may be considering downsizing. Income and net worth estimates help gauge financial position and deal size capability.",
|
|
153
|
+
keyFields: ["estimatedAge", "estimatedIncome", "estimatedNetWorth"],
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
/**
|
|
157
|
+
* Extract key motivation signals from property data
|
|
158
|
+
*/
|
|
159
|
+
function extractMotivationSignals(property) {
|
|
160
|
+
const signals = [];
|
|
161
|
+
const ql = property.quickLists;
|
|
162
|
+
const intel = property.intel;
|
|
163
|
+
const valuation = property.valuation;
|
|
164
|
+
const ownerProfile = property
|
|
165
|
+
.propertyOwnerProfile;
|
|
166
|
+
// QuickList signals
|
|
167
|
+
if (ql?.vacant)
|
|
168
|
+
signals.push("Property appears vacant");
|
|
169
|
+
if (ql?.preforeclosure)
|
|
170
|
+
signals.push("Property is in pre-foreclosure");
|
|
171
|
+
if (ql?.taxDefault)
|
|
172
|
+
signals.push("Property taxes are delinquent");
|
|
173
|
+
if (ql?.inherited)
|
|
174
|
+
signals.push("Property was inherited");
|
|
175
|
+
if (ql?.absenteeOwner)
|
|
176
|
+
signals.push("Owner does not live at property");
|
|
177
|
+
if (ql?.outOfStateOwner)
|
|
178
|
+
signals.push("Owner lives out of state");
|
|
179
|
+
if (ql?.seniorOwner)
|
|
180
|
+
signals.push("Owner is a senior (65+)");
|
|
181
|
+
if (ql?.freeAndClear)
|
|
182
|
+
signals.push("Property is free and clear (no mortgage)");
|
|
183
|
+
if (ql?.highEquity)
|
|
184
|
+
signals.push("High equity position");
|
|
185
|
+
if (ql?.lowEquity)
|
|
186
|
+
signals.push("Low equity position");
|
|
187
|
+
// Sale propensity
|
|
188
|
+
const salePropensity = intel?.salePropensity;
|
|
189
|
+
if (salePropensity !== undefined) {
|
|
190
|
+
if (salePropensity >= 80) {
|
|
191
|
+
signals.push(`Very high sale propensity (${salePropensity}%)`);
|
|
192
|
+
}
|
|
193
|
+
else if (salePropensity >= 60) {
|
|
194
|
+
signals.push(`High sale propensity (${salePropensity}%)`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// LTV signals
|
|
198
|
+
const ltv = valuation?.ltv;
|
|
199
|
+
if (ltv !== undefined) {
|
|
200
|
+
if (ltv > 100) {
|
|
201
|
+
signals.push(`Underwater property (LTV: ${ltv.toFixed(0)}%)`);
|
|
202
|
+
}
|
|
203
|
+
else if (ltv > 80) {
|
|
204
|
+
signals.push(`High LTV may indicate financial pressure (${ltv.toFixed(0)}%)`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Portfolio stress signals
|
|
208
|
+
const portfolioMortgages = ownerProfile?.mortgagesTotalBalance;
|
|
209
|
+
const portfolioValue = ownerProfile?.propertiesTotalEstimatedValue;
|
|
210
|
+
if (portfolioMortgages && portfolioValue && portfolioMortgages > 0) {
|
|
211
|
+
const portfolioLtv = (portfolioMortgages / portfolioValue) * 100;
|
|
212
|
+
if (portfolioLtv > 70) {
|
|
213
|
+
signals.push(`Owner portfolio is leveraged (${portfolioLtv.toFixed(0)}% across all properties)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return signals;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Generate investment considerations based on property data
|
|
220
|
+
*/
|
|
221
|
+
function generateConsiderations(property) {
|
|
222
|
+
const considerations = [];
|
|
223
|
+
const valuation = property.valuation;
|
|
224
|
+
const permit = property.permit;
|
|
225
|
+
const building = property.building;
|
|
226
|
+
// Equity-based considerations
|
|
227
|
+
const equity = valuation?.equityCurrentEstimatedBalance;
|
|
228
|
+
const estimatedValue = valuation?.estimatedValue;
|
|
229
|
+
if (equity && estimatedValue) {
|
|
230
|
+
const equityPercent = (equity / estimatedValue) * 100;
|
|
231
|
+
if (equityPercent > 50) {
|
|
232
|
+
considerations.push("High equity position gives seller flexibility on terms");
|
|
233
|
+
}
|
|
234
|
+
else if (equityPercent < 20) {
|
|
235
|
+
considerations.push("Low equity may limit seller's ability to negotiate on price");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Permit-based considerations
|
|
239
|
+
const permitCount = permit?.permitCount;
|
|
240
|
+
const permitTags = permit?.tags;
|
|
241
|
+
if (permitCount && permitCount > 0) {
|
|
242
|
+
considerations.push(`${permitCount} permits on file indicate renovation history`);
|
|
243
|
+
if (permitTags?.solar) {
|
|
244
|
+
considerations.push("Solar installation may reduce operating costs");
|
|
245
|
+
}
|
|
246
|
+
if (permitTags?.remodel || permitTags?.kitchen || permitTags?.bathroom) {
|
|
247
|
+
considerations.push("Recent remodel permits suggest updated interior");
|
|
248
|
+
}
|
|
249
|
+
if (permitTags?.roofing) {
|
|
250
|
+
considerations.push("Roofing permit suggests recent roof work");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Building condition considerations
|
|
254
|
+
const yearBuilt = building?.yearBuilt;
|
|
255
|
+
if (yearBuilt) {
|
|
256
|
+
const age = new Date().getFullYear() - yearBuilt;
|
|
257
|
+
if (age > 50) {
|
|
258
|
+
considerations.push(`Built ${age} years ago - may need significant updates`);
|
|
259
|
+
}
|
|
260
|
+
else if (age > 30) {
|
|
261
|
+
considerations.push(`Built ${age} years ago - likely needs some updates`);
|
|
262
|
+
}
|
|
263
|
+
else if (age < 10) {
|
|
264
|
+
considerations.push("Newer construction - likely lower maintenance costs");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Confidence considerations
|
|
268
|
+
const confidence = valuation?.confidenceScore;
|
|
269
|
+
if (confidence !== undefined) {
|
|
270
|
+
if (confidence < 50) {
|
|
271
|
+
considerations.push(`Low AVM confidence (${confidence}) - value estimate may be unreliable`);
|
|
272
|
+
}
|
|
273
|
+
else if (confidence >= 80) {
|
|
274
|
+
considerations.push(`High AVM confidence (${confidence}) - value estimate is reliable`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return considerations;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Format address from property data
|
|
281
|
+
*/
|
|
282
|
+
function formatAddress(property) {
|
|
283
|
+
const addr = property.address;
|
|
284
|
+
if (!addr)
|
|
285
|
+
return "Unknown Address";
|
|
286
|
+
const street = addr.street ||
|
|
287
|
+
[
|
|
288
|
+
addr.houseNumber,
|
|
289
|
+
addr.streetName,
|
|
290
|
+
addr.streetSuffix,
|
|
291
|
+
]
|
|
292
|
+
.filter(Boolean)
|
|
293
|
+
.join(" ");
|
|
294
|
+
const city = addr.city || "";
|
|
295
|
+
const state = addr.state || "";
|
|
296
|
+
const zip = addr.zip || "";
|
|
297
|
+
const formatted = `${street}, ${city}, ${state} ${zip}`.trim();
|
|
298
|
+
return formatted.replace(/[,\s]+$/, "") || "Unknown Address";
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Extract summary metrics from property data
|
|
302
|
+
*/
|
|
303
|
+
function extractSummary(property) {
|
|
304
|
+
const building = property.building;
|
|
305
|
+
const valuation = property.valuation;
|
|
306
|
+
const intel = property.intel;
|
|
307
|
+
const permit = property.permit;
|
|
308
|
+
const ownerProfile = property
|
|
309
|
+
.propertyOwnerProfile;
|
|
310
|
+
const owner = property.owner;
|
|
311
|
+
return {
|
|
312
|
+
estimatedValue: valuation?.estimatedValue,
|
|
313
|
+
equity: valuation?.equityCurrentEstimatedBalance,
|
|
314
|
+
equityPercent: valuation?.equityPercent,
|
|
315
|
+
ltv: valuation?.ltv,
|
|
316
|
+
salePropensity: intel?.salePropensity,
|
|
317
|
+
salePropensityCategory: intel?.salePropensityCategory,
|
|
318
|
+
yearBuilt: building?.yearBuilt,
|
|
319
|
+
sqft: building?.size ||
|
|
320
|
+
building?.livingSquareFeet,
|
|
321
|
+
bedrooms: building?.bedroomCount,
|
|
322
|
+
bathrooms: building?.bathCount,
|
|
323
|
+
propertyType: building?.generalPropertyType,
|
|
324
|
+
ownerName: owner?.fullName,
|
|
325
|
+
ownerType: owner?.type,
|
|
326
|
+
permitCount: permit?.permitCount,
|
|
327
|
+
ownerPortfolioSize: ownerProfile?.propertiesCount,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Extract domain-specific data from property
|
|
332
|
+
*/
|
|
333
|
+
function extractDomainData(property, domainName, options) {
|
|
334
|
+
const data = {};
|
|
335
|
+
const domain = exports.PROPERTY_DATA_DOMAINS.find((d) => d.name === domainName);
|
|
336
|
+
if (!domain)
|
|
337
|
+
return data;
|
|
338
|
+
// Get fields for this domain
|
|
339
|
+
const fields = (0, utils_1.getPropertyGroupFields)(domainName);
|
|
340
|
+
const keyFields = new Set(domain.keyFields || []);
|
|
341
|
+
// Prioritize key fields, then add others up to limit
|
|
342
|
+
const sortedFields = [...fields].sort((a, b) => {
|
|
343
|
+
const aKey = keyFields.has(a.fieldPath.split(".").pop() || "");
|
|
344
|
+
const bKey = keyFields.has(b.fieldPath.split(".").pop() || "");
|
|
345
|
+
if (aKey && !bKey)
|
|
346
|
+
return -1;
|
|
347
|
+
if (!aKey && bKey)
|
|
348
|
+
return 1;
|
|
349
|
+
return 0;
|
|
350
|
+
});
|
|
351
|
+
const limit = options.maxFieldsPerDomain || 20;
|
|
352
|
+
const fieldsToProcess = sortedFields.slice(0, limit);
|
|
353
|
+
for (const field of fieldsToProcess) {
|
|
354
|
+
const value = (0, utils_1.getPropertyFieldValue)(property, field.fieldPath);
|
|
355
|
+
if (value !== undefined && value !== null) {
|
|
356
|
+
const fieldName = field.fieldPath.split(".").pop() || field.fieldPath;
|
|
357
|
+
data[fieldName] = value;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return data;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Build comprehensive property data context for AI
|
|
364
|
+
* @param property The property object to analyze
|
|
365
|
+
* @param options Configuration options
|
|
366
|
+
* @returns Structured context for AI consumption
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* const context = buildPropertyDataContext(property);
|
|
370
|
+
* // Use context.summary for quick metrics
|
|
371
|
+
* // Use context.motivationSignals for seller motivation
|
|
372
|
+
* // Use context.considerations for investment analysis
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
function buildPropertyDataContext(property, options = {}) {
|
|
376
|
+
const selectedDomains = options.domains || exports.PROPERTY_DATA_DOMAINS.map((d) => d.name);
|
|
377
|
+
const domains = {};
|
|
378
|
+
for (const domainName of selectedDomains) {
|
|
379
|
+
const domainData = extractDomainData(property, domainName, options);
|
|
380
|
+
if (Object.keys(domainData).length > 0) {
|
|
381
|
+
domains[domainName] = domainData;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
address: formatAddress(property),
|
|
386
|
+
summary: extractSummary(property),
|
|
387
|
+
motivationSignals: extractMotivationSignals(property),
|
|
388
|
+
considerations: generateConsiderations(property),
|
|
389
|
+
domains,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Build AI system prompt section for property data context
|
|
394
|
+
* @param context The property data context
|
|
395
|
+
* @returns Formatted string for AI system prompt
|
|
396
|
+
*/
|
|
397
|
+
function formatPropertyContextForAI(context) {
|
|
398
|
+
const sections = [];
|
|
399
|
+
// Address
|
|
400
|
+
sections.push(`**Address:** ${context.address}`);
|
|
401
|
+
// Summary metrics
|
|
402
|
+
const s = context.summary;
|
|
403
|
+
sections.push("", "## Property Summary");
|
|
404
|
+
if (s.estimatedValue)
|
|
405
|
+
sections.push(`- Estimated Value: $${s.estimatedValue.toLocaleString()}`);
|
|
406
|
+
if (s.equity !== undefined)
|
|
407
|
+
sections.push(`- Equity: $${s.equity.toLocaleString()}`);
|
|
408
|
+
if (s.equityPercent !== undefined)
|
|
409
|
+
sections.push(`- Equity Percent: ${s.equityPercent.toFixed(1)}%`);
|
|
410
|
+
if (s.ltv !== undefined)
|
|
411
|
+
sections.push(`- LTV: ${s.ltv.toFixed(1)}%`);
|
|
412
|
+
if (s.salePropensity !== undefined)
|
|
413
|
+
sections.push(`- Sale Propensity: ${s.salePropensity}% (${s.salePropensityCategory || "Unknown"})`);
|
|
414
|
+
if (s.propertyType)
|
|
415
|
+
sections.push(`- Property Type: ${s.propertyType}`);
|
|
416
|
+
if (s.yearBuilt)
|
|
417
|
+
sections.push(`- Year Built: ${s.yearBuilt}`);
|
|
418
|
+
if (s.sqft)
|
|
419
|
+
sections.push(`- Square Feet: ${s.sqft.toLocaleString()}`);
|
|
420
|
+
if (s.bedrooms !== undefined)
|
|
421
|
+
sections.push(`- Bedrooms: ${s.bedrooms}`);
|
|
422
|
+
if (s.bathrooms !== undefined)
|
|
423
|
+
sections.push(`- Bathrooms: ${s.bathrooms}`);
|
|
424
|
+
if (s.ownerName)
|
|
425
|
+
sections.push(`- Owner: ${s.ownerName}`);
|
|
426
|
+
if (s.ownerType)
|
|
427
|
+
sections.push(`- Owner Type: ${s.ownerType}`);
|
|
428
|
+
if (s.permitCount)
|
|
429
|
+
sections.push(`- Permit Count: ${s.permitCount}`);
|
|
430
|
+
if (s.ownerPortfolioSize)
|
|
431
|
+
sections.push(`- Owner Portfolio Size: ${s.ownerPortfolioSize} properties`);
|
|
432
|
+
// Motivation signals
|
|
433
|
+
if (context.motivationSignals.length > 0) {
|
|
434
|
+
sections.push("", "## Motivation Signals");
|
|
435
|
+
for (const signal of context.motivationSignals) {
|
|
436
|
+
sections.push(`- ${signal}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Investment considerations
|
|
440
|
+
if (context.considerations.length > 0) {
|
|
441
|
+
sections.push("", "## Investment Considerations");
|
|
442
|
+
for (const consideration of context.considerations) {
|
|
443
|
+
sections.push(`- ${consideration}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return sections.join("\n");
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get domain descriptions for AI context
|
|
450
|
+
* @param domainNames Optional list of domains to include
|
|
451
|
+
* @returns Formatted domain descriptions
|
|
452
|
+
*/
|
|
453
|
+
function getPropertyDomainDescriptions(domainNames) {
|
|
454
|
+
const selectedDomains = domainNames
|
|
455
|
+
? exports.PROPERTY_DATA_DOMAINS.filter((d) => domainNames.includes(d.name))
|
|
456
|
+
: exports.PROPERTY_DATA_DOMAINS;
|
|
457
|
+
const lines = ["## Property Data Domains", ""];
|
|
458
|
+
for (const domain of selectedDomains) {
|
|
459
|
+
lines.push(`**${domain.name}**: ${domain.description}`);
|
|
460
|
+
lines.push(` Investment Relevance: ${domain.investmentRelevance}`);
|
|
461
|
+
lines.push("");
|
|
462
|
+
}
|
|
463
|
+
return lines.join("\n");
|
|
464
|
+
}
|
package/package.json
CHANGED