use super::funejson::Value; use super::lending_apr::{apr, apr_result_to_value, CreditFlow}; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; /// A premium paid by monthly instalments. /// /// The credit charge is a flat percentage of the amount financed (premium /// less deposit), the way UK insurers quote it. The instalments are the /// amount financed plus the charge, split so every penny is accounted for: /// equal instalments rounded down, with the odd pennies on the first. The APR /// is then worked from those exact cash flows by lending.apr, because a flat /// 10% repaid over ten months costs far more than 10% a year. /// /// # Panics /// Panics on mixed currencies, a deposit that is negative or covers the whole /// premium, bad counts or months, or instalments smaller than a minor unit. pub fn instalment_plan( premium: &Money, deposit: &Money, instalment_count: i64, credit_charge_basis_points: i64, first_instalment_month: i64, ) -> InstalmentPlan { assert_same_currency(premium, deposit); if instalment_count < 1 { panic!("instalmentCount must be a whole number of 1 or more, received {}", instalment_count); } if credit_charge_basis_points < 0 { panic!( "creditChargeBasisPoints must be a whole number of 0 or more, received {}", credit_charge_basis_points ); } if first_instalment_month < 0 { panic!("firstInstalmentMonth must be a whole number of 0 or more, received {}", first_instalment_month); } if deposit.minor < 0 { panic!("deposit must not be negative, received {}", deposit.minor); } if deposit.minor >= premium.minor { panic!("the deposit leaves nothing to pay by instalments"); } if instalment_count == 1 && first_instalment_month == 0 { panic!("a single instalment at inception is payment in full, not credit"); } let c = &premium.currency; let financed = money(premium.minor - deposit.minor, c); let charge = apply_rate(&financed, credit_charge_basis_points, "half-up"); let repayable = financed.minor + charge.minor; let each = repayable / instalment_count; if each < 1 { panic!("each instalment must be at least one minor unit"); } let first = repayable - each * (instalment_count - 1); let mut instalments = Vec::new(); let mut repayments = Vec::new(); for i in 0..instalment_count { let amount = money(if i == 0 { first } else { each }, c); repayments.push(CreditFlow { period: first_instalment_month + i, amount: amount.clone(), }); instalments.push(amount); } let rate = apr( &[CreditFlow { period: 0, amount: financed.clone(), }], &repayments, 12, ); InstalmentPlan { deposit: deposit.clone(), amount_financed: financed, credit_charge: charge, instalments, total_payable: money(deposit.minor + repayable, c), apr: rate, } } pub fn instalment_plan_to_value(p: &InstalmentPlan) -> Value { Value::obj(vec![ ("deposit", money_to_value(&p.deposit)), ("amountFinanced", money_to_value(&p.amount_financed)), ("creditCharge", money_to_value(&p.credit_charge)), ("instalments", Value::Arr(p.instalments.iter().map(money_to_value).collect())), ("totalPayable", money_to_value(&p.total_payable)), ("apr", apr_result_to_value(&p.apr)), ]) } fn int_arg(v: &Value, message: &str) -> i64 { if let Value::Float(f) = v { panic!("{}, received {}", message, f); } v.as_i64() } pub fn fune_vector(args: &[Value]) -> Value { instalment_plan_to_value(&instalment_plan( &money_from_value(&args[0]), &money_from_value(&args[1]), int_arg(&args[2], "instalmentCount must be a whole number of 1 or more"), int_arg(&args[3], "creditChargeBasisPoints must be a whole number of 0 or more"), int_arg(&args[4], "firstInstalmentMonth must be a whole number of 0 or more"), )) }