use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::time_round_to_increment::round_to_increment; /// Price time entries in whole billing units at each fee earner's hourly rate. /// /// # Panics /// Panics on an empty, duplicated, negative or mixed-currency rate card, an /// entry with no rate, negative minutes or a unit below one. pub fn time_billing(entries: &[TimeEntry], rates: &[FeeEarnerRate], unit_minutes: i64, mode: &str) -> TimeBill { if rates.is_empty() { panic!("rates must not be empty"); } let currency = rates[0].hourly_rate.currency.clone(); let mut card: Vec<(&str, &Money)> = Vec::new(); for rate in rates { if card.iter().any(|(code, _)| *code == rate.fee_earner) { panic!("duplicate rate for fee earner \"{}\"", rate.fee_earner); } if rate.hourly_rate.currency != currency { panic!("currency mismatch: {} and {}", currency, rate.hourly_rate.currency); } if rate.hourly_rate.minor < 0 { panic!("hourly rates must not be negative, received {}", rate.hourly_rate.minor); } card.push((&rate.fee_earner, &rate.hourly_rate)); } let mut lines: Vec = Vec::new(); let mut totals: Vec<(String, i64, i64)> = Vec::new(); let mut units = 0; let mut billed_minutes = 0; let mut total = 0; for entry in entries { let hourly_rate = match card.iter().find(|(code, _)| *code == entry.fee_earner) { Some((_, rate)) => (*rate).clone(), None => panic!("no rate for fee earner \"{}\"", entry.fee_earner), }; // Rounded per entry: seven 1-minute emails are seven units, not two. let billed = round_to_increment(entry.minutes, unit_minutes, mode); let line_units = billed / unit_minutes; let fee = round_div(billed * hourly_rate.minor, 60, "half-up"); match totals.iter_mut().find(|t| t.0 == entry.fee_earner) { Some(t) => { t.1 += line_units; t.2 += fee; } None => totals.push((entry.fee_earner.clone(), line_units, fee)), } lines.push(TimeBillLine { fee_earner: entry.fee_earner.clone(), minutes: entry.minutes, billed_minutes: billed, units: line_units, hourly_rate, fee: money(fee, ¤cy), }); units += line_units; billed_minutes += billed; total += fee; } let by_fee_earner = totals .into_iter() .map(|(code, u, fee)| FeeEarnerTotal { fee_earner: code, units: u, fee: money(fee, ¤cy) }) .collect(); TimeBill { lines, by_fee_earner, units, billed_minutes, total: money(total, ¤cy) } } pub fn time_entry_from_value(v: &Value) -> TimeEntry { TimeEntry { fee_earner: v.get("feeEarner").as_str().to_string(), minutes: v.get("minutes").as_i64() } } pub fn fee_earner_rate_from_value(v: &Value) -> FeeEarnerRate { FeeEarnerRate { fee_earner: v.get("feeEarner").as_str().to_string(), hourly_rate: money_from_value(v.get("hourlyRate")) } } pub fn time_bill_to_value(b: &TimeBill) -> Value { Value::obj(vec![ ( "lines", Value::Arr( b.lines .iter() .map(|l| { Value::obj(vec![ ("feeEarner", Value::str(&l.fee_earner)), ("minutes", Value::Int(l.minutes)), ("billedMinutes", Value::Int(l.billed_minutes)), ("units", Value::Int(l.units)), ("hourlyRate", money_to_value(&l.hourly_rate)), ("fee", money_to_value(&l.fee)), ]) }) .collect(), ), ), ( "byFeeEarner", Value::Arr( b.by_fee_earner .iter() .map(|t| { Value::obj(vec![ ("feeEarner", Value::str(&t.fee_earner)), ("units", Value::Int(t.units)), ("fee", money_to_value(&t.fee)), ]) }) .collect(), ), ), ("units", Value::Int(b.units)), ("billedMinutes", Value::Int(b.billed_minutes)), ("total", money_to_value(&b.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let entries: Vec = args[0].as_arr().iter().map(time_entry_from_value).collect(); let rates: Vec = args[1].as_arr().iter().map(fee_earner_rate_from_value).collect(); time_bill_to_value(&time_billing(&entries, &rates, args[2].as_i64(), args[3].as_str())) }