use super::funejson::Value; use super::dates_days_between::days_between; use super::money_add::add_money; use super::money_amount::{money, money_from_value, money_to_value}; /// Bucket open invoices by how many days past their due date they are. /// /// Age runs from the due date, not the invoice date: an invoice due today is /// current. Every bucket is returned, empty or not, so a report's columns are /// fixed. /// /// # Panics /// Panics on bad bucket bounds, a malformed date or mixed currencies. pub fn aged_debt(invoices: &[OpenInvoice], as_at: &str, bucket_days: &[i64], currency: &str) -> AgedDebt { if bucket_days.is_empty() { panic!("bucketDays needs at least one boundary"); } let mut previous = 0; for &bound in bucket_days { if bound <= previous { panic!( "bucketDays must be whole days, ascending, from 1: received {} after {}", bound, previous ); } previous = bound; } let zero = money(0, currency); let mut buckets: Vec = vec![AgedDebtBucket { label: "current".to_string(), from_days: None, to_days: Some(0), total: zero.clone(), references: Vec::new(), }]; let mut start = 1; for &bound in bucket_days { buckets.push(AgedDebtBucket { label: format!("{}-{}", start, bound), from_days: Some(start), to_days: Some(bound), total: zero.clone(), references: Vec::new(), }); start = bound + 1; } buckets.push(AgedDebtBucket { label: format!("{}+", start - 1), from_days: Some(start), to_days: None, total: zero.clone(), references: Vec::new(), }); let mut total = zero; for invoice in invoices { let overdue = days_between(&invoice.due_date, as_at); let mut index = 0; if overdue > 0 { index = bucket_days.len() + 1; for (i, &bound) in bucket_days.iter().enumerate() { if overdue <= bound { index = i + 1; break; } } } let bucket = &mut buckets[index]; bucket.total = add_money(&bucket.total, &invoice.outstanding); bucket.references.push(invoice.reference.clone()); total = add_money(&total, &invoice.outstanding); } AgedDebt { as_at: as_at.to_string(), buckets, total, } } fn optional_int(value: Option) -> Value { match value { Some(n) => Value::Int(n), None => Value::Null, } } pub fn open_invoice_from_value(v: &Value) -> OpenInvoice { OpenInvoice { reference: v.get("reference").as_str().to_string(), due_date: v.get("dueDate").as_str().to_string(), outstanding: money_from_value(v.get("outstanding")), } } pub fn aged_debt_to_value(report: &AgedDebt) -> Value { Value::obj(vec![ ("asAt", Value::str(&report.as_at)), ( "buckets", Value::Arr( report .buckets .iter() .map(|b| { Value::obj(vec![ ("label", Value::str(&b.label)), ("fromDays", optional_int(b.from_days)), ("toDays", optional_int(b.to_days)), ("total", money_to_value(&b.total)), ("references", Value::Arr(b.references.iter().map(|r| Value::str(r)).collect())), ]) }) .collect(), ), ), ("total", money_to_value(&report.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let invoices: Vec = args[0].as_arr().iter().map(open_invoice_from_value).collect(); let bounds: Vec = args[2].as_arr().iter().map(|v| v.as_i64()).collect(); aged_debt_to_value(&aged_debt(&invoices, args[1].as_str(), &bounds, args[3].as_str())) }