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