@wirms/calculator-engine 1.0.1 → 2.0.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.
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ type TxType = 'purchase' | 'rateTermRefi' | 'cashOutRefi';
2
+ type Lang = 'en' | 'es';
1
3
  interface DSCRInput {
2
4
  /** Purchase price or current value */
3
5
  purchasePrice: number;
@@ -26,6 +28,28 @@ interface DSCRInput {
26
28
  /** Whether this is an interest-only loan */
27
29
  interestOnly?: boolean;
28
30
  }
31
+ interface DSCRInputFull {
32
+ /** Monthly gross rental income */
33
+ monthlyRent: number;
34
+ /** Target loan amount (not derived from price × LTV) */
35
+ loanAmount: number;
36
+ /** Annual interest rate as percentage (e.g. 7.5 for 7.5%) */
37
+ interestRate: number;
38
+ /** Loan term in years (15–40) */
39
+ loanTerm: number;
40
+ /** Annual property taxes */
41
+ annualTaxes: number;
42
+ /** Annual hazard insurance */
43
+ annualInsurance: number;
44
+ /** Monthly HOA dues */
45
+ monthlyHOA: number;
46
+ /** As-is appraised value */
47
+ appraisedValue: number;
48
+ /** FICO credit score (580–800) */
49
+ creditScore: number;
50
+ /** Transaction type */
51
+ transactionType: TxType;
52
+ }
29
53
  interface DSCRResult {
30
54
  dscr: number;
31
55
  loanAmount: number;
@@ -39,18 +63,121 @@ interface DSCRResult {
39
63
  /** DSCR tier label */
40
64
  tier: 'excellent' | 'good' | 'acceptable' | 'weak' | 'negative';
41
65
  }
66
+ interface DSCRResultFull {
67
+ /** DSCR ratio (rent / PITIA) */
68
+ dscr: number;
69
+ /** Monthly principal & interest */
70
+ pi: number;
71
+ /** Monthly taxes (annualTaxes / 12) */
72
+ taxes: number;
73
+ /** Monthly insurance (annualInsurance / 12) */
74
+ insurance: number;
75
+ /** Total PITIA (pi + taxes + insurance + hoa) */
76
+ pitia: number;
77
+ /** Net cash flow (rent - pitia) */
78
+ cashFlow: number;
79
+ /** Max LTV % for user's current scenario (DSCR-based) */
80
+ userLtvPct: number | null;
81
+ /** Max LTV % at DSCR ≥ 1.0 (algebraic solver) */
82
+ dscrLtvPct: number | null;
83
+ /** Max loan at DSCR = 1.0, capped by LTV × appraisal */
84
+ dscrMaxLoan: number | null;
85
+ /** Difference: dscrMaxLoan - loanAmount */
86
+ dscrLoanDelta: number | null;
87
+ /** Monthly rent needed to achieve DSCR = 1.0 at current loan */
88
+ rentForDscr1: number;
89
+ /** Rent gap: rentForDscr1 - monthlyRent */
90
+ rentGap: number;
91
+ /** Alt qualifying max LTV % (85/85/80) */
92
+ altLtvPct: number;
93
+ /** Alt qualifying max loan (appraisedValue × altLtvPct) */
94
+ altMaxLoan: number;
95
+ /** Difference: altMaxLoan - loanAmount */
96
+ altLoanDelta: number;
97
+ /** Leverage gap: altMaxLoan - dscrMaxLoan (or altMaxLoan if no DSCR program) */
98
+ leverageGap: number;
99
+ }
42
100
  interface SolverResult {
43
101
  value: number;
44
102
  resultingDSCR: number;
45
103
  feasible: boolean;
46
104
  }
105
+ interface MaxDscrLoanResult {
106
+ maxLoan: number;
107
+ ltvPct: number;
108
+ binding: 'dscr' | 'ltv';
109
+ }
110
+ interface CreditInsight {
111
+ message: string;
112
+ type: 'boost' | 'info' | 'warning';
113
+ }
114
+ interface AltStrategyMessages {
115
+ subtitle: string;
116
+ planBNote: string;
117
+ planCNote: string;
118
+ }
47
119
 
48
120
  declare function clamp(value: number, min: number, max: number): number;
49
121
  declare function formatCurrency(value: number, decimals?: number): string;
122
+ declare function formatCompact(n: number): string;
50
123
  declare function calculateDSCR(input: DSCRInput): DSCRResult;
51
- /** Find max loan amount that achieves targetDSCR */
124
+ /**
125
+ * Full DSCR calculator matching lukeroasst.com.
126
+ * DSCR = rent / PITIA. Includes LTV matrix lookups,
127
+ * algebraic max-loan solver, alt qualifying comparison,
128
+ * and rent gap analysis.
129
+ */
130
+ declare function calculateDSCRFull(input: DSCRInputFull): DSCRResultFull;
131
+ /** Find max loan amount that achieves targetDSCR (v1 NOI-based) */
52
132
  declare function solveMaxLoan(input: Omit<DSCRInput, 'ltv'>, targetDSCR: number): SolverResult;
53
- /** Find minimum monthly rent to achieve targetDSCR */
133
+ /** Find minimum monthly rent to achieve targetDSCR (v1 NOI-based) */
54
134
  declare function solveMinRent(input: DSCRInput, targetDSCR: number): SolverResult;
55
135
 
56
- export { type DSCRInput, type DSCRResult, type SolverResult, calculateDSCR, clamp, formatCurrency, solveMaxLoan, solveMinRent };
136
+ /**
137
+ * Real lender LTV matrix: DSCR x FICO x Loan Amount x TxType -> Max LTV
138
+ *
139
+ * Sources: HomeXpress (verified S49), theLender (#theNONI), Acra Lending,
140
+ * ARC Homes (Edge), American Heritage, HomeBridge.
141
+ *
142
+ * HARD CAPS: 85% purchase, 85% R/T, 80% cash-out. No exceptions.
143
+ */
144
+ declare function getMaxLtv(dscr: number, fico: number, loanAmt: number, txType: TxType): number | null;
145
+ /**
146
+ * Alternative qualifying max LTV (Asset Depletion, Bank Statement, P&L).
147
+ * Hard caps: 85% purchase, 85% rate/term, 80% cash-out.
148
+ */
149
+ declare function getAltLtv(txType: TxType): number;
150
+
151
+ /**
152
+ * Algebraic solver: find max loan where DSCR = 1.0, capped by LTV x appraised.
153
+ *
154
+ * DSCR = rent / PITIA. PITIA = PI + taxes + insurance + HOA.
155
+ * PI = loan x paymentFactor.
156
+ * So: loan = (rent/targetDscr - fixedExpenses) / paymentFactor.
157
+ * Result is capped by LTV x appraisedValue (secondary constraint, rarely binding).
158
+ *
159
+ * @param monthlyRent - Gross monthly rental income
160
+ * @param interestRate - Annual rate as percentage (e.g. 7.5)
161
+ * @param loanTerm - Loan term in years
162
+ * @param annualTaxes - Annual property taxes
163
+ * @param annualInsurance - Annual hazard insurance
164
+ * @param monthlyHOA - Monthly HOA dues
165
+ * @param appraisedValue - As-is appraised value
166
+ * @param fico - Borrower FICO score
167
+ * @param txType - Transaction type
168
+ */
169
+ declare function getMaxDscrLoan(monthlyRent: number, interestRate: number, loanTerm: number, annualTaxes: number, annualInsurance: number, monthlyHOA: number, appraisedValue: number, fico: number, txType: TxType): MaxDscrLoanResult | null;
170
+
171
+ /**
172
+ * FICO-aware credit insight messaging.
173
+ * Returns a contextual message based on the borrower's score relative
174
+ * to the next LTV tier breakpoint.
175
+ */
176
+ declare function getCreditInsight(fico: number, dscr: number, loanAmt: number, txType: TxType, currentLtv: number | null, lang: Lang): CreditInsight | null;
177
+ /**
178
+ * FICO-aware alternative strategy messaging for Plan B (Asset Depletion)
179
+ * and Plan C (Bank Statement / P&L).
180
+ */
181
+ declare function getAltStrategyMessage(fico: number, _dscr: number, txType: TxType, lang: Lang): AltStrategyMessages;
182
+
183
+ export { type AltStrategyMessages, type CreditInsight, type DSCRInput, type DSCRInputFull, type DSCRResult, type DSCRResultFull, type Lang, type MaxDscrLoanResult, type SolverResult, type TxType, calculateDSCR, calculateDSCRFull, clamp, formatCompact, formatCurrency, getAltLtv, getAltStrategyMessage, getCreditInsight, getMaxDscrLoan, getMaxLtv, solveMaxLoan, solveMinRent };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ type TxType = 'purchase' | 'rateTermRefi' | 'cashOutRefi';
2
+ type Lang = 'en' | 'es';
1
3
  interface DSCRInput {
2
4
  /** Purchase price or current value */
3
5
  purchasePrice: number;
@@ -26,6 +28,28 @@ interface DSCRInput {
26
28
  /** Whether this is an interest-only loan */
27
29
  interestOnly?: boolean;
28
30
  }
31
+ interface DSCRInputFull {
32
+ /** Monthly gross rental income */
33
+ monthlyRent: number;
34
+ /** Target loan amount (not derived from price × LTV) */
35
+ loanAmount: number;
36
+ /** Annual interest rate as percentage (e.g. 7.5 for 7.5%) */
37
+ interestRate: number;
38
+ /** Loan term in years (15–40) */
39
+ loanTerm: number;
40
+ /** Annual property taxes */
41
+ annualTaxes: number;
42
+ /** Annual hazard insurance */
43
+ annualInsurance: number;
44
+ /** Monthly HOA dues */
45
+ monthlyHOA: number;
46
+ /** As-is appraised value */
47
+ appraisedValue: number;
48
+ /** FICO credit score (580–800) */
49
+ creditScore: number;
50
+ /** Transaction type */
51
+ transactionType: TxType;
52
+ }
29
53
  interface DSCRResult {
30
54
  dscr: number;
31
55
  loanAmount: number;
@@ -39,18 +63,121 @@ interface DSCRResult {
39
63
  /** DSCR tier label */
40
64
  tier: 'excellent' | 'good' | 'acceptable' | 'weak' | 'negative';
41
65
  }
66
+ interface DSCRResultFull {
67
+ /** DSCR ratio (rent / PITIA) */
68
+ dscr: number;
69
+ /** Monthly principal & interest */
70
+ pi: number;
71
+ /** Monthly taxes (annualTaxes / 12) */
72
+ taxes: number;
73
+ /** Monthly insurance (annualInsurance / 12) */
74
+ insurance: number;
75
+ /** Total PITIA (pi + taxes + insurance + hoa) */
76
+ pitia: number;
77
+ /** Net cash flow (rent - pitia) */
78
+ cashFlow: number;
79
+ /** Max LTV % for user's current scenario (DSCR-based) */
80
+ userLtvPct: number | null;
81
+ /** Max LTV % at DSCR ≥ 1.0 (algebraic solver) */
82
+ dscrLtvPct: number | null;
83
+ /** Max loan at DSCR = 1.0, capped by LTV × appraisal */
84
+ dscrMaxLoan: number | null;
85
+ /** Difference: dscrMaxLoan - loanAmount */
86
+ dscrLoanDelta: number | null;
87
+ /** Monthly rent needed to achieve DSCR = 1.0 at current loan */
88
+ rentForDscr1: number;
89
+ /** Rent gap: rentForDscr1 - monthlyRent */
90
+ rentGap: number;
91
+ /** Alt qualifying max LTV % (85/85/80) */
92
+ altLtvPct: number;
93
+ /** Alt qualifying max loan (appraisedValue × altLtvPct) */
94
+ altMaxLoan: number;
95
+ /** Difference: altMaxLoan - loanAmount */
96
+ altLoanDelta: number;
97
+ /** Leverage gap: altMaxLoan - dscrMaxLoan (or altMaxLoan if no DSCR program) */
98
+ leverageGap: number;
99
+ }
42
100
  interface SolverResult {
43
101
  value: number;
44
102
  resultingDSCR: number;
45
103
  feasible: boolean;
46
104
  }
105
+ interface MaxDscrLoanResult {
106
+ maxLoan: number;
107
+ ltvPct: number;
108
+ binding: 'dscr' | 'ltv';
109
+ }
110
+ interface CreditInsight {
111
+ message: string;
112
+ type: 'boost' | 'info' | 'warning';
113
+ }
114
+ interface AltStrategyMessages {
115
+ subtitle: string;
116
+ planBNote: string;
117
+ planCNote: string;
118
+ }
47
119
 
48
120
  declare function clamp(value: number, min: number, max: number): number;
49
121
  declare function formatCurrency(value: number, decimals?: number): string;
122
+ declare function formatCompact(n: number): string;
50
123
  declare function calculateDSCR(input: DSCRInput): DSCRResult;
51
- /** Find max loan amount that achieves targetDSCR */
124
+ /**
125
+ * Full DSCR calculator matching lukeroasst.com.
126
+ * DSCR = rent / PITIA. Includes LTV matrix lookups,
127
+ * algebraic max-loan solver, alt qualifying comparison,
128
+ * and rent gap analysis.
129
+ */
130
+ declare function calculateDSCRFull(input: DSCRInputFull): DSCRResultFull;
131
+ /** Find max loan amount that achieves targetDSCR (v1 NOI-based) */
52
132
  declare function solveMaxLoan(input: Omit<DSCRInput, 'ltv'>, targetDSCR: number): SolverResult;
53
- /** Find minimum monthly rent to achieve targetDSCR */
133
+ /** Find minimum monthly rent to achieve targetDSCR (v1 NOI-based) */
54
134
  declare function solveMinRent(input: DSCRInput, targetDSCR: number): SolverResult;
55
135
 
56
- export { type DSCRInput, type DSCRResult, type SolverResult, calculateDSCR, clamp, formatCurrency, solveMaxLoan, solveMinRent };
136
+ /**
137
+ * Real lender LTV matrix: DSCR x FICO x Loan Amount x TxType -> Max LTV
138
+ *
139
+ * Sources: HomeXpress (verified S49), theLender (#theNONI), Acra Lending,
140
+ * ARC Homes (Edge), American Heritage, HomeBridge.
141
+ *
142
+ * HARD CAPS: 85% purchase, 85% R/T, 80% cash-out. No exceptions.
143
+ */
144
+ declare function getMaxLtv(dscr: number, fico: number, loanAmt: number, txType: TxType): number | null;
145
+ /**
146
+ * Alternative qualifying max LTV (Asset Depletion, Bank Statement, P&L).
147
+ * Hard caps: 85% purchase, 85% rate/term, 80% cash-out.
148
+ */
149
+ declare function getAltLtv(txType: TxType): number;
150
+
151
+ /**
152
+ * Algebraic solver: find max loan where DSCR = 1.0, capped by LTV x appraised.
153
+ *
154
+ * DSCR = rent / PITIA. PITIA = PI + taxes + insurance + HOA.
155
+ * PI = loan x paymentFactor.
156
+ * So: loan = (rent/targetDscr - fixedExpenses) / paymentFactor.
157
+ * Result is capped by LTV x appraisedValue (secondary constraint, rarely binding).
158
+ *
159
+ * @param monthlyRent - Gross monthly rental income
160
+ * @param interestRate - Annual rate as percentage (e.g. 7.5)
161
+ * @param loanTerm - Loan term in years
162
+ * @param annualTaxes - Annual property taxes
163
+ * @param annualInsurance - Annual hazard insurance
164
+ * @param monthlyHOA - Monthly HOA dues
165
+ * @param appraisedValue - As-is appraised value
166
+ * @param fico - Borrower FICO score
167
+ * @param txType - Transaction type
168
+ */
169
+ declare function getMaxDscrLoan(monthlyRent: number, interestRate: number, loanTerm: number, annualTaxes: number, annualInsurance: number, monthlyHOA: number, appraisedValue: number, fico: number, txType: TxType): MaxDscrLoanResult | null;
170
+
171
+ /**
172
+ * FICO-aware credit insight messaging.
173
+ * Returns a contextual message based on the borrower's score relative
174
+ * to the next LTV tier breakpoint.
175
+ */
176
+ declare function getCreditInsight(fico: number, dscr: number, loanAmt: number, txType: TxType, currentLtv: number | null, lang: Lang): CreditInsight | null;
177
+ /**
178
+ * FICO-aware alternative strategy messaging for Plan B (Asset Depletion)
179
+ * and Plan C (Bank Statement / P&L).
180
+ */
181
+ declare function getAltStrategyMessage(fico: number, _dscr: number, txType: TxType, lang: Lang): AltStrategyMessages;
182
+
183
+ export { type AltStrategyMessages, type CreditInsight, type DSCRInput, type DSCRInputFull, type DSCRResult, type DSCRResultFull, type Lang, type MaxDscrLoanResult, type SolverResult, type TxType, calculateDSCR, calculateDSCRFull, clamp, formatCompact, formatCurrency, getAltLtv, getAltStrategyMessage, getCreditInsight, getMaxDscrLoan, getMaxLtv, solveMaxLoan, solveMinRent };
package/dist/index.js CHANGED
@@ -21,13 +21,119 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  calculateDSCR: () => calculateDSCR,
24
+ calculateDSCRFull: () => calculateDSCRFull,
24
25
  clamp: () => clamp,
26
+ formatCompact: () => formatCompact,
25
27
  formatCurrency: () => formatCurrency,
28
+ getAltLtv: () => getAltLtv,
29
+ getAltStrategyMessage: () => getAltStrategyMessage,
30
+ getCreditInsight: () => getCreditInsight,
31
+ getMaxDscrLoan: () => getMaxDscrLoan,
32
+ getMaxLtv: () => getMaxLtv,
26
33
  solveMaxLoan: () => solveMaxLoan,
27
34
  solveMinRent: () => solveMinRent
28
35
  });
29
36
  module.exports = __toCommonJS(index_exports);
30
37
 
38
+ // src/dscr/ltv.ts
39
+ function getMaxLtv(dscr, fico, loanAmt, txType) {
40
+ const pick = (p, rt, co) => {
41
+ if (txType === "purchase") return p;
42
+ if (txType === "rateTermRefi") return rt;
43
+ return co;
44
+ };
45
+ if (dscr >= 1) {
46
+ if (fico >= 720) {
47
+ if (loanAmt <= 15e5) return pick(85, 85, 80);
48
+ if (loanAmt <= 2e6) return pick(75, 75, 70);
49
+ if (loanAmt <= 3e6) return pick(70, 70, 65);
50
+ if (loanAmt <= 35e5) return pick(65, 65, 60);
51
+ return pick(60, 60, 55);
52
+ }
53
+ if (fico >= 700) {
54
+ if (loanAmt <= 15e5) return pick(80, 80, 75);
55
+ if (loanAmt <= 2e6) return pick(75, 75, 70);
56
+ if (loanAmt <= 3e6) return pick(70, 70, 65);
57
+ return pick(65, 65, 60);
58
+ }
59
+ if (fico >= 680) {
60
+ if (loanAmt <= 15e5) return pick(75, 75, 75);
61
+ if (loanAmt <= 2e6) return pick(70, 70, 65);
62
+ if (loanAmt <= 3e6) return pick(65, 65, 60);
63
+ return null;
64
+ }
65
+ if (fico >= 660) {
66
+ if (loanAmt <= 15e5) return pick(70, 70, 70);
67
+ if (loanAmt <= 2e6) return pick(70, 70, 65);
68
+ if (loanAmt <= 3e6) return pick(65, 65, 60);
69
+ return null;
70
+ }
71
+ if (fico >= 640) {
72
+ if (loanAmt <= 15e5) return pick(70, 70, 70);
73
+ return null;
74
+ }
75
+ if (fico >= 620) {
76
+ if (loanAmt <= 15e5) return pick(70, 70, 70);
77
+ return null;
78
+ }
79
+ if (fico >= 600 && loanAmt <= 15e5) return pick(60, 60, null);
80
+ return null;
81
+ }
82
+ if (dscr >= 0.75) {
83
+ if (fico >= 720) {
84
+ if (loanAmt <= 15e5) return pick(75, 75, 70);
85
+ if (loanAmt <= 2e6) return pick(70, 70, 65);
86
+ return null;
87
+ }
88
+ if (fico >= 700) {
89
+ if (loanAmt <= 15e5) return pick(75, 70, 70);
90
+ return null;
91
+ }
92
+ if (fico >= 660) {
93
+ if (loanAmt <= 15e5) return pick(70, 70, 65);
94
+ return null;
95
+ }
96
+ if (fico >= 640) {
97
+ if (loanAmt <= 1e6) return pick(65, 65, 60);
98
+ return null;
99
+ }
100
+ return null;
101
+ }
102
+ if (fico >= 700) {
103
+ if (loanAmt <= 1e6) return pick(65, 65, 60);
104
+ if (loanAmt <= 15e5) return pick(60, 60, null);
105
+ return null;
106
+ }
107
+ if (fico >= 660) {
108
+ if (loanAmt <= 1e6) return pick(60, 60, null);
109
+ return null;
110
+ }
111
+ return null;
112
+ }
113
+ function getAltLtv(txType) {
114
+ if (txType === "purchase") return 85;
115
+ if (txType === "rateTermRefi") return 85;
116
+ return 80;
117
+ }
118
+
119
+ // src/dscr/solvers.ts
120
+ function getMaxDscrLoan(monthlyRent, interestRate, loanTerm, annualTaxes, annualInsurance, monthlyHOA, appraisedValue, fico, txType) {
121
+ const monthlyRate = interestRate / 100 / 12;
122
+ const n = loanTerm * 12;
123
+ const fixedExpenses = annualTaxes / 12 + annualInsurance / 12 + monthlyHOA;
124
+ const paymentFactor = monthlyRate > 0 ? monthlyRate * Math.pow(1 + monthlyRate, n) / (Math.pow(1 + monthlyRate, n) - 1) : 1 / n;
125
+ const targetPi = monthlyRent / 1 - fixedExpenses;
126
+ if (targetPi <= 0) return null;
127
+ const maxLoanAtDscr1 = Math.floor(targetPi / paymentFactor);
128
+ const ltvPct = getMaxLtv(1, fico, maxLoanAtDscr1, txType);
129
+ if (ltvPct === null) return null;
130
+ const ltvCap = appraisedValue > 0 ? Math.floor(appraisedValue * (ltvPct / 100)) : Infinity;
131
+ if (maxLoanAtDscr1 <= ltvCap) {
132
+ return { maxLoan: maxLoanAtDscr1, ltvPct, binding: "dscr" };
133
+ }
134
+ return { maxLoan: ltvCap, ltvPct, binding: "ltv" };
135
+ }
136
+
31
137
  // src/dscr/engine.ts
32
138
  function clamp(value, min, max) {
33
139
  return Math.max(min, Math.min(max, value));
@@ -40,6 +146,11 @@ function formatCurrency(value, decimals = 0) {
40
146
  maximumFractionDigits: decimals
41
147
  }).format(value);
42
148
  }
149
+ function formatCompact(n) {
150
+ if (n >= 1e6) return `$${(n / 1e6).toFixed(1)}M`;
151
+ if (n >= 1e3) return `$${Math.round(n / 1e3)}K`;
152
+ return formatCurrency(n);
153
+ }
43
154
  function calcMonthlyPayment(principal, annualRate, amortYears, interestOnly) {
44
155
  if (principal <= 0 || annualRate <= 0) return 0;
45
156
  const monthlyRate = annualRate / 12;
@@ -82,6 +193,68 @@ function calculateDSCR(input) {
82
193
  tier: getDSCRTier(dscr)
83
194
  };
84
195
  }
196
+ function calculateDSCRFull(input) {
197
+ const {
198
+ monthlyRent,
199
+ loanAmount,
200
+ interestRate,
201
+ loanTerm,
202
+ annualTaxes,
203
+ annualInsurance,
204
+ monthlyHOA,
205
+ appraisedValue,
206
+ creditScore,
207
+ transactionType
208
+ } = input;
209
+ const monthlyRate = interestRate / 100 / 12;
210
+ const n = loanTerm * 12;
211
+ const pi = monthlyRate > 0 ? loanAmount * (monthlyRate * Math.pow(1 + monthlyRate, n)) / (Math.pow(1 + monthlyRate, n) - 1) : loanAmount / n;
212
+ const taxes = annualTaxes / 12;
213
+ const insurance = annualInsurance / 12;
214
+ const pitia = pi + taxes + insurance + monthlyHOA;
215
+ const dscrRaw = pitia > 0 ? monthlyRent / pitia : 0;
216
+ const dscr = Math.round(dscrRaw * 100) / 100;
217
+ const cashFlow = monthlyRent - pitia;
218
+ const userLtvPct = getMaxLtv(dscr, creditScore, loanAmount, transactionType);
219
+ const solved = getMaxDscrLoan(
220
+ monthlyRent,
221
+ interestRate,
222
+ loanTerm,
223
+ annualTaxes,
224
+ annualInsurance,
225
+ monthlyHOA,
226
+ appraisedValue,
227
+ creditScore,
228
+ transactionType
229
+ );
230
+ const dscrLtvPct = solved ? solved.ltvPct : null;
231
+ const dscrMaxLoan = solved ? solved.maxLoan : null;
232
+ const dscrLoanDelta = dscrMaxLoan !== null ? dscrMaxLoan - loanAmount : null;
233
+ const rentForDscr1 = Math.ceil(pitia);
234
+ const rentGap = rentForDscr1 - monthlyRent;
235
+ const altLtvPct = getAltLtv(transactionType);
236
+ const altMaxLoan = Math.round(appraisedValue * (altLtvPct / 100));
237
+ const altLoanDelta = altMaxLoan - loanAmount;
238
+ const leverageGap = dscrMaxLoan !== null ? altMaxLoan - dscrMaxLoan : altMaxLoan;
239
+ return {
240
+ dscr,
241
+ pi,
242
+ taxes,
243
+ insurance,
244
+ pitia,
245
+ cashFlow,
246
+ userLtvPct,
247
+ dscrLtvPct,
248
+ dscrMaxLoan,
249
+ dscrLoanDelta,
250
+ rentForDscr1,
251
+ rentGap,
252
+ altLtvPct,
253
+ altMaxLoan,
254
+ altLoanDelta,
255
+ leverageGap
256
+ };
257
+ }
85
258
  function solveMaxLoan(input, targetDSCR) {
86
259
  const grossMonthly = input.monthlyRent;
87
260
  const totalExpenses = (input.vacancyAmount ?? 0) + (input.managementAmount ?? 0) + (input.monthlyInsurance ?? 0) + (input.monthlyTaxes ?? 0) + (input.monthlyHOA ?? 0) + (input.otherMonthlyExpenses ?? 0);
@@ -131,11 +304,96 @@ function solveMinRent(input, targetDSCR) {
131
304
  feasible: minRent > 0
132
305
  };
133
306
  }
307
+
308
+ // src/dscr/credit.ts
309
+ function getCreditInsight(fico, dscr, loanAmt, txType, currentLtv, lang) {
310
+ const es = lang === "es";
311
+ if (fico === 700) {
312
+ return {
313
+ message: es ? "700 es el valor predeterminado. Ingrese su puntaje real \u2014 720+ desbloquea hasta 85% LTV en compras." : "700 is the default. Enter your actual score \u2014 720+ unlocks up to 85% LTV on purchases.",
314
+ type: "info"
315
+ };
316
+ }
317
+ if (fico >= 710 && fico < 720 && dscr >= 1) {
318
+ const nextLtv = getMaxLtv(dscr, 720, loanAmt, txType);
319
+ if (nextLtv !== null && currentLtv !== null && nextLtv > currentLtv) {
320
+ return {
321
+ message: es ? `A solo ${720 - fico} puntos de 720 FICO \u2014 eso desbloquea ${nextLtv}% LTV (${nextLtv - currentLtv}% m\xE1s apalancamiento).` : `Just ${720 - fico} points from 720 FICO \u2014 that unlocks ${nextLtv}% LTV (${nextLtv - currentLtv}% more leverage).`,
322
+ type: "boost"
323
+ };
324
+ }
325
+ }
326
+ if (fico >= 690 && fico < 700 && dscr >= 1) {
327
+ const nextLtv = getMaxLtv(dscr, 700, loanAmt, txType);
328
+ if (nextLtv !== null && currentLtv !== null && nextLtv > currentLtv) {
329
+ return {
330
+ message: es ? `A solo ${700 - fico} puntos de 700 FICO \u2014 eso desbloquea ${nextLtv}% LTV.` : `Just ${700 - fico} points from 700 FICO \u2014 that unlocks ${nextLtv}% LTV.`,
331
+ type: "boost"
332
+ };
333
+ }
334
+ }
335
+ if (fico >= 760) {
336
+ return {
337
+ message: es ? "Puntaje \xE9lite \u2014 califica para el mejor pricing y m\xE1ximo apalancamiento en todos los programas." : "Elite score \u2014 qualifies for best pricing and maximum leverage across all programs.",
338
+ type: "boost"
339
+ };
340
+ }
341
+ if (fico < 640 && fico >= 620) {
342
+ return {
343
+ message: es ? "Opciones limitadas a 70% LTV max y montos menores. Considere estrategias alternativas de calificaci\xF3n." : "Limited to 70% LTV max and smaller loan amounts. Consider alternative qualifying strategies below.",
344
+ type: "warning"
345
+ };
346
+ }
347
+ if (fico < 620) {
348
+ return {
349
+ message: es ? "La mayor\xEDa de programas DSCR requieren 620+ FICO. Las estrategias alternativas pueden funcionar con puntajes m\xE1s bajos." : "Most DSCR programs require 620+ FICO. Alternative strategies below may work with lower scores.",
350
+ type: "warning"
351
+ };
352
+ }
353
+ return null;
354
+ }
355
+ function getAltStrategyMessage(fico, _dscr, txType, lang) {
356
+ const es = lang === "es";
357
+ const altLtv = getAltLtv(txType);
358
+ if (fico >= 720) {
359
+ return {
360
+ subtitle: es ? `Con ${fico} FICO, las estrategias alternativas desbloquean hasta ${altLtv}% LTV \u2014 su puntaje le da acceso a las mejores opciones:` : `With ${fico} FICO, alternative strategies unlock up to ${altLtv}% LTV \u2014 your score gives you access to the best options:`,
361
+ planBNote: es ? "Su cr\xE9dito de 720+ califica para el m\xE1ximo apalancamiento con agotamiento de activos." : "Your 720+ credit qualifies for maximum leverage with asset depletion.",
362
+ planCNote: es ? "Puntaje fuerte \u2014 bank statement y P&L programas disponibles con pricing competitivo." : "Strong score \u2014 bank statement and P&L programs available at competitive pricing."
363
+ };
364
+ }
365
+ if (fico >= 700) {
366
+ return {
367
+ subtitle: es ? `Con ${fico} FICO, todav\xEDa tiene acceso fuerte a estrategias alternativas hasta ${altLtv}% LTV:` : `With ${fico} FICO, you still have strong access to alternative strategies up to ${altLtv}% LTV:`,
368
+ planBNote: es ? "Buen cr\xE9dito \u2014 califica para agotamiento de activos con la mayor\xEDa de prestamistas." : "Good credit \u2014 qualifies for asset depletion with most lenders.",
369
+ planCNote: es ? "Bank statement y P&L programas ampliamente disponibles en este rango de puntaje." : "Bank statement and P&L programs widely available at this score range."
370
+ };
371
+ }
372
+ if (fico >= 660) {
373
+ return {
374
+ subtitle: es ? `Con ${fico} FICO, las estrategias alternativas a\xFAn funcionan pero con LTV reducido. Mejorar a 700+ ampl\xEDa significativamente sus opciones:` : `With ${fico} FICO, alternative strategies still work but at reduced LTV. Improving to 700+ significantly expands your options:`,
375
+ planBNote: es ? "Disponible \u2014 algunos prestamistas pueden requerir reservas adicionales." : "Available \u2014 some lenders may require additional reserves.",
376
+ planCNote: es ? "Bank statement disponible. P&L puede requerir 680+ con algunos prestamistas." : "Bank statement available. P&L may require 680+ with some lenders."
377
+ };
378
+ }
379
+ return {
380
+ subtitle: es ? `Con ${fico} FICO, las opciones son m\xE1s limitadas. Las estrategias alternativas pueden restaurar apalancamiento, pero considere mejorar su cr\xE9dito para mejores t\xE9rminos:` : `With ${fico} FICO, options are more limited. Alternative strategies can restore leverage, but consider improving credit for better terms:`,
381
+ planBNote: es ? "Disponibilidad limitada \u2014 consulte para opciones espec\xEDficas." : "Limited availability \u2014 consult for specific options.",
382
+ planCNote: es ? "Bank statement puede funcionar con reservas adicionales. P&L generalmente requiere 660+." : "Bank statement may work with additional reserves. P&L typically requires 660+."
383
+ };
384
+ }
134
385
  // Annotate the CommonJS export names for ESM import in node:
135
386
  0 && (module.exports = {
136
387
  calculateDSCR,
388
+ calculateDSCRFull,
137
389
  clamp,
390
+ formatCompact,
138
391
  formatCurrency,
392
+ getAltLtv,
393
+ getAltStrategyMessage,
394
+ getCreditInsight,
395
+ getMaxDscrLoan,
396
+ getMaxLtv,
139
397
  solveMaxLoan,
140
398
  solveMinRent
141
399
  });
package/dist/index.mjs CHANGED
@@ -1,3 +1,102 @@
1
+ // src/dscr/ltv.ts
2
+ function getMaxLtv(dscr, fico, loanAmt, txType) {
3
+ const pick = (p, rt, co) => {
4
+ if (txType === "purchase") return p;
5
+ if (txType === "rateTermRefi") return rt;
6
+ return co;
7
+ };
8
+ if (dscr >= 1) {
9
+ if (fico >= 720) {
10
+ if (loanAmt <= 15e5) return pick(85, 85, 80);
11
+ if (loanAmt <= 2e6) return pick(75, 75, 70);
12
+ if (loanAmt <= 3e6) return pick(70, 70, 65);
13
+ if (loanAmt <= 35e5) return pick(65, 65, 60);
14
+ return pick(60, 60, 55);
15
+ }
16
+ if (fico >= 700) {
17
+ if (loanAmt <= 15e5) return pick(80, 80, 75);
18
+ if (loanAmt <= 2e6) return pick(75, 75, 70);
19
+ if (loanAmt <= 3e6) return pick(70, 70, 65);
20
+ return pick(65, 65, 60);
21
+ }
22
+ if (fico >= 680) {
23
+ if (loanAmt <= 15e5) return pick(75, 75, 75);
24
+ if (loanAmt <= 2e6) return pick(70, 70, 65);
25
+ if (loanAmt <= 3e6) return pick(65, 65, 60);
26
+ return null;
27
+ }
28
+ if (fico >= 660) {
29
+ if (loanAmt <= 15e5) return pick(70, 70, 70);
30
+ if (loanAmt <= 2e6) return pick(70, 70, 65);
31
+ if (loanAmt <= 3e6) return pick(65, 65, 60);
32
+ return null;
33
+ }
34
+ if (fico >= 640) {
35
+ if (loanAmt <= 15e5) return pick(70, 70, 70);
36
+ return null;
37
+ }
38
+ if (fico >= 620) {
39
+ if (loanAmt <= 15e5) return pick(70, 70, 70);
40
+ return null;
41
+ }
42
+ if (fico >= 600 && loanAmt <= 15e5) return pick(60, 60, null);
43
+ return null;
44
+ }
45
+ if (dscr >= 0.75) {
46
+ if (fico >= 720) {
47
+ if (loanAmt <= 15e5) return pick(75, 75, 70);
48
+ if (loanAmt <= 2e6) return pick(70, 70, 65);
49
+ return null;
50
+ }
51
+ if (fico >= 700) {
52
+ if (loanAmt <= 15e5) return pick(75, 70, 70);
53
+ return null;
54
+ }
55
+ if (fico >= 660) {
56
+ if (loanAmt <= 15e5) return pick(70, 70, 65);
57
+ return null;
58
+ }
59
+ if (fico >= 640) {
60
+ if (loanAmt <= 1e6) return pick(65, 65, 60);
61
+ return null;
62
+ }
63
+ return null;
64
+ }
65
+ if (fico >= 700) {
66
+ if (loanAmt <= 1e6) return pick(65, 65, 60);
67
+ if (loanAmt <= 15e5) return pick(60, 60, null);
68
+ return null;
69
+ }
70
+ if (fico >= 660) {
71
+ if (loanAmt <= 1e6) return pick(60, 60, null);
72
+ return null;
73
+ }
74
+ return null;
75
+ }
76
+ function getAltLtv(txType) {
77
+ if (txType === "purchase") return 85;
78
+ if (txType === "rateTermRefi") return 85;
79
+ return 80;
80
+ }
81
+
82
+ // src/dscr/solvers.ts
83
+ function getMaxDscrLoan(monthlyRent, interestRate, loanTerm, annualTaxes, annualInsurance, monthlyHOA, appraisedValue, fico, txType) {
84
+ const monthlyRate = interestRate / 100 / 12;
85
+ const n = loanTerm * 12;
86
+ const fixedExpenses = annualTaxes / 12 + annualInsurance / 12 + monthlyHOA;
87
+ const paymentFactor = monthlyRate > 0 ? monthlyRate * Math.pow(1 + monthlyRate, n) / (Math.pow(1 + monthlyRate, n) - 1) : 1 / n;
88
+ const targetPi = monthlyRent / 1 - fixedExpenses;
89
+ if (targetPi <= 0) return null;
90
+ const maxLoanAtDscr1 = Math.floor(targetPi / paymentFactor);
91
+ const ltvPct = getMaxLtv(1, fico, maxLoanAtDscr1, txType);
92
+ if (ltvPct === null) return null;
93
+ const ltvCap = appraisedValue > 0 ? Math.floor(appraisedValue * (ltvPct / 100)) : Infinity;
94
+ if (maxLoanAtDscr1 <= ltvCap) {
95
+ return { maxLoan: maxLoanAtDscr1, ltvPct, binding: "dscr" };
96
+ }
97
+ return { maxLoan: ltvCap, ltvPct, binding: "ltv" };
98
+ }
99
+
1
100
  // src/dscr/engine.ts
2
101
  function clamp(value, min, max) {
3
102
  return Math.max(min, Math.min(max, value));
@@ -10,6 +109,11 @@ function formatCurrency(value, decimals = 0) {
10
109
  maximumFractionDigits: decimals
11
110
  }).format(value);
12
111
  }
112
+ function formatCompact(n) {
113
+ if (n >= 1e6) return `$${(n / 1e6).toFixed(1)}M`;
114
+ if (n >= 1e3) return `$${Math.round(n / 1e3)}K`;
115
+ return formatCurrency(n);
116
+ }
13
117
  function calcMonthlyPayment(principal, annualRate, amortYears, interestOnly) {
14
118
  if (principal <= 0 || annualRate <= 0) return 0;
15
119
  const monthlyRate = annualRate / 12;
@@ -52,6 +156,68 @@ function calculateDSCR(input) {
52
156
  tier: getDSCRTier(dscr)
53
157
  };
54
158
  }
159
+ function calculateDSCRFull(input) {
160
+ const {
161
+ monthlyRent,
162
+ loanAmount,
163
+ interestRate,
164
+ loanTerm,
165
+ annualTaxes,
166
+ annualInsurance,
167
+ monthlyHOA,
168
+ appraisedValue,
169
+ creditScore,
170
+ transactionType
171
+ } = input;
172
+ const monthlyRate = interestRate / 100 / 12;
173
+ const n = loanTerm * 12;
174
+ const pi = monthlyRate > 0 ? loanAmount * (monthlyRate * Math.pow(1 + monthlyRate, n)) / (Math.pow(1 + monthlyRate, n) - 1) : loanAmount / n;
175
+ const taxes = annualTaxes / 12;
176
+ const insurance = annualInsurance / 12;
177
+ const pitia = pi + taxes + insurance + monthlyHOA;
178
+ const dscrRaw = pitia > 0 ? monthlyRent / pitia : 0;
179
+ const dscr = Math.round(dscrRaw * 100) / 100;
180
+ const cashFlow = monthlyRent - pitia;
181
+ const userLtvPct = getMaxLtv(dscr, creditScore, loanAmount, transactionType);
182
+ const solved = getMaxDscrLoan(
183
+ monthlyRent,
184
+ interestRate,
185
+ loanTerm,
186
+ annualTaxes,
187
+ annualInsurance,
188
+ monthlyHOA,
189
+ appraisedValue,
190
+ creditScore,
191
+ transactionType
192
+ );
193
+ const dscrLtvPct = solved ? solved.ltvPct : null;
194
+ const dscrMaxLoan = solved ? solved.maxLoan : null;
195
+ const dscrLoanDelta = dscrMaxLoan !== null ? dscrMaxLoan - loanAmount : null;
196
+ const rentForDscr1 = Math.ceil(pitia);
197
+ const rentGap = rentForDscr1 - monthlyRent;
198
+ const altLtvPct = getAltLtv(transactionType);
199
+ const altMaxLoan = Math.round(appraisedValue * (altLtvPct / 100));
200
+ const altLoanDelta = altMaxLoan - loanAmount;
201
+ const leverageGap = dscrMaxLoan !== null ? altMaxLoan - dscrMaxLoan : altMaxLoan;
202
+ return {
203
+ dscr,
204
+ pi,
205
+ taxes,
206
+ insurance,
207
+ pitia,
208
+ cashFlow,
209
+ userLtvPct,
210
+ dscrLtvPct,
211
+ dscrMaxLoan,
212
+ dscrLoanDelta,
213
+ rentForDscr1,
214
+ rentGap,
215
+ altLtvPct,
216
+ altMaxLoan,
217
+ altLoanDelta,
218
+ leverageGap
219
+ };
220
+ }
55
221
  function solveMaxLoan(input, targetDSCR) {
56
222
  const grossMonthly = input.monthlyRent;
57
223
  const totalExpenses = (input.vacancyAmount ?? 0) + (input.managementAmount ?? 0) + (input.monthlyInsurance ?? 0) + (input.monthlyTaxes ?? 0) + (input.monthlyHOA ?? 0) + (input.otherMonthlyExpenses ?? 0);
@@ -101,10 +267,95 @@ function solveMinRent(input, targetDSCR) {
101
267
  feasible: minRent > 0
102
268
  };
103
269
  }
270
+
271
+ // src/dscr/credit.ts
272
+ function getCreditInsight(fico, dscr, loanAmt, txType, currentLtv, lang) {
273
+ const es = lang === "es";
274
+ if (fico === 700) {
275
+ return {
276
+ message: es ? "700 es el valor predeterminado. Ingrese su puntaje real \u2014 720+ desbloquea hasta 85% LTV en compras." : "700 is the default. Enter your actual score \u2014 720+ unlocks up to 85% LTV on purchases.",
277
+ type: "info"
278
+ };
279
+ }
280
+ if (fico >= 710 && fico < 720 && dscr >= 1) {
281
+ const nextLtv = getMaxLtv(dscr, 720, loanAmt, txType);
282
+ if (nextLtv !== null && currentLtv !== null && nextLtv > currentLtv) {
283
+ return {
284
+ message: es ? `A solo ${720 - fico} puntos de 720 FICO \u2014 eso desbloquea ${nextLtv}% LTV (${nextLtv - currentLtv}% m\xE1s apalancamiento).` : `Just ${720 - fico} points from 720 FICO \u2014 that unlocks ${nextLtv}% LTV (${nextLtv - currentLtv}% more leverage).`,
285
+ type: "boost"
286
+ };
287
+ }
288
+ }
289
+ if (fico >= 690 && fico < 700 && dscr >= 1) {
290
+ const nextLtv = getMaxLtv(dscr, 700, loanAmt, txType);
291
+ if (nextLtv !== null && currentLtv !== null && nextLtv > currentLtv) {
292
+ return {
293
+ message: es ? `A solo ${700 - fico} puntos de 700 FICO \u2014 eso desbloquea ${nextLtv}% LTV.` : `Just ${700 - fico} points from 700 FICO \u2014 that unlocks ${nextLtv}% LTV.`,
294
+ type: "boost"
295
+ };
296
+ }
297
+ }
298
+ if (fico >= 760) {
299
+ return {
300
+ message: es ? "Puntaje \xE9lite \u2014 califica para el mejor pricing y m\xE1ximo apalancamiento en todos los programas." : "Elite score \u2014 qualifies for best pricing and maximum leverage across all programs.",
301
+ type: "boost"
302
+ };
303
+ }
304
+ if (fico < 640 && fico >= 620) {
305
+ return {
306
+ message: es ? "Opciones limitadas a 70% LTV max y montos menores. Considere estrategias alternativas de calificaci\xF3n." : "Limited to 70% LTV max and smaller loan amounts. Consider alternative qualifying strategies below.",
307
+ type: "warning"
308
+ };
309
+ }
310
+ if (fico < 620) {
311
+ return {
312
+ message: es ? "La mayor\xEDa de programas DSCR requieren 620+ FICO. Las estrategias alternativas pueden funcionar con puntajes m\xE1s bajos." : "Most DSCR programs require 620+ FICO. Alternative strategies below may work with lower scores.",
313
+ type: "warning"
314
+ };
315
+ }
316
+ return null;
317
+ }
318
+ function getAltStrategyMessage(fico, _dscr, txType, lang) {
319
+ const es = lang === "es";
320
+ const altLtv = getAltLtv(txType);
321
+ if (fico >= 720) {
322
+ return {
323
+ subtitle: es ? `Con ${fico} FICO, las estrategias alternativas desbloquean hasta ${altLtv}% LTV \u2014 su puntaje le da acceso a las mejores opciones:` : `With ${fico} FICO, alternative strategies unlock up to ${altLtv}% LTV \u2014 your score gives you access to the best options:`,
324
+ planBNote: es ? "Su cr\xE9dito de 720+ califica para el m\xE1ximo apalancamiento con agotamiento de activos." : "Your 720+ credit qualifies for maximum leverage with asset depletion.",
325
+ planCNote: es ? "Puntaje fuerte \u2014 bank statement y P&L programas disponibles con pricing competitivo." : "Strong score \u2014 bank statement and P&L programs available at competitive pricing."
326
+ };
327
+ }
328
+ if (fico >= 700) {
329
+ return {
330
+ subtitle: es ? `Con ${fico} FICO, todav\xEDa tiene acceso fuerte a estrategias alternativas hasta ${altLtv}% LTV:` : `With ${fico} FICO, you still have strong access to alternative strategies up to ${altLtv}% LTV:`,
331
+ planBNote: es ? "Buen cr\xE9dito \u2014 califica para agotamiento de activos con la mayor\xEDa de prestamistas." : "Good credit \u2014 qualifies for asset depletion with most lenders.",
332
+ planCNote: es ? "Bank statement y P&L programas ampliamente disponibles en este rango de puntaje." : "Bank statement and P&L programs widely available at this score range."
333
+ };
334
+ }
335
+ if (fico >= 660) {
336
+ return {
337
+ subtitle: es ? `Con ${fico} FICO, las estrategias alternativas a\xFAn funcionan pero con LTV reducido. Mejorar a 700+ ampl\xEDa significativamente sus opciones:` : `With ${fico} FICO, alternative strategies still work but at reduced LTV. Improving to 700+ significantly expands your options:`,
338
+ planBNote: es ? "Disponible \u2014 algunos prestamistas pueden requerir reservas adicionales." : "Available \u2014 some lenders may require additional reserves.",
339
+ planCNote: es ? "Bank statement disponible. P&L puede requerir 680+ con algunos prestamistas." : "Bank statement available. P&L may require 680+ with some lenders."
340
+ };
341
+ }
342
+ return {
343
+ subtitle: es ? `Con ${fico} FICO, las opciones son m\xE1s limitadas. Las estrategias alternativas pueden restaurar apalancamiento, pero considere mejorar su cr\xE9dito para mejores t\xE9rminos:` : `With ${fico} FICO, options are more limited. Alternative strategies can restore leverage, but consider improving credit for better terms:`,
344
+ planBNote: es ? "Disponibilidad limitada \u2014 consulte para opciones espec\xEDficas." : "Limited availability \u2014 consult for specific options.",
345
+ planCNote: es ? "Bank statement puede funcionar con reservas adicionales. P&L generalmente requiere 660+." : "Bank statement may work with additional reserves. P&L typically requires 660+."
346
+ };
347
+ }
104
348
  export {
105
349
  calculateDSCR,
350
+ calculateDSCRFull,
106
351
  clamp,
352
+ formatCompact,
107
353
  formatCurrency,
354
+ getAltLtv,
355
+ getAltStrategyMessage,
356
+ getCreditInsight,
357
+ getMaxDscrLoan,
358
+ getMaxLtv,
108
359
  solveMaxLoan,
109
360
  solveMinRent
110
361
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wirms/calculator-engine",
3
- "version": "1.0.1",
4
- "description": "Pure TypeScript DSCR and investor calculator engine — zero dependencies",
3
+ "version": "2.0.0",
4
+ "description": "Pure TypeScript DSCR and investor calculator engine — full lender LTV matrix, algebraic solvers, credit insight engine, bilingual. Zero dependencies.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -26,6 +26,7 @@
26
26
  "url": "https://github.com/roasst/investor-tools.git",
27
27
  "directory": "packages/calculator-engine"
28
28
  },
29
+ "keywords": ["dscr", "calculator", "mortgage", "real-estate", "investor", "ltv", "loan"],
29
30
  "author": "WIRMS <luke@roasst.com>",
30
31
  "license": "UNLICENSED"
31
32
  }