use super::funejson::Value; use super::dates_days_between::days_between; use super::dates_overlap::overlap_days; use super::money_allocate::allocate; use super::money_amount::{money, money_from_value, money_to_value, Money}; /// Split an invoice across accounting periods by days of service. /// /// money.allocate does the split with each period's days as the weight, so the /// shares add back to the invoice exactly. A negative amount is split as its /// positive and negated, so a credit note mirrors its invoice period by period. /// /// # Panics /// Panics on malformed dates, a service that ends before it starts, periods /// that overlap, are out of order or leave days of service uncovered. pub fn allocate_to_periods( amount: &Money, service_start: &str, service_end: &str, periods: &[AccountingPeriod], ) -> Vec { let service_days = days_between(service_start, service_end) + 1; if service_days < 1 { panic!("service ends {}, before it starts {}", service_end, service_start); } if periods.is_empty() { panic!("at least one accounting period is needed"); } let mut days: Vec = Vec::with_capacity(periods.len()); for (index, period) in periods.iter().enumerate() { if days_between(&period.start, &period.end) < 0 { panic!("period {} ends before it starts", period.label); } if index > 0 && days_between(&periods[index - 1].end, &period.start) < 1 { panic!("period {} overlaps or precedes the period before it", period.label); } days.push(overlap_days(service_start, service_end, &period.start, &period.end, true)); } let covered: i64 = days.iter().sum(); if covered != service_days { panic!( "the accounting periods cover {} of the {} days of service", covered, service_days ); } let negative = amount.minor < 0; let magnitude = money(if negative { -amount.minor } else { amount.minor }, &amount.currency); let shares = allocate(&magnitude, &days); periods .iter() .enumerate() .map(|(index, period)| PeriodAllocation { label: period.label.clone(), days: days[index], amount: if negative { money(-shares[index].minor, &amount.currency) } else { shares[index].clone() }, }) .collect() } pub fn accounting_period_from_value(v: &Value) -> AccountingPeriod { AccountingPeriod { label: v.get("label").as_str().to_string(), start: v.get("start").as_str().to_string(), end: v.get("end").as_str().to_string(), } } pub fn period_allocation_to_value(a: &PeriodAllocation) -> Value { Value::obj(vec![ ("label", Value::str(&a.label)), ("days", Value::Int(a.days)), ("amount", money_to_value(&a.amount)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let periods: Vec = args[3].as_arr().iter().map(accounting_period_from_value).collect(); Value::Arr( allocate_to_periods(&money_from_value(&args[0]), args[1].as_str(), args[2].as_str(), &periods) .iter() .map(period_allocation_to_value) .collect(), ) }