use super::funejson::Value; use super::dates_add_days::{add_days, days_between}; use super::finance_late_payment_interest::late_payment_interest; 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; use super::money_amount::{money, money_from_value, money_to_value, Money}; 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(), )) }