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