use super::funejson::Value; use super::money_allocate::allocate; use super::money_amount::{money_from_value, money_to_value, Money}; /// Split a period's charge into the part used and the part not used. /// /// The mid-cycle upgrade, downgrade and cancellation calculation. Written the /// obvious way - one rounding for the charge and another for the credit - the /// two halves do not reliably add back up: 9.99 over two days is 4.995 each way, /// and two half-up roundings produce 5.00 and 5.00, a penny invented on a line /// the customer can see. /// /// One call to `money::allocate` does the whole split instead, so /// `used + unused` is the original amount for every input, credits included. /// /// # Panics /// Panics if `total_days` is not positive, or `used_days` is outside /// `0..=total_days`. pub fn prorate(amount: &Money, total_days: i64, used_days: i64) -> ProrationSplit { if total_days <= 0 { panic!( "total days must be greater than zero, received {}", total_days ); } if used_days < 0 { panic!("used days must not be negative, received {}", used_days); } if used_days > total_days { // Clamping here would turn a bug in the caller's period arithmetic into // a plausible invoice, which is far more expensive to find later. panic!( "used days must not exceed total days, received {} of {}", used_days, total_days ); } // total_days > 0 guarantees the ratios do not sum to zero, so allocate is // safe even when one side is zero: [0, n] and [n, 0] are both well defined. let parts = allocate(amount, &[used_days, total_days - used_days]); ProrationSplit { total: amount.clone(), used: parts[0].clone(), unused: parts[1].clone(), } } pub fn proration_split_to_value(split: &ProrationSplit) -> Value { Value::obj(vec![ ("total", money_to_value(&split.total)), ("used", money_to_value(&split.used)), ("unused", money_to_value(&split.unused)), ]) } pub fn fune_vector(args: &[Value]) -> Value { // Refuse what the typed signature cannot hold, with the wording TypeScript // and Python use, rather than let the conversion below quietly change it. if let Value::Float(f) = args[1] { if f.fract() != 0.0 { panic!("total days must be greater than zero, received {}", f); } } if let Value::Float(f) = args[2] { if f.fract() != 0.0 { panic!("used days must not be negative, received {}", f); } } proration_split_to_value(&prorate( &money_from_value(&args[0]), args[1].as_i64(), args[2].as_i64(), )) }