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
package/src/types.ts ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Mortgage underwriting types for mortctl
3
+ */
4
+
5
+ /** Loan program types */
6
+ export type LoanProgram =
7
+ | 'conventional'
8
+ | 'fha'
9
+ | 'va'
10
+ | 'usda'
11
+ | 'jumbo'
12
+ | 'non-qm';
13
+
14
+ /** Property types */
15
+ export type PropertyType =
16
+ | 'single-family'
17
+ | 'condo'
18
+ | 'townhouse'
19
+ | 'multi-family-2'
20
+ | 'multi-family-3'
21
+ | 'multi-family-4'
22
+ | 'manufactured'
23
+ | 'coop';
24
+
25
+ /** Occupancy types */
26
+ export type OccupancyType =
27
+ | 'primary'
28
+ | 'second-home'
29
+ | 'investment';
30
+
31
+ /** Loan purpose */
32
+ export type LoanPurpose =
33
+ | 'purchase'
34
+ | 'rate-term-refi'
35
+ | 'cash-out-refi';
36
+
37
+ /** Amortization type */
38
+ export type AmortizationType =
39
+ | 'fixed'
40
+ | 'arm-5-1'
41
+ | 'arm-7-1'
42
+ | 'arm-10-1'
43
+ | 'interest-only';
44
+
45
+ /**
46
+ * Conforming loan limits by year and area
47
+ * 2026 limits (estimated based on trends)
48
+ */
49
+ export interface LoanLimits {
50
+ /** Standard conforming limit (baseline counties) */
51
+ baseline: number;
52
+ /** High-cost area ceiling */
53
+ highCost: number;
54
+ /** FHA floor */
55
+ fhaFloor: number;
56
+ /** FHA ceiling */
57
+ fhaCeiling: number;
58
+ }
59
+
60
+ /**
61
+ * LTV calculation result
62
+ */
63
+ export interface LtvResult {
64
+ /** Loan-to-value ratio (primary mortgage only) */
65
+ ltv: number;
66
+ /** Combined loan-to-value (includes second liens) */
67
+ cltv: number;
68
+ /** Home equity combined LTV (includes HELOCs) */
69
+ hcltv: number;
70
+ /** Whether PMI is required */
71
+ pmiRequired: boolean;
72
+ /** Estimated PMI amount (monthly) */
73
+ estimatedPmi?: number;
74
+ /** When PMI can be removed (at what LTV) */
75
+ pmiRemovalLtv?: number;
76
+ }
77
+
78
+ /**
79
+ * PMI calculation inputs
80
+ */
81
+ export interface PmiInputs {
82
+ /** LTV percentage */
83
+ ltv: number;
84
+ /** Credit score */
85
+ creditScore: number;
86
+ /** Loan amount */
87
+ loanAmount: number;
88
+ /** Loan term in years */
89
+ termYears?: number;
90
+ /** Property type */
91
+ propertyType?: PropertyType;
92
+ /** Occupancy */
93
+ occupancy?: OccupancyType;
94
+ }
95
+
96
+ /**
97
+ * PMI calculation result
98
+ */
99
+ export interface PmiResult {
100
+ /** Whether PMI is required */
101
+ required: boolean;
102
+ /** Monthly PMI amount */
103
+ monthlyAmount: number;
104
+ /** Annual PMI amount */
105
+ annualAmount: number;
106
+ /** PMI rate (percentage of loan) */
107
+ ratePercent: number;
108
+ /** LTV at which PMI can be cancelled */
109
+ cancellationLtv: number;
110
+ /** Estimated months until cancellation (based on normal amortization) */
111
+ monthsToCancel?: number;
112
+ }
113
+
114
+ /**
115
+ * Loan eligibility result
116
+ */
117
+ export interface EligibilityResult {
118
+ /** Program name */
119
+ program: LoanProgram;
120
+ /** Whether eligible */
121
+ eligible: boolean;
122
+ /** Reasons if not eligible */
123
+ reasons?: string[];
124
+ /** Warnings/conditions */
125
+ warnings?: string[];
126
+ /** Maximum loan amount for this program */
127
+ maxLoanAmount?: number;
128
+ /** Minimum down payment required */
129
+ minDownPayment?: number;
130
+ /** Estimated rate adjustment vs baseline */
131
+ rateAdjustment?: number;
132
+ }
133
+
134
+ /**
135
+ * Complete loan scenario
136
+ */
137
+ export interface LoanScenario {
138
+ /** Purchase price or appraised value */
139
+ propertyValue: number;
140
+ /** Down payment amount */
141
+ downPayment: number;
142
+ /** Base loan amount */
143
+ loanAmount: number;
144
+ /** Second lien amount (if any) */
145
+ secondLien?: number;
146
+ /** HELOC amount (if any) */
147
+ heloc?: number;
148
+ /** Interest rate */
149
+ interestRate: number;
150
+ /** Loan term in years */
151
+ termYears: number;
152
+ /** Credit score */
153
+ creditScore: number;
154
+ /** Property type */
155
+ propertyType: PropertyType;
156
+ /** Occupancy */
157
+ occupancy: OccupancyType;
158
+ /** Loan purpose */
159
+ purpose: LoanPurpose;
160
+ /** Number of units (for multi-family) */
161
+ units?: number;
162
+ /** County for loan limits */
163
+ county?: string;
164
+ /** State */
165
+ state?: string;
166
+ /** First-time homebuyer */
167
+ firstTimeHomebuyer?: boolean;
168
+ /** Veteran status (for VA) */
169
+ veteran?: boolean;
170
+ /** Rural location (for USDA) */
171
+ rural?: boolean;
172
+ /** Annual property taxes */
173
+ propertyTaxes?: number;
174
+ /** Annual homeowners insurance */
175
+ homeownersInsurance?: number;
176
+ /** Monthly HOA */
177
+ hoaDues?: number;
178
+ }
179
+
180
+ /**
181
+ * Monthly payment breakdown
182
+ */
183
+ export interface PaymentBreakdown {
184
+ /** Principal and interest */
185
+ principalAndInterest: number;
186
+ /** Property taxes (monthly) */
187
+ propertyTaxes: number;
188
+ /** Homeowners insurance (monthly) */
189
+ homeownersInsurance: number;
190
+ /** PMI/MIP (if applicable) */
191
+ mortgageInsurance: number;
192
+ /** HOA dues */
193
+ hoaDues: number;
194
+ /** Total PITI + HOA */
195
+ totalPayment: number;
196
+ }
197
+
198
+ /**
199
+ * Full underwriting analysis
200
+ */
201
+ export interface UnderwritingAnalysis {
202
+ /** Loan scenario */
203
+ scenario: LoanScenario;
204
+ /** LTV analysis */
205
+ ltv: LtvResult;
206
+ /** Payment breakdown */
207
+ payment: PaymentBreakdown;
208
+ /** Program eligibility for each program */
209
+ eligibility: EligibilityResult[];
210
+ /** Best recommended program */
211
+ recommendedProgram?: LoanProgram;
212
+ /** Risk flags */
213
+ riskFlags: string[];
214
+ /** Recommendations */
215
+ recommendations: string[];
216
+ }
@@ -0,0 +1,154 @@
1
+ import {
2
+ calculateLtv,
3
+ calculatePmi,
4
+ calculateMonthlyPayment,
5
+ calculatePaymentBreakdown,
6
+ getConformingLimit,
7
+ calculateFhaMip,
8
+ calculateVaFundingFee,
9
+ LOAN_LIMITS_2026,
10
+ } from '../src/lib/calculations';
11
+ import { LoanScenario } from '../src/types';
12
+
13
+ describe('calculateMonthlyPayment', () => {
14
+ it('should calculate correct P&I for 30-year loan', () => {
15
+ const payment = calculateMonthlyPayment(300000, 6.5, 30);
16
+ expect(payment).toBeCloseTo(1896.20, 0);
17
+ });
18
+
19
+ it('should calculate correct P&I for 15-year loan', () => {
20
+ const payment = calculateMonthlyPayment(300000, 6.0, 15);
21
+ expect(payment).toBeCloseTo(2531.57, 0);
22
+ });
23
+
24
+ it('should handle 0% interest', () => {
25
+ const payment = calculateMonthlyPayment(120000, 0, 30);
26
+ expect(payment).toBeCloseTo(333.33, 0);
27
+ });
28
+ });
29
+
30
+ describe('calculateLtv', () => {
31
+ it('should calculate LTV correctly', () => {
32
+ const result = calculateLtv(400000, 320000);
33
+ expect(result.ltv).toBe(80);
34
+ expect(result.pmiRequired).toBe(false);
35
+ });
36
+
37
+ it('should identify PMI required above 80%', () => {
38
+ const result = calculateLtv(400000, 380000);
39
+ expect(result.ltv).toBe(95);
40
+ expect(result.pmiRequired).toBe(true);
41
+ });
42
+
43
+ it('should calculate CLTV with second lien', () => {
44
+ const result = calculateLtv(400000, 320000, 40000);
45
+ expect(result.ltv).toBe(80);
46
+ expect(result.cltv).toBe(90);
47
+ });
48
+
49
+ it('should calculate HCLTV with HELOC', () => {
50
+ const result = calculateLtv(400000, 320000, 0, 50000);
51
+ expect(result.ltv).toBe(80);
52
+ expect(result.hcltv).toBe(92.5);
53
+ });
54
+ });
55
+
56
+ describe('calculatePmi', () => {
57
+ it('should return no PMI for 80% LTV', () => {
58
+ const result = calculatePmi({ ltv: 80, creditScore: 720, loanAmount: 320000 });
59
+ expect(result.required).toBe(false);
60
+ expect(result.monthlyAmount).toBe(0);
61
+ });
62
+
63
+ it('should calculate PMI for 95% LTV', () => {
64
+ const result = calculatePmi({ ltv: 95, creditScore: 720, loanAmount: 380000 });
65
+ expect(result.required).toBe(true);
66
+ expect(result.monthlyAmount).toBeGreaterThan(0);
67
+ expect(result.cancellationLtv).toBe(78);
68
+ });
69
+
70
+ it('should have lower PMI for higher credit scores', () => {
71
+ const low = calculatePmi({ ltv: 90, creditScore: 680, loanAmount: 360000 });
72
+ const high = calculatePmi({ ltv: 90, creditScore: 760, loanAmount: 360000 });
73
+ expect(high.monthlyAmount).toBeLessThan(low.monthlyAmount);
74
+ });
75
+ });
76
+
77
+ describe('getConformingLimit', () => {
78
+ it('should return baseline for standard county', () => {
79
+ const limit = getConformingLimit(undefined, 1);
80
+ expect(limit).toBe(LOAN_LIMITS_2026.baseline);
81
+ });
82
+
83
+ it('should return high-cost limit for high-cost county', () => {
84
+ const limit = getConformingLimit('Los Angeles, CA', 1);
85
+ expect(limit).toBe(LOAN_LIMITS_2026.highCost);
86
+ });
87
+
88
+ it('should apply unit multipliers', () => {
89
+ const oneUnit = getConformingLimit(undefined, 1);
90
+ const twoUnit = getConformingLimit(undefined, 2);
91
+ expect(twoUnit).toBeGreaterThan(oneUnit);
92
+ expect(twoUnit).toBeCloseTo(oneUnit * 1.28, -2);
93
+ });
94
+ });
95
+
96
+ describe('calculateFhaMip', () => {
97
+ it('should calculate upfront MIP at 1.75%', () => {
98
+ const result = calculateFhaMip(300000, 96.5, 30);
99
+ expect(result.upfront).toBe(5250);
100
+ });
101
+
102
+ it('should calculate annual MIP based on LTV and term', () => {
103
+ const highLtv = calculateFhaMip(300000, 95, 30);
104
+ const lowLtv = calculateFhaMip(300000, 85, 30);
105
+ expect(highLtv.annualRate).toBeGreaterThan(lowLtv.annualRate);
106
+ });
107
+ });
108
+
109
+ describe('calculateVaFundingFee', () => {
110
+ it('should calculate lower fee with down payment', () => {
111
+ const noDown = calculateVaFundingFee(300000, 0, true);
112
+ const withDown = calculateVaFundingFee(300000, 10, true);
113
+ expect(withDown).toBeLessThan(noDown);
114
+ });
115
+
116
+ it('should calculate higher fee for subsequent use', () => {
117
+ const first = calculateVaFundingFee(300000, 0, true);
118
+ const subsequent = calculateVaFundingFee(300000, 0, false);
119
+ expect(subsequent).toBeGreaterThan(first);
120
+ });
121
+ });
122
+
123
+ describe('calculatePaymentBreakdown', () => {
124
+ const scenario: LoanScenario = {
125
+ propertyValue: 400000,
126
+ loanAmount: 320000,
127
+ downPayment: 80000,
128
+ interestRate: 6.5,
129
+ termYears: 30,
130
+ creditScore: 720,
131
+ propertyType: 'single-family',
132
+ occupancy: 'primary',
133
+ purpose: 'purchase',
134
+ };
135
+
136
+ it('should calculate all payment components', () => {
137
+ const breakdown = calculatePaymentBreakdown(scenario);
138
+ expect(breakdown.principalAndInterest).toBeGreaterThan(0);
139
+ expect(breakdown.propertyTaxes).toBeGreaterThan(0);
140
+ expect(breakdown.homeownersInsurance).toBeGreaterThan(0);
141
+ expect(breakdown.totalPayment).toBeGreaterThan(breakdown.principalAndInterest);
142
+ });
143
+
144
+ it('should not include PMI at 80% LTV', () => {
145
+ const breakdown = calculatePaymentBreakdown(scenario);
146
+ expect(breakdown.mortgageInsurance).toBe(0);
147
+ });
148
+
149
+ it('should include PMI above 80% LTV', () => {
150
+ const highLtvScenario = { ...scenario, loanAmount: 380000, downPayment: 20000 };
151
+ const breakdown = calculatePaymentBreakdown(highLtvScenario);
152
+ expect(breakdown.mortgageInsurance).toBeGreaterThan(0);
153
+ });
154
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "tests"]
19
+ }