Functional Weave
Code in TypeScript

insurance.instalment-plan@1.0.0

impl/rust.rs

4,189 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_apr::{apr, apr_result_to_value, CreditFlow};  ← from lending.apr ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate;  ← from money.apply-rate ^1.0.0 · built alongside by fune

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