Functional Weave
Code in Rust

lending.early-settlement@1.0.1

impl/rust.rs

5,948 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::dates_add_days::add_days;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_add_months::add_months;  ← from dates.add-months ^1.0.0 · built alongside by fune
use super::dates_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune
use super::math_big_integer::BigInt;  ← from math.big-integer ^1.0.0 · built alongside by fune
use super::math_fractional_power::{fixed_scale, pow_fixed, root_fixed};  ← from math.fractional-power ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

/// The settlement date: 28 days after the creditor receives the notice
/// (regulation 5(1)(a)), or a later date the notice names, then deferred by a
/// month or 30 days where the creditor elects and regulation 6 allows it
/// (credit repaid over more than a year from the first drawdown).
fn settlement_date(
    advances: &[DatedAmount],
    repayments: &[DatedAmount],
    notice_received_iso: &str,
    requested_settlement_iso: Option<&str>,
    deferral: &str,
) -> String {
    let mut date = add_days(notice_received_iso, 28);
    if let Some(requested) = requested_settlement_iso {
        days_between(requested, requested);
        if requested > date.as_str() {
            date = requested.to_string();
        }
    }
    if deferral == "none" {
        return date;
    }
    if deferral != "one-month" && deferral != "thirty-days" {
        panic!("unknown deferral \"{}\": expected none, one-month or thirty-days", deferral);
    }
    let first = advances.iter().map(|a| a.date.as_str()).min().unwrap();
    let last = repayments.iter().map(|r| r.date.as_str()).max().unwrap();
    if last <= add_months(first, 12).as_str() {
        panic!("deferral is only allowed when the credit is repaid over more than a year (regulation 6)");
    }
    if deferral == "one-month" {
        add_months(&date, 1)
    } else {
        add_days(&date, 30)
    }
}

/// The amount a debtor pays to settle a regulated consumer credit agreement
/// early, and the rebate, per the Consumer Credit (Early Settlement)
/// Regulations 2004, regulation 4(1): the rebate is the scheduled repayments
/// falling due after the settlement date less
/// Σ A_i (1 + r)^a_i − Σ B_j (1 + r)^b_j over the advances and the repayments
/// made before the settlement date, each carried forward to the settlement
/// date at the APR. That difference is the settlement figure. Times are
/// days / 365 of a year.
///
/// # Panics
/// Panics on empty or invalid flows, an APR out of range, an unknown or
/// disallowed deferral, or no advance before the settlement date.
pub fn early_settlement(
    advances: &[DatedAmount],
    repayments: &[DatedAmount],
    apr_basis_points: i64,
    notice_received_iso: &str,
    requested_settlement_iso: Option<&str>,
    deferral: &str,
) -> EarlySettlement {
    if advances.is_empty() {
        panic!("advances must not be empty");
    }
    if repayments.is_empty() {
        panic!("repayments must not be empty");
    }
    let b = apr_basis_points;
    if b < 0 || b > 1000000 {
        panic!("aprBasisPoints must be between 0 and 1000000, received {}", b);
    }
    let currency = advances[0].amount.currency.clone();
    for flow in advances.iter().chain(repayments.iter()) {
        days_between(&flow.date, &flow.date);
        if flow.amount.currency != currency {
            panic!("currency mismatch: {} and {}", currency, flow.amount.currency);
        }
        if flow.amount.minor <= 0 {
            panic!("every amount must be greater than zero, received {}", flow.amount.minor);
        }
    }
    let date = settlement_date(advances, repayments, notice_received_iso, requested_settlement_iso, deferral);
    let scale = fixed_scale();
    // (1 + APR)^(1/365), once; each flow is then a whole power of it.
    let daily = root_fixed(
        &scale.mul(&BigInt::from_i64(10000 + b)).div(&BigInt::from_i64(10000)),
        365,
    );
    let carried = |flow: &DatedAmount| {
        BigInt::from_i64(flow.amount.minor).mul(&pow_fixed(&daily, days_between(&flow.date, &date) as u64))
    };
    let mut owed = BigInt::zero();
    let mut drawn = false;
    for advance in advances {
        if advance.date < date {
            owed = owed.add(&carried(advance));
            drawn = true;
        }
    }
    if !drawn {
        panic!("no credit was advanced before the settlement date {}", date);
    }
    let mut remaining = 0i64;
    for repayment in repayments {
        if repayment.date < date {
            owed = owed.sub(&carried(repayment));
        } else {
            remaining += repayment.amount.minor;
        }
    }
    // Rounded down: the rebate must be at least the one the formula gives.
    let formula = if owed > BigInt::zero() { owed.div(&scale).to_i64() } else { 0 };
    let settlement = formula.min(remaining);
    EarlySettlement {
        settlement_amount: money(settlement, &currency),
        remaining_repayments: money(remaining, &currency),
        rebate: money(remaining - settlement, &currency),
        settlement_date: date,
    }
}

pub fn dated_amount_from_value(v: &Value) -> DatedAmount {
    DatedAmount {
        date: v.get("date").as_str().to_string(),
        amount: money_from_value(v.get("amount")),
    }
}

pub fn early_settlement_to_value(result: &EarlySettlement) -> Value {
    Value::obj(vec![
        ("settlementDate", Value::str(&result.settlement_date)),
        ("settlementAmount", money_to_value(&result.settlement_amount)),
        ("remainingRepayments", money_to_value(&result.remaining_repayments)),
        ("rebate", money_to_value(&result.rebate)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let advances: Vec<DatedAmount> = args[0].as_arr().iter().map(dated_amount_from_value).collect();
    let repayments: Vec<DatedAmount> = args[1].as_arr().iter().map(dated_amount_from_value).collect();
    let requested = if args[4].is_null() { None } else { Some(args[4].as_str()) };
    early_settlement_to_value(&early_settlement(
        &advances,
        &repayments,
        args[2].as_i64(),
        args[3].as_str(),
        requested,
        args[5].as_str(),
    ))
}