Functional Weave
Code in Python

lending.overpayment-effect@1.0.1

impl/rust.rs

4,905 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_amortisation_schedule::{amortisation_schedule, period_interest};  ← from lending.amortisation-schedule ^1.0.0 · built alongside by fune
use super::lending_loan_payment::payment_count;  ← from lending.loan-payment ^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

/// 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(),
    ))
}