Functional Weave
Code in Rust

legal.time-billing@1.0.0

impl/rust.rs

4,978 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::time_round_to_increment::round_to_increment;  ← from time.round-to-increment ^1.0.0 · built alongside by fune

/// 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<TimeBillLine> = 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, &currency),
        });
        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, &currency) })
        .collect();
    TimeBill { lines, by_fee_earner, units, billed_minutes, total: money(total, &currency) }
}

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<TimeEntry> = args[0].as_arr().iter().map(time_entry_from_value).collect();
    let rates: Vec<FeeEarnerRate> = 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()))
}