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,121 @@
1
+ /**
2
+ * mortctl eligible command - Check loan program eligibility
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { LoanScenario, PropertyType, OccupancyType, LoanPurpose } from '../types';
7
+ import { checkAllEligibility, findBestProgram } from '../lib/eligibility';
8
+
9
+ export function createEligibleCommand(): Command {
10
+ const eligible = new Command('eligible')
11
+ .description('Check eligibility for loan programs')
12
+ .requiredOption('--property-value <amount>', 'Property value')
13
+ .requiredOption('--loan-amount <amount>', 'Loan amount')
14
+ .requiredOption('--credit-score <score>', 'Credit score')
15
+ .option('--property-type <type>', 'Property type', 'single-family')
16
+ .option('--occupancy <type>', 'Occupancy type', 'primary')
17
+ .option('--purpose <type>', 'Loan purpose', 'purchase')
18
+ .option('--units <n>', 'Number of units', '1')
19
+ .option('--county <name>', 'County for loan limits')
20
+ .option('--state <abbr>', 'State abbreviation')
21
+ .option('--veteran', 'Veteran/military eligibility', false)
22
+ .option('--rural', 'Rural/USDA eligible location', false)
23
+ .option('--first-time', 'First-time homebuyer', false)
24
+ .option('--format <type>', 'Output format (json|table)', 'table')
25
+ .action(async (options) => {
26
+ try {
27
+ const scenario: LoanScenario = {
28
+ propertyValue: parseFloat(options.propertyValue),
29
+ loanAmount: parseFloat(options.loanAmount),
30
+ downPayment: parseFloat(options.propertyValue) - parseFloat(options.loanAmount),
31
+ creditScore: parseInt(options.creditScore),
32
+ propertyType: options.propertyType as PropertyType,
33
+ occupancy: options.occupancy as OccupancyType,
34
+ purpose: options.purpose as LoanPurpose,
35
+ units: parseInt(options.units),
36
+ county: options.county,
37
+ state: options.state,
38
+ veteran: options.veteran,
39
+ rural: options.rural,
40
+ firstTimeHomebuyer: options.firstTime,
41
+ interestRate: 6.5, // Default for calculations
42
+ termYears: 30,
43
+ };
44
+
45
+ const eligibility = checkAllEligibility(scenario);
46
+ const recommended = findBestProgram(eligibility);
47
+
48
+ if (options.format === 'json') {
49
+ console.log(JSON.stringify({
50
+ scenario: {
51
+ propertyValue: scenario.propertyValue,
52
+ loanAmount: scenario.loanAmount,
53
+ ltv: (scenario.loanAmount / scenario.propertyValue * 100).toFixed(2),
54
+ creditScore: scenario.creditScore,
55
+ },
56
+ eligibility,
57
+ recommendedProgram: recommended,
58
+ }, null, 2));
59
+ } else {
60
+ const ltv = (scenario.loanAmount / scenario.propertyValue * 100);
61
+
62
+ console.log('═══════════════════════════════════════════════════════════════');
63
+ console.log('LOAN PROGRAM ELIGIBILITY');
64
+ console.log('═══════════════════════════════════════════════════════════════');
65
+ console.log('');
66
+ console.log(`Property Value: $${scenario.propertyValue.toLocaleString()}`);
67
+ console.log(`Loan Amount: $${scenario.loanAmount.toLocaleString()}`);
68
+ console.log(`LTV: ${ltv.toFixed(1)}%`);
69
+ console.log(`Credit Score: ${scenario.creditScore}`);
70
+ console.log(`Property: ${scenario.propertyType}, ${scenario.occupancy}`);
71
+ console.log('');
72
+ console.log('PROGRAM ELIGIBILITY');
73
+ console.log('───────────────────────────────────────────────────────────────');
74
+
75
+ for (const result of eligibility) {
76
+ const status = result.eligible ? '✓' : '✗';
77
+ const programName = result.program.toUpperCase().padEnd(15);
78
+ console.log(`${status} ${programName}`);
79
+
80
+ if (!result.eligible && result.reasons) {
81
+ for (const reason of result.reasons) {
82
+ console.log(` ✗ ${reason}`);
83
+ }
84
+ }
85
+
86
+ if (result.eligible) {
87
+ if (result.minDownPayment !== undefined) {
88
+ console.log(` Min down: $${result.minDownPayment.toLocaleString()}`);
89
+ }
90
+ if (result.rateAdjustment) {
91
+ const adj = result.rateAdjustment > 0 ? `+${result.rateAdjustment}%` : `${result.rateAdjustment}%`;
92
+ console.log(` Rate adj: ${adj}`);
93
+ }
94
+ }
95
+
96
+ if (result.warnings) {
97
+ for (const warning of result.warnings) {
98
+ console.log(` ⚠ ${warning}`);
99
+ }
100
+ }
101
+ console.log('');
102
+ }
103
+
104
+ if (recommended) {
105
+ console.log('───────────────────────────────────────────────────────────────');
106
+ console.log(`💡 RECOMMENDED: ${recommended.toUpperCase()}`);
107
+ } else {
108
+ console.log('───────────────────────────────────────────────────────────────');
109
+ console.log('⚠ No programs eligible - review requirements');
110
+ }
111
+
112
+ console.log('═══════════════════════════════════════════════════════════════');
113
+ }
114
+ } catch (error: any) {
115
+ console.error(`Error: ${error.message}`);
116
+ process.exit(1);
117
+ }
118
+ });
119
+
120
+ return eligible;
121
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * mortctl limits command - Show conforming loan limits
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { getConformingLimit, LOAN_LIMITS_2026, HIGH_COST_COUNTIES } from '../lib/calculations';
7
+
8
+ export function createLimitsCommand(): Command {
9
+ const limits = new Command('limits')
10
+ .description('Show conforming loan limits')
11
+ .option('--county <name>', 'County name (e.g., "Los Angeles")')
12
+ .option('--state <abbr>', 'State abbreviation (e.g., "CA")')
13
+ .option('--units <n>', 'Number of units (1-4)', '1')
14
+ .option('--list-high-cost', 'List all high-cost counties', false)
15
+ .option('--format <type>', 'Output format (json|table)', 'table')
16
+ .action(async (options) => {
17
+ try {
18
+ const units = parseInt(options.units);
19
+
20
+ if (options.listHighCost) {
21
+ if (options.format === 'json') {
22
+ console.log(JSON.stringify(HIGH_COST_COUNTIES, null, 2));
23
+ } else {
24
+ console.log('═══════════════════════════════════════════════════════════════');
25
+ console.log('HIGH-COST COUNTIES (2026)');
26
+ console.log('═══════════════════════════════════════════════════════════════');
27
+ console.log('');
28
+ for (const [county, limit] of Object.entries(HIGH_COST_COUNTIES)) {
29
+ console.log(`${county.padEnd(30)} $${limit.toLocaleString()}`);
30
+ }
31
+ console.log('═══════════════════════════════════════════════════════════════');
32
+ }
33
+ return;
34
+ }
35
+
36
+ const countyKey = options.county && options.state
37
+ ? `${options.county}, ${options.state}`
38
+ : undefined;
39
+
40
+ const conformingLimit = getConformingLimit(countyKey, units);
41
+ const isHighCost = countyKey && HIGH_COST_COUNTIES[countyKey];
42
+
43
+ // Unit multipliers for display
44
+ const unitLimits: Record<number, number> = {};
45
+ for (let u = 1; u <= 4; u++) {
46
+ unitLimits[u] = getConformingLimit(countyKey, u);
47
+ }
48
+
49
+ const result = {
50
+ year: 2026,
51
+ county: countyKey || 'Baseline (standard counties)',
52
+ isHighCost: !!isHighCost,
53
+ units,
54
+ conformingLimit,
55
+ allUnitLimits: unitLimits,
56
+ baseline: LOAN_LIMITS_2026,
57
+ };
58
+
59
+ if (options.format === 'json') {
60
+ console.log(JSON.stringify(result, null, 2));
61
+ } else {
62
+ console.log('═══════════════════════════════════════════════════════════════');
63
+ console.log('CONFORMING LOAN LIMITS (2026)');
64
+ console.log('═══════════════════════════════════════════════════════════════');
65
+ console.log('');
66
+ console.log(`Area: ${result.county}`);
67
+ console.log(`High-Cost: ${result.isHighCost ? 'Yes' : 'No'}`);
68
+ console.log('');
69
+ console.log('LIMITS BY UNIT COUNT');
70
+ console.log('───────────────────────────────────────────────────────────────');
71
+ console.log(`1-Unit: $${unitLimits[1].toLocaleString()}`);
72
+ console.log(`2-Unit: $${unitLimits[2].toLocaleString()}`);
73
+ console.log(`3-Unit: $${unitLimits[3].toLocaleString()}`);
74
+ console.log(`4-Unit: $${unitLimits[4].toLocaleString()}`);
75
+ console.log('');
76
+ console.log('NATIONAL BASELINE');
77
+ console.log('───────────────────────────────────────────────────────────────');
78
+ console.log(`Conforming Floor: $${LOAN_LIMITS_2026.baseline.toLocaleString()}`);
79
+ console.log(`High-Cost Ceiling: $${LOAN_LIMITS_2026.highCost.toLocaleString()}`);
80
+ console.log(`FHA Floor: $${LOAN_LIMITS_2026.fhaFloor.toLocaleString()}`);
81
+ console.log(`FHA Ceiling: $${LOAN_LIMITS_2026.fhaCeiling.toLocaleString()}`);
82
+ console.log('═══════════════════════════════════════════════════════════════');
83
+ }
84
+ } catch (error: any) {
85
+ console.error(`Error: ${error.message}`);
86
+ process.exit(1);
87
+ }
88
+ });
89
+
90
+ return limits;
91
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * mortctl ltv command - Calculate LTV/CLTV/HCLTV
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { calculateLtv, calculatePmi } from '../lib/calculations';
7
+
8
+ export function createLtvCommand(): Command {
9
+ const ltv = new Command('ltv')
10
+ .description('Calculate loan-to-value ratios')
11
+ .requiredOption('--property-value <amount>', 'Property value or purchase price')
12
+ .option('--loan-amount <amount>', 'Loan amount (or use --down-payment)')
13
+ .option('--down-payment <amount>', 'Down payment amount')
14
+ .option('--second-lien <amount>', 'Second mortgage/lien amount', '0')
15
+ .option('--heloc <amount>', 'HELOC amount', '0')
16
+ .option('--credit-score <score>', 'Credit score for PMI estimate', '720')
17
+ .option('--format <type>', 'Output format (json|table)', 'table')
18
+ .action(async (options) => {
19
+ try {
20
+ const propertyValue = parseFloat(options.propertyValue);
21
+ let loanAmount: number;
22
+
23
+ if (options.loanAmount) {
24
+ loanAmount = parseFloat(options.loanAmount);
25
+ } else if (options.downPayment) {
26
+ loanAmount = propertyValue - parseFloat(options.downPayment);
27
+ } else {
28
+ console.error('Error: Must specify either --loan-amount or --down-payment');
29
+ process.exit(1);
30
+ }
31
+
32
+ const secondLien = parseFloat(options.secondLien);
33
+ const heloc = parseFloat(options.heloc);
34
+ const creditScore = parseInt(options.creditScore);
35
+
36
+ const ltvResult = calculateLtv(propertyValue, loanAmount, secondLien, heloc);
37
+ const pmiResult = calculatePmi({
38
+ ltv: ltvResult.ltv,
39
+ creditScore,
40
+ loanAmount,
41
+ });
42
+
43
+ const downPayment = propertyValue - loanAmount;
44
+ const downPaymentPct = (downPayment / propertyValue) * 100;
45
+
46
+ if (options.format === 'json') {
47
+ console.log(JSON.stringify({
48
+ propertyValue,
49
+ loanAmount,
50
+ downPayment,
51
+ downPaymentPct: Math.round(downPaymentPct * 100) / 100,
52
+ ...ltvResult,
53
+ pmi: pmiResult,
54
+ }, null, 2));
55
+ } else {
56
+ console.log('═══════════════════════════════════════════════════════════════');
57
+ console.log('LTV CALCULATION');
58
+ console.log('═══════════════════════════════════════════════════════════════');
59
+ console.log('');
60
+ console.log(`Property Value: $${propertyValue.toLocaleString()}`);
61
+ console.log(`Loan Amount: $${loanAmount.toLocaleString()}`);
62
+ console.log(`Down Payment: $${downPayment.toLocaleString()} (${downPaymentPct.toFixed(1)}%)`);
63
+ console.log('');
64
+ console.log('LTV RATIOS');
65
+ console.log('───────────────────────────────────────────────────────────────');
66
+ console.log(`LTV: ${ltvResult.ltv.toFixed(2)}%`);
67
+ if (secondLien > 0 || heloc > 0) {
68
+ console.log(`CLTV: ${ltvResult.cltv.toFixed(2)}%`);
69
+ console.log(`HCLTV: ${ltvResult.hcltv.toFixed(2)}%`);
70
+ }
71
+ console.log('');
72
+
73
+ if (pmiResult.required) {
74
+ console.log('PMI ESTIMATE');
75
+ console.log('───────────────────────────────────────────────────────────────');
76
+ console.log(`PMI Required: Yes`);
77
+ console.log(`Monthly PMI: $${pmiResult.monthlyAmount.toLocaleString()}`);
78
+ console.log(`Annual PMI: $${pmiResult.annualAmount.toLocaleString()}`);
79
+ console.log(`PMI Rate: ${pmiResult.ratePercent}%`);
80
+ console.log(`Auto-Cancel at: ${pmiResult.cancellationLtv}% LTV`);
81
+ if (pmiResult.monthsToCancel) {
82
+ console.log(`Est. Months to Cancel: ~${pmiResult.monthsToCancel}`);
83
+ }
84
+ } else {
85
+ console.log('PMI: Not required (LTV ≤ 80%)' );
86
+ }
87
+
88
+ console.log('═══════════════════════════════════════════════════════════════');
89
+ }
90
+ } catch (error: any) {
91
+ console.error(`Error: ${error.message}`);
92
+ process.exit(1);
93
+ }
94
+ });
95
+
96
+ return ltv;
97
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * mortctl payment command - Calculate monthly payment breakdown
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { calculatePaymentBreakdown, calculateMonthlyPayment } from '../lib/calculations';
7
+ import { LoanScenario, PropertyType, OccupancyType, LoanPurpose } from '../types';
8
+
9
+ export function createPaymentCommand(): Command {
10
+ const payment = new Command('payment')
11
+ .description('Calculate monthly payment breakdown (PITI)')
12
+ .requiredOption('--loan-amount <amount>', 'Loan amount')
13
+ .requiredOption('--rate <percent>', 'Interest rate (e.g., 6.5)')
14
+ .option('--term <years>', 'Loan term in years', '30')
15
+ .option('--property-value <amount>', 'Property value (for PMI/tax estimates)')
16
+ .option('--credit-score <score>', 'Credit score (for PMI)', '720')
17
+ .option('--taxes <amount>', 'Annual property taxes')
18
+ .option('--insurance <amount>', 'Annual homeowners insurance')
19
+ .option('--hoa <amount>', 'Monthly HOA dues', '0')
20
+ .option('--format <type>', 'Output format (json|table)', 'table')
21
+ .action(async (options) => {
22
+ try {
23
+ const loanAmount = parseFloat(options.loanAmount);
24
+ const rate = parseFloat(options.rate);
25
+ const term = parseInt(options.term);
26
+ const propertyValue = options.propertyValue ? parseFloat(options.propertyValue) : loanAmount * 1.25;
27
+
28
+ const scenario: LoanScenario = {
29
+ propertyValue,
30
+ loanAmount,
31
+ downPayment: propertyValue - loanAmount,
32
+ interestRate: rate,
33
+ termYears: term,
34
+ creditScore: parseInt(options.creditScore),
35
+ propertyType: 'single-family' as PropertyType,
36
+ occupancy: 'primary' as OccupancyType,
37
+ purpose: 'purchase' as LoanPurpose,
38
+ propertyTaxes: options.taxes ? parseFloat(options.taxes) : undefined,
39
+ homeownersInsurance: options.insurance ? parseFloat(options.insurance) : undefined,
40
+ hoaDues: parseFloat(options.hoa),
41
+ };
42
+
43
+ const breakdown = calculatePaymentBreakdown(scenario);
44
+ const ltv = (loanAmount / propertyValue * 100);
45
+
46
+ // Calculate total interest over life of loan
47
+ const piOnly = calculateMonthlyPayment(loanAmount, rate, term);
48
+ const totalPayments = piOnly * term * 12;
49
+ const totalInterest = totalPayments - loanAmount;
50
+
51
+ if (options.format === 'json') {
52
+ console.log(JSON.stringify({
53
+ loanAmount,
54
+ interestRate: rate,
55
+ termYears: term,
56
+ ltv: Math.round(ltv * 100) / 100,
57
+ breakdown,
58
+ lifetime: {
59
+ totalPayments: Math.round(totalPayments),
60
+ totalInterest: Math.round(totalInterest),
61
+ },
62
+ }, null, 2));
63
+ } else {
64
+ console.log('═══════════════════════════════════════════════════════════════');
65
+ console.log('MONTHLY PAYMENT BREAKDOWN');
66
+ console.log('═══════════════════════════════════════════════════════════════');
67
+ console.log('');
68
+ console.log(`Loan Amount: $${loanAmount.toLocaleString()}`);
69
+ console.log(`Interest Rate: ${rate}%`);
70
+ console.log(`Term: ${term} years`);
71
+ console.log(`LTV: ${ltv.toFixed(1)}%`);
72
+ console.log('');
73
+ console.log('PAYMENT BREAKDOWN');
74
+ console.log('───────────────────────────────────────────────────────────────');
75
+ console.log(`Principal & Interest: $${breakdown.principalAndInterest.toLocaleString().padStart(10)}`);
76
+ console.log(`Property Taxes: $${breakdown.propertyTaxes.toLocaleString().padStart(10)}`);
77
+ console.log(`Homeowners Insurance: $${breakdown.homeownersInsurance.toLocaleString().padStart(10)}`);
78
+ if (breakdown.mortgageInsurance > 0) {
79
+ console.log(`Mortgage Insurance: $${breakdown.mortgageInsurance.toLocaleString().padStart(10)}`);
80
+ }
81
+ if (breakdown.hoaDues > 0) {
82
+ console.log(`HOA Dues: $${breakdown.hoaDues.toLocaleString().padStart(10)}`);
83
+ }
84
+ console.log('───────────────────────────────────────────────────────────────');
85
+ console.log(`TOTAL PAYMENT: $${breakdown.totalPayment.toLocaleString().padStart(10)}`);
86
+ console.log('');
87
+ console.log('LOAN LIFETIME');
88
+ console.log('───────────────────────────────────────────────────────────────');
89
+ console.log(`Total Payments: $${Math.round(totalPayments).toLocaleString()}`);
90
+ console.log(`Total Interest: $${Math.round(totalInterest).toLocaleString()}`);
91
+ console.log('═══════════════════════════════════════════════════════════════');
92
+ }
93
+ } catch (error: any) {
94
+ console.error(`Error: ${error.message}`);
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ return payment;
100
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mortctl - Mortgage underwriting calculations and eligibility
5
+ * Part of the LendCtl Suite
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { createLtvCommand } from './commands/ltv';
10
+ import { createEligibleCommand } from './commands/eligible';
11
+ import { createPaymentCommand } from './commands/payment';
12
+ import { createLimitsCommand } from './commands/limits';
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('mortctl')
18
+ .description('Mortgage underwriting calculations and eligibility - part of the LendCtl Suite')
19
+ .version('0.1.0');
20
+
21
+ // Add commands
22
+ program.addCommand(createLtvCommand());
23
+ program.addCommand(createEligibleCommand());
24
+ program.addCommand(createPaymentCommand());
25
+ program.addCommand(createLimitsCommand());
26
+
27
+ program.parse();
28
+
29
+ // Export library components for programmatic use
30
+ export * from './lib/calculations';
31
+ export * from './lib/eligibility';
32
+ export * from './types';