@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 +130 -3
- package/dist/index.d.ts +130 -3
- package/dist/index.js +258 -0
- package/dist/index.mjs +251 -0
- package/package.json +3 -2
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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": "
|
|
4
|
-
"description": "Pure TypeScript DSCR and investor calculator engine —
|
|
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
|
}
|