Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::lending_loan_payment::loan_payment; ← from lending.loan-payment ^1.0.0 · built alongside by fune
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate; ← from money.apply-rate ^1.0.0 · built alongside by fune
/// 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.
///
/// # Panics
/// Panics on mismatched currencies, a non-positive income, negative
/// commitments, a stress rate below the actual rate, a ceiling out of range,
/// or the loan arguments lending.loan-payment refuses.
pub fn affordability(
monthly_income: &Money,
monthly_commitments: &Money,
principal: &Money,
annual_rate_basis_points: i64,
stress_rate_basis_points: i64,
term_months: i64,
max_debt_to_income_basis_points: i64,
) -> AffordabilityResult {
let currency = monthly_income.currency.as_str();
for other in [monthly_commitments, principal] {
if other.currency != currency {
panic!("currency mismatch: {} and {}", currency, other.currency);
}
}
if monthly_income.minor <= 0 {
panic!("monthlyIncome must be greater than zero, received {}", monthly_income.minor);
}
if monthly_commitments.minor < 0 {
panic!("monthlyCommitments must not be negative, received {}", monthly_commitments.minor);
}
if stress_rate_basis_points < annual_rate_basis_points {
panic!(
"stressRateBasisPoints must not be below the actual rate, received {}",
stress_rate_basis_points
);
}
let m = max_debt_to_income_basis_points;
if m < 1 || m > 10000 {
panic!("maxDebtToIncomeBasisPoints must be between 1 and 10000, received {}", m);
}
let payment = loan_payment(principal, annual_rate_basis_points, term_months, 12, "up");
let stressed = loan_payment(principal, stress_rate_basis_points, term_months, 12, "up");
let ratio = |repayment: i64| round_div((monthly_commitments.minor + repayment) * 10000, monthly_income.minor, "up");
let stressed_ratio = ratio(stressed.minor);
let allowance = apply_rate(monthly_income, m, "down");
AffordabilityResult {
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),
payment,
stressed_payment: stressed,
}
}
pub fn affordability_result_to_value(result: &AffordabilityResult) -> Value {
Value::obj(vec![
("payment", money_to_value(&result.payment)),
("stressedPayment", money_to_value(&result.stressed_payment)),
("debtToIncomeBasisPoints", Value::Int(result.debt_to_income_basis_points)),
("stressedDebtToIncomeBasisPoints", Value::Int(result.stressed_debt_to_income_basis_points)),
("affordable", Value::Bool(result.affordable)),
("headroom", money_to_value(&result.headroom)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
affordability_result_to_value(&affordability(
&money_from_value(&args[0]),
&money_from_value(&args[1]),
&money_from_value(&args[2]),
args[3].as_i64(),
args[4].as_i64(),
args[5].as_i64(),
args[6].as_i64(),
))
}