use super::funejson::Value; use super::dates_add_days::epoch_day_from_iso; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value}; /// A claim's position after each transaction: paid, recovered, the /// outstanding case reserve, and incurred = paid - recovered + outstanding. /// /// A payment erodes the outstanding reserve by what it pays, never below /// zero: a payment larger than the reserve raises incurred by the excess, /// which is how an under-reserved claim shows up. A reserve transaction sets /// the outstanding estimate outright. The movement is what each transaction /// did to incurred, the figure that goes through the accounts. /// /// # Panics /// Panics on transactions out of date order, another currency, a negative /// reserve, a payment or recovery that is not positive, or an unknown kind. pub fn claim_reserve_history(transactions: &[ClaimTransaction], currency: &str) -> Vec { let zero = money(0, currency); let (mut paid, mut recovered, mut outstanding, mut incurred) = (0i64, 0i64, 0i64, 0i64); let mut previous: Option<(i64, &str)> = None; let mut out = Vec::new(); for t in transactions { let day = epoch_day_from_iso(&t.date); if let Some((previous_day, previous_date)) = previous { if day < previous_day { panic!("transactions must be in date order: {} comes after {}", t.date, previous_date); } } previous = Some((day, &t.date)); assert_same_currency(&zero, &t.amount); match t.kind.as_str() { "reserve" => { if t.amount.minor < 0 { panic!("a reserve must not be negative, received {} on {}", t.amount.minor, t.date); } outstanding = t.amount.minor; } "payment" | "recovery" => { if t.amount.minor <= 0 { panic!("a {} must be more than zero, received {} on {}", t.kind, t.amount.minor, t.date); } if t.kind == "payment" { paid += t.amount.minor; outstanding = (outstanding - t.amount.minor).max(0); } else { recovered += t.amount.minor; } } other => panic!("unknown transaction kind \"{}\": use reserve, payment or recovery", other), } let now = paid - recovered + outstanding; out.push(ReservePosition { date: t.date.clone(), kind: t.kind.clone(), paid: money(paid, currency), recovered: money(recovered, currency), outstanding: money(outstanding, currency), incurred: money(now, currency), movement: money(now - incurred, currency), }); incurred = now; } out } pub fn reserve_position_to_value(p: &ReservePosition) -> Value { Value::obj(vec![ ("date", Value::str(&p.date)), ("kind", Value::str(&p.kind)), ("paid", money_to_value(&p.paid)), ("recovered", money_to_value(&p.recovered)), ("outstanding", money_to_value(&p.outstanding)), ("incurred", money_to_value(&p.incurred)), ("movement", money_to_value(&p.movement)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let transactions: Vec = args[0] .as_arr() .iter() .map(|v| ClaimTransaction { date: v.get("date").as_str().to_string(), kind: v.get("kind").as_str().to_string(), amount: money_from_value(v.get("amount")), }) .collect(); Value::Arr( claim_reserve_history(&transactions, args[1].as_str()) .iter() .map(reserve_position_to_value) .collect(), ) }