Functional Weave
Code in Python

lending.affordability@1.0.1

impl/rust.rs

3,736 bytes · the Rust implementation · view raw

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(),
    ))
}