mortctl 0.1.0

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.
Files changed (45) hide show
  1. package/README.md +234 -0
  2. package/dist/commands/eligible.d.ts +6 -0
  3. package/dist/commands/eligible.d.ts.map +1 -0
  4. package/dist/commands/eligible.js +115 -0
  5. package/dist/commands/eligible.js.map +1 -0
  6. package/dist/commands/limits.d.ts +6 -0
  7. package/dist/commands/limits.d.ts.map +1 -0
  8. package/dist/commands/limits.js +89 -0
  9. package/dist/commands/limits.js.map +1 -0
  10. package/dist/commands/ltv.d.ts +6 -0
  11. package/dist/commands/ltv.d.ts.map +1 -0
  12. package/dist/commands/ltv.js +96 -0
  13. package/dist/commands/ltv.js.map +1 -0
  14. package/dist/commands/payment.d.ts +6 -0
  15. package/dist/commands/payment.d.ts.map +1 -0
  16. package/dist/commands/payment.js +98 -0
  17. package/dist/commands/payment.js.map +1 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +42 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/lib/calculations.d.ts +52 -0
  23. package/dist/lib/calculations.d.ts.map +1 -0
  24. package/dist/lib/calculations.js +270 -0
  25. package/dist/lib/calculations.js.map +1 -0
  26. package/dist/lib/eligibility.d.ts +33 -0
  27. package/dist/lib/eligibility.d.ts.map +1 -0
  28. package/dist/lib/eligibility.js +296 -0
  29. package/dist/lib/eligibility.js.map +1 -0
  30. package/dist/types.d.ts +179 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +6 -0
  33. package/dist/types.js.map +1 -0
  34. package/jest.config.js +8 -0
  35. package/package.json +46 -0
  36. package/src/commands/eligible.ts +121 -0
  37. package/src/commands/limits.ts +91 -0
  38. package/src/commands/ltv.ts +97 -0
  39. package/src/commands/payment.ts +100 -0
  40. package/src/index.ts +32 -0
  41. package/src/lib/calculations.ts +314 -0
  42. package/src/lib/eligibility.ts +343 -0
  43. package/src/types.ts +216 -0
  44. package/tests/calculations.test.ts +154 -0
  45. package/tsconfig.json +19 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Core mortgage calculations
3
+ */
4
+
5
+ import {
6
+ LtvResult,
7
+ PmiInputs,
8
+ PmiResult,
9
+ LoanScenario,
10
+ PaymentBreakdown,
11
+ LoanLimits,
12
+ } from '../types';
13
+
14
+ /**
15
+ * 2026 Conforming Loan Limits (estimated)
16
+ */
17
+ export const LOAN_LIMITS_2026: LoanLimits = {
18
+ baseline: 766550, // Standard conforming limit
19
+ highCost: 1149825, // High-cost area ceiling (150% of baseline)
20
+ fhaFloor: 498257, // FHA floor
21
+ fhaCeiling: 1149825, // FHA ceiling (same as high-cost)
22
+ };
23
+
24
+ /**
25
+ * High-cost counties (sample - would be comprehensive in production)
26
+ */
27
+ export const HIGH_COST_COUNTIES: Record<string, number> = {
28
+ 'Los Angeles, CA': 1149825,
29
+ 'San Francisco, CA': 1149825,
30
+ 'New York, NY': 1149825,
31
+ 'Kings, NY': 1149825,
32
+ 'Queens, NY': 1149825,
33
+ 'Bronx, NY': 1149825,
34
+ 'San Diego, CA': 1006250,
35
+ 'Orange, CA': 1149825,
36
+ 'Santa Clara, CA': 1149825,
37
+ 'Alameda, CA': 1149825,
38
+ 'Seattle, WA': 977500,
39
+ 'King, WA': 977500,
40
+ 'Honolulu, HI': 1149825,
41
+ 'Washington, DC': 1149825,
42
+ 'Montgomery, MD': 1149825,
43
+ 'Fairfax, VA': 1149825,
44
+ };
45
+
46
+ /**
47
+ * Get conforming loan limit for a county
48
+ */
49
+ export function getConformingLimit(county?: string, units: number = 1): number {
50
+ let baseLimit = LOAN_LIMITS_2026.baseline;
51
+
52
+ if (county && HIGH_COST_COUNTIES[county]) {
53
+ baseLimit = HIGH_COST_COUNTIES[county];
54
+ }
55
+
56
+ // Multi-unit adjustments
57
+ const unitMultipliers: Record<number, number> = {
58
+ 1: 1,
59
+ 2: 1.28,
60
+ 3: 1.55,
61
+ 4: 1.92,
62
+ };
63
+
64
+ return Math.round(baseLimit * (unitMultipliers[units] || 1));
65
+ }
66
+
67
+ /**
68
+ * Calculate monthly P&I payment
69
+ */
70
+ export function calculateMonthlyPayment(
71
+ principal: number,
72
+ annualRate: number,
73
+ termYears: number
74
+ ): number {
75
+ const monthlyRate = annualRate / 100 / 12;
76
+ const numPayments = termYears * 12;
77
+
78
+ if (monthlyRate === 0) {
79
+ return principal / numPayments;
80
+ }
81
+
82
+ const payment = principal *
83
+ (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) /
84
+ (Math.pow(1 + monthlyRate, numPayments) - 1);
85
+
86
+ return Math.round(payment * 100) / 100;
87
+ }
88
+
89
+ /**
90
+ * Calculate LTV/CLTV/HCLTV
91
+ */
92
+ export function calculateLtv(
93
+ propertyValue: number,
94
+ loanAmount: number,
95
+ secondLien: number = 0,
96
+ heloc: number = 0
97
+ ): LtvResult {
98
+ const ltv = (loanAmount / propertyValue) * 100;
99
+ const cltv = ((loanAmount + secondLien) / propertyValue) * 100;
100
+ const hcltv = ((loanAmount + secondLien + heloc) / propertyValue) * 100;
101
+
102
+ const pmiRequired = ltv > 80;
103
+
104
+ return {
105
+ ltv: Math.round(ltv * 100) / 100,
106
+ cltv: Math.round(cltv * 100) / 100,
107
+ hcltv: Math.round(hcltv * 100) / 100,
108
+ pmiRequired,
109
+ pmiRemovalLtv: pmiRequired ? 78 : undefined,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * PMI rate lookup table (approximate)
115
+ * Format: [LTV range][Credit score range] = annual rate %
116
+ */
117
+ const PMI_RATES: Record<string, Record<string, number>> = {
118
+ '95-97': { '760+': 0.55, '740-759': 0.65, '720-739': 0.78, '700-719': 0.95, '680-699': 1.15, '<680': 1.40 },
119
+ '90-95': { '760+': 0.38, '740-759': 0.45, '720-739': 0.55, '700-719': 0.70, '680-699': 0.90, '<680': 1.15 },
120
+ '85-90': { '760+': 0.25, '740-759': 0.30, '720-739': 0.38, '700-719': 0.50, '680-699': 0.65, '<680': 0.85 },
121
+ '80-85': { '760+': 0.15, '740-759': 0.18, '720-739': 0.23, '700-719': 0.30, '680-699': 0.40, '<680': 0.55 },
122
+ };
123
+
124
+ /**
125
+ * Get credit score bracket for PMI lookup
126
+ */
127
+ function getCreditBracket(score: number): string {
128
+ if (score >= 760) return '760+';
129
+ if (score >= 740) return '740-759';
130
+ if (score >= 720) return '720-739';
131
+ if (score >= 700) return '700-719';
132
+ if (score >= 680) return '680-699';
133
+ return '<680';
134
+ }
135
+
136
+ /**
137
+ * Get LTV bracket for PMI lookup
138
+ */
139
+ function getLtvBracket(ltv: number): string {
140
+ if (ltv > 95) return '95-97';
141
+ if (ltv > 90) return '90-95';
142
+ if (ltv > 85) return '85-90';
143
+ if (ltv > 80) return '80-85';
144
+ return '80-85'; // Below 80 - no PMI normally
145
+ }
146
+
147
+ /**
148
+ * Calculate PMI
149
+ */
150
+ export function calculatePmi(inputs: PmiInputs): PmiResult {
151
+ const { ltv, creditScore, loanAmount, termYears = 30 } = inputs;
152
+
153
+ // No PMI below 80% LTV
154
+ if (ltv <= 80) {
155
+ return {
156
+ required: false,
157
+ monthlyAmount: 0,
158
+ annualAmount: 0,
159
+ ratePercent: 0,
160
+ cancellationLtv: 80,
161
+ };
162
+ }
163
+
164
+ const ltvBracket = getLtvBracket(ltv);
165
+ const creditBracket = getCreditBracket(creditScore);
166
+
167
+ const ratePercent = PMI_RATES[ltvBracket]?.[creditBracket] || 0.85;
168
+ const annualAmount = (loanAmount * ratePercent) / 100;
169
+ const monthlyAmount = annualAmount / 12;
170
+
171
+ // Estimate months to reach 78% LTV (automatic cancellation)
172
+ // Simplified - assumes 30-year at average rate
173
+ const avgRate = 6.5; // Approximate
174
+ const monthlyPayment = calculateMonthlyPayment(loanAmount, avgRate, termYears);
175
+
176
+ // Very rough estimate of months to 78% LTV
177
+ const targetLoanAmount = loanAmount * (78 / ltv);
178
+ const reductionNeeded = loanAmount - targetLoanAmount;
179
+
180
+ // Approximate based on early amortization (mostly interest)
181
+ const avgMonthlyPrincipal = monthlyPayment * 0.25; // Rough estimate
182
+ const monthsToCancel = Math.ceil(reductionNeeded / avgMonthlyPrincipal);
183
+
184
+ return {
185
+ required: true,
186
+ monthlyAmount: Math.round(monthlyAmount * 100) / 100,
187
+ annualAmount: Math.round(annualAmount * 100) / 100,
188
+ ratePercent,
189
+ cancellationLtv: 78,
190
+ monthsToCancel,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Calculate full payment breakdown
196
+ */
197
+ export function calculatePaymentBreakdown(scenario: LoanScenario): PaymentBreakdown {
198
+ const principalAndInterest = calculateMonthlyPayment(
199
+ scenario.loanAmount,
200
+ scenario.interestRate,
201
+ scenario.termYears
202
+ );
203
+
204
+ const propertyTaxes = (scenario.propertyTaxes || scenario.propertyValue * 0.0125) / 12;
205
+ const homeownersInsurance = (scenario.homeownersInsurance || scenario.propertyValue * 0.0035) / 12;
206
+ const hoaDues = scenario.hoaDues || 0;
207
+
208
+ // Calculate PMI if needed
209
+ const ltv = (scenario.loanAmount / scenario.propertyValue) * 100;
210
+ const pmiResult = calculatePmi({
211
+ ltv,
212
+ creditScore: scenario.creditScore,
213
+ loanAmount: scenario.loanAmount,
214
+ termYears: scenario.termYears,
215
+ });
216
+
217
+ const totalPayment = principalAndInterest + propertyTaxes + homeownersInsurance +
218
+ pmiResult.monthlyAmount + hoaDues;
219
+
220
+ return {
221
+ principalAndInterest: Math.round(principalAndInterest * 100) / 100,
222
+ propertyTaxes: Math.round(propertyTaxes * 100) / 100,
223
+ homeownersInsurance: Math.round(homeownersInsurance * 100) / 100,
224
+ mortgageInsurance: pmiResult.monthlyAmount,
225
+ hoaDues,
226
+ totalPayment: Math.round(totalPayment * 100) / 100,
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Calculate FHA MIP (Mortgage Insurance Premium)
232
+ */
233
+ export function calculateFhaMip(
234
+ loanAmount: number,
235
+ ltv: number,
236
+ termYears: number
237
+ ): { upfront: number; monthly: number; annualRate: number } {
238
+ // Upfront MIP: 1.75% of loan amount
239
+ const upfront = loanAmount * 0.0175;
240
+
241
+ // Annual MIP rates (as of 2024, may change)
242
+ // For loans > 15 years and LTV > 90%: 0.85%
243
+ // For loans > 15 years and LTV <= 90%: 0.80%
244
+ // For loans <= 15 years: 0.45% to 0.70% depending on LTV
245
+ let annualRate: number;
246
+
247
+ if (termYears > 15) {
248
+ annualRate = ltv > 90 ? 0.85 : 0.80;
249
+ } else {
250
+ annualRate = ltv > 90 ? 0.70 : 0.45;
251
+ }
252
+
253
+ const monthly = (loanAmount * annualRate / 100) / 12;
254
+
255
+ return {
256
+ upfront: Math.round(upfront * 100) / 100,
257
+ monthly: Math.round(monthly * 100) / 100,
258
+ annualRate,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Calculate VA Funding Fee
264
+ */
265
+ export function calculateVaFundingFee(
266
+ loanAmount: number,
267
+ downPaymentPercent: number,
268
+ firstTimeUse: boolean,
269
+ reservist: boolean = false
270
+ ): number {
271
+ // VA Funding Fee rates (2024)
272
+ // First use, no down: 2.15% (2.40% for reserves/NG)
273
+ // First use, 5-10% down: 1.50% (1.75% for reserves/NG)
274
+ // First use, 10%+ down: 1.25% (1.50% for reserves/NG)
275
+ // Subsequent use, no down: 3.30%
276
+ // Subsequent use, 5-10% down: 1.50%
277
+ // Subsequent use, 10%+ down: 1.25%
278
+
279
+ let rate: number;
280
+
281
+ if (firstTimeUse) {
282
+ if (downPaymentPercent >= 10) {
283
+ rate = reservist ? 1.50 : 1.25;
284
+ } else if (downPaymentPercent >= 5) {
285
+ rate = reservist ? 1.75 : 1.50;
286
+ } else {
287
+ rate = reservist ? 2.40 : 2.15;
288
+ }
289
+ } else {
290
+ if (downPaymentPercent >= 10) {
291
+ rate = 1.25;
292
+ } else if (downPaymentPercent >= 5) {
293
+ rate = 1.50;
294
+ } else {
295
+ rate = 3.30;
296
+ }
297
+ }
298
+
299
+ return Math.round((loanAmount * rate / 100) * 100) / 100;
300
+ }
301
+
302
+ /**
303
+ * Calculate USDA Guarantee Fee
304
+ */
305
+ export function calculateUsdaFee(loanAmount: number): { upfront: number; annual: number } {
306
+ // USDA fees (2024)
307
+ // Upfront: 1.0%
308
+ // Annual: 0.35%
309
+
310
+ return {
311
+ upfront: Math.round((loanAmount * 0.01) * 100) / 100,
312
+ annual: Math.round((loanAmount * 0.0035) * 100) / 100,
313
+ };
314
+ }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Loan program eligibility checking
3
+ */
4
+
5
+ import {
6
+ LoanScenario,
7
+ EligibilityResult,
8
+ LoanProgram,
9
+ OccupancyType,
10
+ } from '../types';
11
+ import { getConformingLimit, LOAN_LIMITS_2026 } from './calculations';
12
+
13
+ /**
14
+ * Check conventional loan eligibility
15
+ */
16
+ export function checkConventionalEligibility(scenario: LoanScenario): EligibilityResult {
17
+ const reasons: string[] = [];
18
+ const warnings: string[] = [];
19
+ let eligible = true;
20
+
21
+ const ltv = (scenario.loanAmount / scenario.propertyValue) * 100;
22
+ const conformingLimit = getConformingLimit(
23
+ scenario.county ? `${scenario.county}, ${scenario.state}` : undefined,
24
+ scenario.units || 1
25
+ );
26
+
27
+ // Credit score check
28
+ if (scenario.creditScore < 620) {
29
+ eligible = false;
30
+ reasons.push('Credit score below 620 minimum');
31
+ } else if (scenario.creditScore < 680) {
32
+ warnings.push('Credit score below 680 may result in pricing adjustments');
33
+ }
34
+
35
+ // LTV check
36
+ if (ltv > 97) {
37
+ eligible = false;
38
+ reasons.push('LTV exceeds 97% maximum');
39
+ } else if (ltv > 95) {
40
+ warnings.push('LTV >95% requires additional eligibility criteria');
41
+ }
42
+
43
+ // Loan amount check (conforming vs jumbo)
44
+ if (scenario.loanAmount > conformingLimit) {
45
+ warnings.push(`Loan exceeds conforming limit ($${conformingLimit.toLocaleString()}), jumbo pricing applies`);
46
+ }
47
+
48
+ // Occupancy check for high LTV
49
+ if (ltv > 90 && scenario.occupancy !== 'primary') {
50
+ eligible = false;
51
+ reasons.push('LTV >90% only available for primary residence');
52
+ }
53
+
54
+ // Investment property restrictions
55
+ if (scenario.occupancy === 'investment') {
56
+ if (ltv > 85) {
57
+ eligible = false;
58
+ reasons.push('Investment property maximum LTV is 85%');
59
+ }
60
+ if (scenario.creditScore < 680) {
61
+ eligible = false;
62
+ reasons.push('Investment property requires 680+ credit score');
63
+ }
64
+ }
65
+
66
+ // Property type restrictions
67
+ if (scenario.propertyType === 'manufactured' && ltv > 95) {
68
+ eligible = false;
69
+ reasons.push('Manufactured homes limited to 95% LTV');
70
+ }
71
+
72
+ // Calculate min down payment
73
+ let minDownPct = scenario.occupancy === 'investment' ? 15 : 3;
74
+ if (scenario.occupancy === 'second-home') minDownPct = 10;
75
+ const minDownPayment = scenario.propertyValue * (minDownPct / 100);
76
+
77
+ return {
78
+ program: 'conventional',
79
+ eligible,
80
+ reasons: reasons.length > 0 ? reasons : undefined,
81
+ warnings: warnings.length > 0 ? warnings : undefined,
82
+ maxLoanAmount: conformingLimit,
83
+ minDownPayment,
84
+ rateAdjustment: scenario.creditScore < 740 ? 0.25 : 0,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Check FHA loan eligibility
90
+ */
91
+ export function checkFhaEligibility(scenario: LoanScenario): EligibilityResult {
92
+ const reasons: string[] = [];
93
+ const warnings: string[] = [];
94
+ let eligible = true;
95
+
96
+ const ltv = (scenario.loanAmount / scenario.propertyValue) * 100;
97
+ const fhaLimit = LOAN_LIMITS_2026.fhaCeiling; // Would be county-specific in production
98
+
99
+ // Credit score check
100
+ if (scenario.creditScore < 500) {
101
+ eligible = false;
102
+ reasons.push('Credit score below 500 minimum');
103
+ } else if (scenario.creditScore < 580) {
104
+ if (ltv > 90) {
105
+ eligible = false;
106
+ reasons.push('Credit score 500-579 requires 10% down payment (90% LTV max)');
107
+ }
108
+ warnings.push('Credit score 500-579 requires manual underwriting');
109
+ }
110
+
111
+ // LTV check
112
+ if (scenario.creditScore >= 580 && ltv > 96.5) {
113
+ eligible = false;
114
+ reasons.push('FHA maximum LTV is 96.5%');
115
+ }
116
+
117
+ // Occupancy - FHA is owner-occupied only
118
+ if (scenario.occupancy !== 'primary') {
119
+ eligible = false;
120
+ reasons.push('FHA loans require owner occupancy');
121
+ }
122
+
123
+ // Loan amount check
124
+ if (scenario.loanAmount > fhaLimit) {
125
+ eligible = false;
126
+ reasons.push(`Loan exceeds FHA limit ($${fhaLimit.toLocaleString()})`);
127
+ }
128
+
129
+ // Property type restrictions
130
+ if (scenario.propertyType === 'coop') {
131
+ warnings.push('Co-ops require FHA approval');
132
+ }
133
+
134
+ // Calculate min down payment
135
+ const minDownPct = scenario.creditScore >= 580 ? 3.5 : 10;
136
+ const minDownPayment = scenario.propertyValue * (minDownPct / 100);
137
+
138
+ return {
139
+ program: 'fha',
140
+ eligible,
141
+ reasons: reasons.length > 0 ? reasons : undefined,
142
+ warnings: warnings.length > 0 ? warnings : undefined,
143
+ maxLoanAmount: fhaLimit,
144
+ minDownPayment,
145
+ rateAdjustment: 0, // FHA rates are generally competitive
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Check VA loan eligibility
151
+ */
152
+ export function checkVaEligibility(scenario: LoanScenario): EligibilityResult {
153
+ const reasons: string[] = [];
154
+ const warnings: string[] = [];
155
+ let eligible = true;
156
+
157
+ const ltv = (scenario.loanAmount / scenario.propertyValue) * 100;
158
+
159
+ // Veteran status required
160
+ if (!scenario.veteran) {
161
+ eligible = false;
162
+ reasons.push('VA loans require veteran/military eligibility');
163
+ }
164
+
165
+ // Credit score - VA has no official minimum but lenders typically require 580-620
166
+ if (scenario.creditScore < 580) {
167
+ warnings.push('Most VA lenders require 580+ credit score');
168
+ }
169
+
170
+ // LTV - VA allows 100% financing
171
+ if (ltv > 100) {
172
+ eligible = false;
173
+ reasons.push('VA maximum LTV is 100%');
174
+ }
175
+
176
+ // Occupancy - VA is owner-occupied only
177
+ if (scenario.occupancy !== 'primary') {
178
+ eligible = false;
179
+ reasons.push('VA loans require owner occupancy');
180
+ }
181
+
182
+ // No loan limit for full entitlement (post-2020)
183
+ // But there are limits for partial entitlement
184
+
185
+ // Property type restrictions
186
+ if (scenario.propertyType === 'coop') {
187
+ eligible = false;
188
+ reasons.push('VA does not allow co-op properties');
189
+ }
190
+
191
+ return {
192
+ program: 'va',
193
+ eligible,
194
+ reasons: reasons.length > 0 ? reasons : undefined,
195
+ warnings: warnings.length > 0 ? warnings : undefined,
196
+ minDownPayment: 0, // VA allows 0% down
197
+ rateAdjustment: -0.25, // VA rates typically lower
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Check USDA loan eligibility
203
+ */
204
+ export function checkUsdaEligibility(scenario: LoanScenario): EligibilityResult {
205
+ const reasons: string[] = [];
206
+ const warnings: string[] = [];
207
+ let eligible = true;
208
+
209
+ const ltv = (scenario.loanAmount / scenario.propertyValue) * 100;
210
+
211
+ // Rural location required
212
+ if (!scenario.rural) {
213
+ eligible = false;
214
+ reasons.push('USDA loans require rural/eligible suburban location');
215
+ }
216
+
217
+ // Credit score
218
+ if (scenario.creditScore < 640) {
219
+ warnings.push('Credit score below 640 requires manual underwriting');
220
+ }
221
+
222
+ // LTV - USDA allows 100% financing
223
+ if (ltv > 100) {
224
+ eligible = false;
225
+ reasons.push('USDA maximum LTV is 100%');
226
+ }
227
+
228
+ // Occupancy - owner-occupied only
229
+ if (scenario.occupancy !== 'primary') {
230
+ eligible = false;
231
+ reasons.push('USDA loans require owner occupancy');
232
+ }
233
+
234
+ // Property type - single family only
235
+ if (scenario.units && scenario.units > 1) {
236
+ eligible = false;
237
+ reasons.push('USDA loans are for single-family homes only');
238
+ }
239
+
240
+ // Income limits would apply (not modeled here)
241
+ warnings.push('USDA has household income limits - verify eligibility');
242
+
243
+ return {
244
+ program: 'usda',
245
+ eligible,
246
+ reasons: reasons.length > 0 ? reasons : undefined,
247
+ warnings: warnings.length > 0 ? warnings : undefined,
248
+ minDownPayment: 0,
249
+ rateAdjustment: 0,
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Check jumbo loan eligibility
255
+ */
256
+ export function checkJumboEligibility(scenario: LoanScenario): EligibilityResult {
257
+ const reasons: string[] = [];
258
+ const warnings: string[] = [];
259
+ let eligible = true;
260
+
261
+ const ltv = (scenario.loanAmount / scenario.propertyValue) * 100;
262
+ const conformingLimit = getConformingLimit(
263
+ scenario.county ? `${scenario.county}, ${scenario.state}` : undefined,
264
+ scenario.units || 1
265
+ );
266
+
267
+ // Check if actually jumbo
268
+ if (scenario.loanAmount <= conformingLimit) {
269
+ eligible = false;
270
+ reasons.push('Loan amount is within conforming limits - use conventional');
271
+ }
272
+
273
+ // Credit score - jumbo typically requires higher scores
274
+ if (scenario.creditScore < 700) {
275
+ eligible = false;
276
+ reasons.push('Jumbo loans typically require 700+ credit score');
277
+ } else if (scenario.creditScore < 720) {
278
+ warnings.push('Credit score below 720 may limit options');
279
+ }
280
+
281
+ // LTV - jumbo typically more restrictive
282
+ const maxLtv = scenario.occupancy === 'primary' ? 90 : 80;
283
+ if (ltv > maxLtv) {
284
+ eligible = false;
285
+ reasons.push(`Jumbo maximum LTV is ${maxLtv}% for ${scenario.occupancy}`);
286
+ }
287
+
288
+ // Reserves typically required
289
+ warnings.push('Jumbo loans typically require 6-12 months reserves');
290
+
291
+ // Min down payment
292
+ const minDownPct = scenario.occupancy === 'primary' ? 10 : 20;
293
+ const minDownPayment = scenario.propertyValue * (minDownPct / 100);
294
+
295
+ return {
296
+ program: 'jumbo',
297
+ eligible,
298
+ reasons: reasons.length > 0 ? reasons : undefined,
299
+ warnings: warnings.length > 0 ? warnings : undefined,
300
+ minDownPayment,
301
+ rateAdjustment: 0.25, // Jumbo typically slightly higher
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Check all program eligibility
307
+ */
308
+ export function checkAllEligibility(scenario: LoanScenario): EligibilityResult[] {
309
+ return [
310
+ checkConventionalEligibility(scenario),
311
+ checkFhaEligibility(scenario),
312
+ checkVaEligibility(scenario),
313
+ checkUsdaEligibility(scenario),
314
+ checkJumboEligibility(scenario),
315
+ ];
316
+ }
317
+
318
+ /**
319
+ * Find best recommended program
320
+ */
321
+ export function findBestProgram(
322
+ eligibility: EligibilityResult[]
323
+ ): LoanProgram | undefined {
324
+ const eligible = eligibility.filter(e => e.eligible);
325
+ if (eligible.length === 0) return undefined;
326
+
327
+ // Priority order: VA (if eligible), then lowest down payment, then best rate
328
+ const va = eligible.find(e => e.program === 'va');
329
+ if (va) return 'va';
330
+
331
+ const usda = eligible.find(e => e.program === 'usda');
332
+ if (usda) return 'usda';
333
+
334
+ // Sort by min down payment, then rate adjustment
335
+ eligible.sort((a, b) => {
336
+ const downA = a.minDownPayment || 0;
337
+ const downB = b.minDownPayment || 0;
338
+ if (downA !== downB) return downA - downB;
339
+ return (a.rateAdjustment || 0) - (b.rateAdjustment || 0);
340
+ });
341
+
342
+ return eligible[0].program;
343
+ }