use super::funejson::Value; use super::lending_loan_payment::loan_payment; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; /// 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(), )) }