use super::funejson::Value; use super::dates_add_days::add_days; use super::dates_days_between::days_between; use super::finance_late_payment_interest_data::{FIXED_SUMS, FIXED_SUMS_HISTORY}; use super::finance_uk_bank_rate::uk_bank_rate; use super::math_round_div::round_div; use super::money_add::add_money; use super::money_amount::{money, money_from_value, money_to_value, Money}; 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) -> 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(), )) }