from .lending_affordability_types import AffordabilityResult from .lending_loan_payment import loan_payment from .math_round_div import round_div from .money_amount import Money, money from .money_apply_rate import apply_rate def affordability( monthly_income: Money, monthly_commitments: Money, principal: Money, annual_rate_basis_points: int, stress_rate_basis_points: int, term_months: int, max_debt_to_income_basis_points: int, ) -> AffordabilityResult: """A proposed loan's monthly repayment as a share of income, at the rate it will be charged and at a stress rate, and whether the stressed share fits under the lender's ceiling. Everything is rounded against the borrower (payments and ratios up, the allowance down), because an affordability test that passes on a rounding is not a test. """ currency = monthly_income.currency for other in (monthly_commitments, principal): if other.currency != currency: raise ValueError("currency mismatch: %s and %s" % (currency, other.currency)) if monthly_income.minor <= 0: raise ValueError("monthlyIncome must be greater than zero, received %r" % (monthly_income.minor,)) if monthly_commitments.minor < 0: raise ValueError("monthlyCommitments must not be negative, received %r" % (monthly_commitments.minor,)) if ( isinstance(stress_rate_basis_points, bool) or not isinstance(stress_rate_basis_points, int) or stress_rate_basis_points < annual_rate_basis_points ): raise ValueError( "stressRateBasisPoints must not be below the actual rate, received %r" % (stress_rate_basis_points,) ) m = max_debt_to_income_basis_points if isinstance(m, bool) or not isinstance(m, int) or m < 1 or m > 10000: raise ValueError("maxDebtToIncomeBasisPoints must be between 1 and 10000, received %r" % (m,)) payment = loan_payment(principal, annual_rate_basis_points, term_months, 12, "up") stressed = loan_payment(principal, stress_rate_basis_points, term_months, 12, "up") def ratio(repayment: int) -> int: return round_div((monthly_commitments.minor + repayment) * 10000, monthly_income.minor, "up") stressed_ratio = ratio(stressed.minor) allowance = apply_rate(monthly_income, m, "down") return AffordabilityResult( payment=payment, stressed_payment=stressed, debt_to_income_basis_points=ratio(payment.minor), stressed_debt_to_income_basis_points=stressed_ratio, # The ratio is rounded up and the ceiling is whole, so this is the # exact comparison (commitments + payment) / income <= ceiling. affordable=stressed_ratio <= m, headroom=money(allowance.minor - monthly_commitments.minor - stressed.minor, currency), )