use super::funejson::Value; use super::lending_loan_payment::{loan_payment, payment_count}; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; /// Interest for one period on a positive balance: balance × b / D, rounded /// half-up to the minor unit, which is what gets posted to the account. pub fn period_interest(balance: i64, annual_rate_basis_points: i64, payments_per_year: i64) -> i64 { if balance <= 0 || annual_rate_basis_points == 0 { return 0; } let product = balance as i128 * annual_rate_basis_points as i128; if product > i64::MAX as i128 { panic!("balance too large for exact interest"); } round_div(product as i64, 10000 * payments_per_year, "half-up") } /// The schedule a lender's system produces: each period's interest is /// computed on the opening balance and rounded to a whole minor unit, the /// level payment repays interest first and principal with the rest, and the /// last payment is whatever clears the balance, so it ends at exactly zero. /// /// # Panics /// Panics on the same arguments lending.loan-payment refuses. pub fn amortisation_schedule( principal: &Money, annual_rate_basis_points: i64, term_months: i64, payments_per_year: i64, mode: &str, ) -> AmortisationSchedule { let payment = loan_payment(principal, annual_rate_basis_points, term_months, payments_per_year, mode); let n = payment_count(annual_rate_basis_points, term_months, payments_per_year); let currency = principal.currency.as_str(); let mut rows: Vec = Vec::new(); let mut balance = principal.minor; let mut total_interest = 0i64; let mut total_paid = 0i64; let mut period = 1i64; while period <= n && balance > 0 { let interest = period_interest(balance, annual_rate_basis_points, payments_per_year); // The last period, or one where the level payment would overshoot (a // rounded-up payment can clear the loan a period early), pays exactly // what is owed. let due = if period == n || balance + interest <= payment.minor { balance + interest } else { payment.minor }; let repaid = due - interest; balance -= repaid; total_interest += interest; total_paid += due; rows.push(AmortisationRow { period, payment: money(due, currency), interest: money(interest, currency), principal: money(repaid, currency), balance: money(balance, currency), }); period += 1; } AmortisationSchedule { payment, rows, total_interest: money(total_interest, currency), total_paid: money(total_paid, currency), } } pub fn amortisation_row_to_value(row: &AmortisationRow) -> Value { Value::obj(vec![ ("period", Value::Int(row.period)), ("payment", money_to_value(&row.payment)), ("interest", money_to_value(&row.interest)), ("principal", money_to_value(&row.principal)), ("balance", money_to_value(&row.balance)), ]) } pub fn amortisation_schedule_to_value(schedule: &AmortisationSchedule) -> Value { Value::obj(vec![ ("payment", money_to_value(&schedule.payment)), ("rows", Value::Arr(schedule.rows.iter().map(amortisation_row_to_value).collect())), ("totalInterest", money_to_value(&schedule.total_interest)), ("totalPaid", money_to_value(&schedule.total_paid)), ]) } pub fn fune_vector(args: &[Value]) -> Value { amortisation_schedule_to_value(&amortisation_schedule( &money_from_value(&args[0]), args[1].as_i64(), args[2].as_i64(), args[3].as_i64(), args[4].as_str(), )) }