use super::funejson::Value; use super::dates_add_days::add_days; use super::dates_add_months::add_months; use super::dates_days_between::days_between; use super::math_big_integer::BigInt; use super::math_fractional_power::{fixed_scale, pow_fixed, root_fixed}; use super::money_amount::{money, money_from_value, money_to_value}; /// 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 = args[0].as_arr().iter().map(dated_amount_from_value).collect(); let repayments: Vec = 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(), )) }