Functional Weave
Code in Rust

property.mortgage-affordability@1.0.1

impl/rust.rs

3,378 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_affordability::{affordability, affordability_result_to_value};  ← from lending.affordability ^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

const MAX_INCOME: i64 = 10_000_000_000;
const MAX_MULTIPLE: i64 = 200_000;

/// Loan-to-income against the lender's multiple, and the stressed repayment
/// against income via lending.affordability. Every rounding goes against the
/// borrower, and the multiple comparison itself is exact.
///
/// # Panics
/// Panics on mismatched currencies, an income, loan or multiple out of range,
/// or anything lending.affordability refuses.
#[allow(clippy::too_many_arguments)]
pub fn mortgage_affordability(
    annual_income: &Money,
    monthly_commitments: &Money,
    loan: &Money,
    annual_rate_basis_points: i64,
    stress_rate_basis_points: i64,
    term_months: i64,
    max_income_multiple_basis_points: i64,
    max_debt_to_income_basis_points: i64,
) -> MortgageAffordability {
    if loan.currency != annual_income.currency {
        panic!("currency mismatch: {} and {}", annual_income.currency, loan.currency);
    }
    if annual_income.minor < 12 || annual_income.minor > MAX_INCOME {
        panic!("annualIncome must be between 12 and {} minor units, received {}", MAX_INCOME, annual_income.minor);
    }
    if loan.minor <= 0 || loan.minor > MAX_INCOME * 20 {
        panic!("loan must be between 1 and {} minor units, received {}", MAX_INCOME * 20, loan.minor);
    }
    let m = max_income_multiple_basis_points;
    if m < 1 || m > MAX_MULTIPLE {
        panic!("maxIncomeMultipleBasisPoints must be between 1 and {}, received {}", MAX_MULTIPLE, m);
    }
    let currency = annual_income.currency.as_str();
    let monthly_income = money(annual_income.minor / 12, currency);
    let repayment = affordability(
        &monthly_income,
        monthly_commitments,
        loan,
        annual_rate_basis_points,
        stress_rate_basis_points,
        term_months,
        max_debt_to_income_basis_points,
    );
    let within = loan.minor * 10000 <= annual_income.minor * m;
    MortgageAffordability {
        income_multiple_basis_points: round_div(loan.minor * 10000, annual_income.minor, "up"),
        max_loan_by_multiple: money(round_div(annual_income.minor * m, 10000, "down"), currency),
        within_multiple: within,
        monthly_income,
        affordable: within && repayment.affordable,
        repayment,
    }
}

pub fn mortgage_affordability_to_value(result: &MortgageAffordability) -> Value {
    Value::obj(vec![
        ("incomeMultipleBasisPoints", Value::Int(result.income_multiple_basis_points)),
        ("maxLoanByMultiple", money_to_value(&result.max_loan_by_multiple)),
        ("withinMultiple", Value::Bool(result.within_multiple)),
        ("monthlyIncome", money_to_value(&result.monthly_income)),
        ("repayment", affordability_result_to_value(&result.repayment)),
        ("affordable", Value::Bool(result.affordable)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    mortgage_affordability_to_value(&mortgage_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(),
        args[7].as_i64(),
    ))
}