Functional Weave
Code in Python

legal.statutory-interest@1.0.1

impl/rust.rs

6,391 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, days_between};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::finance_late_payment_interest::late_payment_interest;  ← from finance.late-payment-interest ^1.0.0 · built alongside by fune
use super::legal_statutory_interest_data::{
    COUNTY_COURT_THRESHOLDS, COUNTY_COURT_THRESHOLDS_HISTORY, JUDGMENT_RATES, JUDGMENT_RATES_HISTORY,
};
use super::math_round_div::round_div;  ← from math.round-div ^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 YEAR: i64 = 10000 * 365;
const MAX_EXACT: i128 = 9_007_199_254_740_991;

fn 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 rule 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 judgments.",
            what, on_date, history, earliest
        );
    }
}

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 rate_on(on_date: &str) -> i64 {
    for row in JUDGMENT_RATES {
        if in_force(on_date, row.valid_from, row.valid_to) {
            return row.basis_points;
        }
    }
    let froms: Vec<&str> = JUDGMENT_RATES.iter().map(|r| r.valid_from).collect();
    horizon(JUDGMENT_RATES_HISTORY, &froms, on_date, "judgment interest rate");
    panic!("no judgment interest rate in force on {}", on_date);
}

fn checked(a: i64, b: i64, c: i64) -> i64 {
    if (a as i128) * (b as i128) * (c as i128) > MAX_EXACT {
        panic!("the interest calculation exceeds the exact integer range");
    }
    a * b * c
}

/// Statutory interest on a judgment debt or a late commercial debt.
///
/// # Panics
/// Panics on a non-GBP or non-positive amount, an unknown basis, dates out of
/// order or outside the data, or a product beyond the exact integer range.
pub fn statutory_interest(amount: &Money, from_date: &str, to_date: &str, basis: &str) -> StatutoryInterest {
    if amount.currency != "GBP" {
        panic!("statutory interest is defined for GBP debts, received {}", amount.currency);
    }
    if amount.minor <= 0 {
        panic!("amount must be greater than zero");
    }
    let zero = money(0, "GBP");
    if basis == "late-payment" {
        let claim = late_payment_interest(amount, from_date, to_date);
        let rate = claim.interest_rate_basis_points.unwrap_or(0);
        return StatutoryInterest {
            days: claim.days_late,
            rate_basis_points: rate,
            daily_interest: money(round_div(amount.minor * rate, YEAR, "half-up"), "GBP"),
            interest: claim.interest,
            compensation: claim.compensation,
            total: claim.total,
        };
    }
    if basis != "judgment-high-court" && basis != "judgment-county-court" {
        panic!(
            "unknown interest basis \"{}\": expected judgment-high-court, judgment-county-court or late-payment",
            basis
        );
    }
    let days = days_between(from_date, to_date);
    if days < 0 {
        panic!("toDate {} is before fromDate {}", to_date, from_date);
    }
    if basis == "judgment-county-court" {
        match COUNTY_COURT_THRESHOLDS.iter().find(|r| in_force(from_date, r.valid_from, r.valid_to)) {
            None => {
                let froms: Vec<&str> = COUNTY_COURT_THRESHOLDS.iter().map(|r| r.valid_from).collect();
                horizon(COUNTY_COURT_THRESHOLDS_HISTORY, &froms, from_date, "County Court judgment interest rule");
                panic!("no County Court judgment interest rule in force on {}", from_date);
            }
            Some(threshold) => {
                if amount.minor < threshold.min_judgment_minor {
                    // Not a "relevant judgment": no statutory interest at all.
                    return StatutoryInterest {
                        days,
                        rate_basis_points: 0,
                        daily_interest: zero.clone(),
                        interest: zero.clone(),
                        compensation: zero.clone(),
                        total: zero,
                    };
                }
            }
        }
    }
    // Each day from the day after judgment to the payment date at the rate in force on it.
    let mut numerator: i128 = 0;
    let mut covered = 0;
    let first = add_days(from_date, 1);
    for row in JUDGMENT_RATES {
        if days == 0 {
            break;
        }
        let start: &str = if first.as_str() > row.valid_from { &first } else { row.valid_from };
        let end: &str = match row.valid_to {
            Some(to) if to < to_date => to,
            _ => to_date,
        };
        if start > end {
            continue;
        }
        let segment = days_between(start, end) + 1;
        covered += segment;
        numerator += checked(amount.minor, row.basis_points, segment) as i128;
    }
    if covered != days {
        rate_on(&first);
        panic!("no judgment interest rate for every day from {} to {}", first, to_date);
    }
    if numerator > MAX_EXACT {
        panic!("the interest calculation exceeds the exact integer range");
    }
    let rate = rate_on(to_date);
    let interest = money(round_div(numerator as i64, YEAR, "half-up"), "GBP");
    StatutoryInterest {
        days,
        rate_basis_points: rate,
        daily_interest: money(round_div(amount.minor * rate, YEAR, "half-up"), "GBP"),
        interest: interest.clone(),
        compensation: zero,
        total: interest,
    }
}

pub fn statutory_interest_to_value(s: &StatutoryInterest) -> Value {
    Value::obj(vec![
        ("days", Value::Int(s.days)),
        ("rateBasisPoints", Value::Int(s.rate_basis_points)),
        ("dailyInterest", money_to_value(&s.daily_interest)),
        ("interest", money_to_value(&s.interest)),
        ("compensation", money_to_value(&s.compensation)),
        ("total", money_to_value(&s.total)),
    ])
}

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