use super::funejson::Value; use super::lending_amortisation_schedule::{amortisation_schedule, period_interest}; use super::lending_loan_payment::payment_count; use super::money_amount::{money, money_from_value, money_to_value, Money}; /// Run a balance down at a fixed payment, with the schedule's rounding, until /// it is clear or the term ends: (payments, final payment, interest). As in /// lending.amortisation-schedule, the last payment of the term is whatever /// clears the balance. fn pay_off( balance: i64, annual_rate_basis_points: i64, payments_per_year: i64, payment: i64, term: i64, ) -> (i64, i64, i64) { let mut owed = balance; let mut payments = 0i64; let mut interest = 0i64; let mut final_payment = 0i64; while owed > 0 { let accrued = period_interest(owed, annual_rate_basis_points, payments_per_year); if payment <= accrued { panic!("the payment of {} does not cover the interest of {}", payment, accrued); } let due = if payments + 1 == term || owed + accrued <= payment { owed + accrued } else { payment }; owed -= due - accrued; interest += accrued; payments += 1; final_payment = due; } (payments, final_payment, interest) } /// The two things a lender offers after a lump-sum overpayment, side by side: /// keep paying the same amount and finish sooner, or keep the same end date /// and pay less each period. Both are built period by period with the same /// rounding as lending.amortisation-schedule, and compared with carrying on /// as if nothing had been overpaid. /// /// # Panics /// Panics on mismatched currencies, an overpayment of zero or of the whole /// balance, a payment that does not cover the interest, or the arguments /// lending.loan-payment refuses. pub fn overpayment_effect( balance: &Money, annual_rate_basis_points: i64, remaining_term_months: i64, payments_per_year: i64, payment: &Money, overpayment: &Money, mode: &str, ) -> OverpaymentEffect { let term = payment_count(annual_rate_basis_points, remaining_term_months, payments_per_year); let currency = balance.currency.as_str(); for other in [payment, overpayment] { if other.currency != currency { panic!("currency mismatch: {} and {}", currency, other.currency); } } if payment.minor <= 0 { panic!("payment must be greater than zero, received {}", payment.minor); } if overpayment.minor <= 0 { panic!("overpayment must be greater than zero, received {}", overpayment.minor); } if overpayment.minor >= balance.minor { panic!("overpayment must be less than the balance; paying it all off is an early settlement"); } let new_balance = balance.minor - overpayment.minor; let (base_n, _, base_interest) = pay_off(balance.minor, annual_rate_basis_points, payments_per_year, payment.minor, term); let (short_n, short_final, short_interest) = pay_off(new_balance, annual_rate_basis_points, payments_per_year, payment.minor, term); let lower = amortisation_schedule( &money(new_balance, currency), annual_rate_basis_points, remaining_term_months, payments_per_year, mode, ); OverpaymentEffect { new_balance: money(new_balance, currency), baseline_payments: base_n, baseline_interest: money(base_interest, currency), reduced_term_payments: short_n, reduced_term_final_payment: money(short_final, currency), reduced_term_interest: money(short_interest, currency), payments_saved: base_n - short_n, reduced_payment: lower.payment, reduced_payment_interest: lower.total_interest, } } pub fn overpayment_effect_to_value(effect: &OverpaymentEffect) -> Value { Value::obj(vec![ ("newBalance", money_to_value(&effect.new_balance)), ("baselinePayments", Value::Int(effect.baseline_payments)), ("baselineInterest", money_to_value(&effect.baseline_interest)), ("reducedTermPayments", Value::Int(effect.reduced_term_payments)), ("reducedTermFinalPayment", money_to_value(&effect.reduced_term_final_payment)), ("reducedTermInterest", money_to_value(&effect.reduced_term_interest)), ("paymentsSaved", Value::Int(effect.payments_saved)), ("reducedPayment", money_to_value(&effect.reduced_payment)), ("reducedPaymentInterest", money_to_value(&effect.reduced_payment_interest)), ]) } pub fn fune_vector(args: &[Value]) -> Value { overpayment_effect_to_value(&overpayment_effect( &money_from_value(&args[0]), args[1].as_i64(), args[2].as_i64(), args[3].as_i64(), &money_from_value(&args[4]), &money_from_value(&args[5]), args[6].as_str(), )) }