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_add_months::add_months; ← from dates.add-months ^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::math_big_integer::BigInt; ← from math.big-integer ^1.0.0 · built alongside by fune
use super::math_fractional_power::{fixed_scale, pow_fixed, root_fixed}; ← from math.fractional-power ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
/// The settlement date: 28 days after the creditor receives the notice
/// (regulation 5(1)(a)), or a later date the notice names, then deferred by a
/// month or 30 days where the creditor elects and regulation 6 allows it
/// (credit repaid over more than a year from the first drawdown).
fn settlement_date(
advances: &[DatedAmount],
repayments: &[DatedAmount],
notice_received_iso: &str,
requested_settlement_iso: Option<&str>,
deferral: &str,
) -> String {
let mut date = add_days(notice_received_iso, 28);
if let Some(requested) = requested_settlement_iso {
days_between(requested, requested);
if requested > date.as_str() {
date = requested.to_string();
}
}
if deferral == "none" {
return date;
}
if deferral != "one-month" && deferral != "thirty-days" {
panic!("unknown deferral \"{}\": expected none, one-month or thirty-days", deferral);
}
let first = advances.iter().map(|a| a.date.as_str()).min().unwrap();
let last = repayments.iter().map(|r| r.date.as_str()).max().unwrap();
if last <= add_months(first, 12).as_str() {
panic!("deferral is only allowed when the credit is repaid over more than a year (regulation 6)");
}
if deferral == "one-month" {
add_months(&date, 1)
} else {
add_days(&date, 30)
}
}
/// The amount a debtor pays to settle a regulated consumer credit agreement
/// early, and the rebate, per the Consumer Credit (Early Settlement)
/// Regulations 2004, regulation 4(1): the rebate is the scheduled repayments
/// falling due after the settlement date less
/// Σ A_i (1 + r)^a_i − Σ B_j (1 + r)^b_j over the advances and the repayments
/// made before the settlement date, each carried forward to the settlement
/// date at the APR. That difference is the settlement figure. Times are
/// days / 365 of a year.
///
/// # Panics
/// Panics on empty or invalid flows, an APR out of range, an unknown or
/// disallowed deferral, or no advance before the settlement date.
pub fn early_settlement(
advances: &[DatedAmount],
repayments: &[DatedAmount],
apr_basis_points: i64,
notice_received_iso: &str,
requested_settlement_iso: Option<&str>,
deferral: &str,
) -> EarlySettlement {
if advances.is_empty() {
panic!("advances must not be empty");
}
if repayments.is_empty() {
panic!("repayments must not be empty");
}
let b = apr_basis_points;
if b < 0 || b > 1000000 {
panic!("aprBasisPoints must be between 0 and 1000000, received {}", b);
}
let currency = advances[0].amount.currency.clone();
for flow in advances.iter().chain(repayments.iter()) {
days_between(&flow.date, &flow.date);
if flow.amount.currency != currency {
panic!("currency mismatch: {} and {}", currency, flow.amount.currency);
}
if flow.amount.minor <= 0 {
panic!("every amount must be greater than zero, received {}", flow.amount.minor);
}
}
let date = settlement_date(advances, repayments, notice_received_iso, requested_settlement_iso, deferral);
let scale = fixed_scale();
// (1 + APR)^(1/365), once; each flow is then a whole power of it.
let daily = root_fixed(
&scale.mul(&BigInt::from_i64(10000 + b)).div(&BigInt::from_i64(10000)),
365,
);
let carried = |flow: &DatedAmount| {
BigInt::from_i64(flow.amount.minor).mul(&pow_fixed(&daily, days_between(&flow.date, &date) as u64))
};
let mut owed = BigInt::zero();
let mut drawn = false;
for advance in advances {
if advance.date < date {
owed = owed.add(&carried(advance));
drawn = true;
}
}
if !drawn {
panic!("no credit was advanced before the settlement date {}", date);
}
let mut remaining = 0i64;
for repayment in repayments {
if repayment.date < date {
owed = owed.sub(&carried(repayment));
} else {
remaining += repayment.amount.minor;
}
}
// Rounded down: the rebate must be at least the one the formula gives.
let formula = if owed > BigInt::zero() { owed.div(&scale).to_i64() } else { 0 };
let settlement = formula.min(remaining);
EarlySettlement {
settlement_amount: money(settlement, ¤cy),
remaining_repayments: money(remaining, ¤cy),
rebate: money(remaining - settlement, ¤cy),
settlement_date: date,
}
}
pub fn dated_amount_from_value(v: &Value) -> DatedAmount {
DatedAmount {
date: v.get("date").as_str().to_string(),
amount: money_from_value(v.get("amount")),
}
}
pub fn early_settlement_to_value(result: &EarlySettlement) -> Value {
Value::obj(vec![
("settlementDate", Value::str(&result.settlement_date)),
("settlementAmount", money_to_value(&result.settlement_amount)),
("remainingRepayments", money_to_value(&result.remaining_repayments)),
("rebate", money_to_value(&result.rebate)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let advances: Vec<DatedAmount> = args[0].as_arr().iter().map(dated_amount_from_value).collect();
let repayments: Vec<DatedAmount> = args[1].as_arr().iter().map(dated_amount_from_value).collect();
let requested = if args[4].is_null() { None } else { Some(args[4].as_str()) };
early_settlement_to_value(&early_settlement(
&advances,
&repayments,
args[2].as_i64(),
args[3].as_str(),
requested,
args[5].as_str(),
))
}