Functional Weave
Code in Rust

finance.late-payment-interest@1.0.1

impl/rust.rs

5,257 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_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune
use super::finance_late_payment_interest_data::{FIXED_SUMS, FIXED_SUMS_HISTORY};  ← this capability’s own data, compiled from data/fixed-sums.json into the same file by fune build
use super::finance_uk_bank_rate::uk_bank_rate;  ← from finance.uk-bank-rate ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_add::add_money;  ← from money.add ^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

const STATUTORY_MARGIN: i64 = 800; // 8% over the reference rate, SI 2002/1675 art. 4
const MAX_EXACT: i128 = 9_007_199_254_740_991;

fn in_force(on_date: &str, valid_from: &str, valid_to: Option<&str>) -> bool {
    on_date >= valid_from && valid_to.map_or(true, |to| on_date <= to)
}

fn pruned_horizon(history: &str, froms: &[&str], on_date: &str, what: &str) {
    // A history=current build keeps only rows still in force; answering an older
    // date with today's figure is the failure dated data exists to prevent.
    if history == "full" || froms.is_empty() {
        return;
    }
    let earliest = froms.iter().min().unwrap();
    if on_date < *earliest {
        panic!(
            "no {} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for older debts.",
            what, on_date, history, earliest
        );
    }
}

fn fixed_sum_for(debt_minor: i64, on_date: &str) -> i64 {
    let mut best: Option<(i64, i64)> = None;
    for row in FIXED_SUMS {
        if !in_force(on_date, row.valid_from, row.valid_to) || debt_minor < row.min_debt_minor {
            continue;
        }
        if best.map_or(true, |(min, _)| row.min_debt_minor > min) {
            best = Some((row.min_debt_minor, row.compensation_minor));
        }
    }
    match best {
        Some((_, compensation)) => compensation,
        None => {
            let froms: Vec<&str> = FIXED_SUMS.iter().map(|r| r.valid_from).collect();
            pruned_horizon(FIXED_SUMS_HISTORY, &froms, on_date, "Late Payment Act fixed sum");
            panic!("no Late Payment Act fixed sum in force on {}", on_date);
        }
    }
}

/// The 30 June or 31 December immediately before the day interest starts.
fn reference_date_for(start_date: &str) -> String {
    let year: i64 = start_date[0..4].parse().unwrap();
    let month: i64 = start_date[5..7].parse().unwrap();
    if month <= 6 {
        format!("{:04}-12-31", year - 1)
    } else {
        format!("{}-06-30", &start_date[0..4])
    }
}

/// Statutory interest and the fixed sum on a late commercial debt (UK).
///
/// The reference rate is fixed when interest starts, not when it is paid, and
/// interest is simple at 1/365 of the yearly rate per day, rounded once.
///
/// # Panics
/// Panics on a non-GBP or non-positive debt, malformed dates, dates outside the
/// rules' data, or a product beyond the exact integer range.
pub fn late_payment_interest(debt: &Money, due_date: &str, payment_date: &str) -> LatePaymentClaim {
    if debt.currency != "GBP" {
        panic!("late payment interest is defined for GBP debts, received {}", debt.currency);
    }
    if debt.minor <= 0 {
        panic!("debt must be greater than zero");
    }
    let late = days_between(due_date, payment_date);
    let zero = money(0, "GBP");
    if late <= 0 {
        return LatePaymentClaim {
            days_late: 0,
            reference_date: None,
            reference_rate_basis_points: None,
            interest_rate_basis_points: None,
            interest: zero.clone(),
            compensation: zero.clone(),
            total: zero,
        };
    }

    let start_date = add_days(due_date, 1);
    let compensation = money(fixed_sum_for(debt.minor, &start_date), "GBP");
    let reference_date = reference_date_for(&start_date);
    let reference_rate = uk_bank_rate(&reference_date).basis_points;
    let rate = STATUTORY_MARGIN + reference_rate;

    if (debt.minor as i128) * (rate as i128) * (late as i128) > MAX_EXACT {
        panic!("the interest calculation exceeds the exact integer range");
    }
    let interest = money(round_div(debt.minor * rate * late, 10000 * 365, "half-up"), "GBP");
    let total = add_money(&interest, &compensation);
    LatePaymentClaim {
        days_late: late,
        reference_date: Some(reference_date),
        reference_rate_basis_points: Some(reference_rate),
        interest_rate_basis_points: Some(rate),
        interest,
        compensation,
        total,
    }
}

fn optional_int(value: Option<i64>) -> Value {
    value.map_or(Value::Null, Value::Int)
}

pub fn late_payment_claim_to_value(c: &LatePaymentClaim) -> Value {
    Value::obj(vec![
        ("daysLate", Value::Int(c.days_late)),
        ("referenceDate", c.reference_date.as_deref().map_or(Value::Null, Value::str)),
        ("referenceRateBasisPoints", optional_int(c.reference_rate_basis_points)),
        ("interestRateBasisPoints", optional_int(c.interest_rate_basis_points)),
        ("interest", money_to_value(&c.interest)),
        ("compensation", money_to_value(&c.compensation)),
        ("total", money_to_value(&c.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    late_payment_claim_to_value(&late_payment_interest(
        &money_from_value(&args[0]),
        args[1].as_str(),
        args[2].as_str(),
    ))
}