Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .lending_affordability_types import AffordabilityResult
from .lending_loan_payment import loan_payment ← from lending.loan-payment ^1.0.0 · built alongside by fune
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
from .money_apply_rate import apply_rate ← from money.apply-rate ^1.0.0 · built alongside by fune
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),
)