Functional Weave
Code in Python

fleet.mileage-allowance@1.0.2

impl/rust.rs

6,668 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::fleet_mileage_allowance_data::{AmapRate, AMAP_RATES, AMAP_RATES_HISTORY};  ← this capability’s own data, compiled from data/amap-rates.json into the same file by fune build
use super::money_add::subtract_money;  ← from money.add ^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

fn is_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 10
        && bytes[4] == b'-'
        && bytes[7] == b'-'
        && bytes
            .iter()
            .enumerate()
            .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}

fn rate_on(vehicle: &str, on_date: &str) -> &'static AmapRate {
    let mut best: Option<&'static AmapRate> = None;
    let mut earliest: Option<&'static str> = None;
    for rule in AMAP_RATES {
        if rule.vehicle != vehicle {
            continue;
        }
        if earliest.map_or(true, |e| rule.valid_from < e) {
            earliest = Some(rule.valid_from);
        }
        if on_date < rule.valid_from {
            continue;
        }
        if let Some(valid_to) = rule.valid_to {
            if on_date > valid_to {
                continue;
            }
        }
        if best.map_or(true, |b| rule.valid_from > b.valid_from) {
            best = Some(rule);
        }
    }
    if let Some(rule) = best {
        return rule;
    }
    // A history=current build has dropped the old rows; refuse rather than pay
    // an old claim at today's rate.
    if AMAP_RATES_HISTORY != "full" {
        if let Some(earliest) = earliest {
            if on_date < earliest {
                panic!(
                    "no approved mileage rate for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older claims.",
                    vehicle, on_date, AMAP_RATES_HISTORY, earliest
                );
            }
        }
    }
    panic!("no approved mileage rate for {} on {}", vehicle, on_date)
}

fn whole_miles(name: &str, value: i64) {
    if value < 0 {
        panic!("{} must be a whole number of miles, not negative, received {}", name, value);
    }
}

fn gbp(name: &str, value: &Money) {
    if value.currency != "GBP" {
        panic!("{} must be in GBP, received {}", name, value.currency);
    }
    if value.minor < 0 {
        panic!("{} must not be negative, received {}", name, value.minor);
    }
}

fn excess_and_shortfall(paid: &Money, approved: &Money) -> (Money, Money) {
    let difference = subtract_money(paid, approved);
    if difference.minor > 0 {
        (difference, money(0, "GBP"))
    } else {
        (money(0, "GBP"), money(-difference.minor, "GBP"))
    }
}

/// HMRC approved mileage allowance payments for business miles in an
/// employee's own vehicle.
///
/// The 10,000-mile threshold is per tax year, so a claim that crosses it is
/// split: `previous_miles` says where in the year this claim starts. Paying a
/// flat 45p (or 55p) on every mile past 10,000 is the usual mistake; the
/// excess over 25p is taxable.
///
/// # Panics
/// Panics on an unknown vehicle, negative miles or amounts, a currency other
/// than GBP, passenger payments for a motorcycle or bicycle, a malformed date,
/// or a date no rate covers.
pub fn mileage_allowance(
    vehicle: &str,
    previous_miles: i64,
    miles: i64,
    passenger_miles: i64,
    paid: &Money,
    passenger_paid: &Money,
    on_date: &str,
) -> MileageAllowance {
    if vehicle != "car-van" && vehicle != "motorcycle" && vehicle != "bicycle" {
        panic!("unknown vehicle \"{}\": use car-van, motorcycle or bicycle", vehicle);
    }
    whole_miles("previousMiles", previous_miles);
    whole_miles("miles", miles);
    whole_miles("passengerMiles", passenger_miles);
    gbp("paid", paid);
    gbp("passengerPaid", passenger_paid);
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    if vehicle != "car-van" && (passenger_miles > 0 || passenger_paid.minor > 0) {
        panic!("passenger payments apply only to cars and vans");
    }

    let rate = rate_on(vehicle, on_date);
    let mut miles_at_first_rate = miles;
    let mut miles_at_second_rate = 0;
    if let Some(threshold) = rate.threshold_miles {
        let first_left = (threshold - previous_miles).max(0);
        miles_at_first_rate = miles.min(first_left);
        miles_at_second_rate = miles - miles_at_first_rate;
    }
    let approved = money(
        miles_at_first_rate * rate.first_rate_pence + miles_at_second_rate * rate.second_rate_pence,
        "GBP",
    );
    let (taxable_excess, relief) = excess_and_shortfall(paid, &approved);

    let passenger_approved = if vehicle == "car-van" {
        money(passenger_miles * rate_on("passenger", on_date).first_rate_pence, "GBP")
    } else {
        money(0, "GBP")
    };
    // There is no relief for passenger payments below 5p, only tax above it.
    let (passenger_taxable_excess, _) = excess_and_shortfall(passenger_paid, &passenger_approved);

    MileageAllowance {
        approved,
        paid: paid.clone(),
        taxable_excess,
        relief,
        miles_at_first_rate,
        miles_at_second_rate,
        first_rate_pence: rate.first_rate_pence,
        second_rate_pence: rate.second_rate_pence,
        passenger_approved,
        passenger_taxable_excess,
    }
}

pub fn mileage_allowance_to_value(m: &MileageAllowance) -> Value {
    Value::obj(vec![
        ("approved", money_to_value(&m.approved)),
        ("paid", money_to_value(&m.paid)),
        ("taxableExcess", money_to_value(&m.taxable_excess)),
        ("relief", money_to_value(&m.relief)),
        ("milesAtFirstRate", Value::Int(m.miles_at_first_rate)),
        ("milesAtSecondRate", Value::Int(m.miles_at_second_rate)),
        ("firstRatePence", Value::Int(m.first_rate_pence)),
        ("secondRatePence", Value::Int(m.second_rate_pence)),
        ("passengerApproved", money_to_value(&m.passenger_approved)),
        ("passengerTaxableExcess", money_to_value(&m.passenger_taxable_excess)),
    ])
}

fn miles_arg(value: &Value, name: &str) -> i64 {
    match value {
        Value::Int(i) => *i,
        Value::Float(f) if f.fract() == 0.0 => *f as i64,
        other => panic!(
            "{} must be a whole number of miles, not negative, received {:?}",
            name, other
        ),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    mileage_allowance_to_value(&mileage_allowance(
        args[0].as_str(),
        miles_arg(&args[1], "previousMiles"),
        miles_arg(&args[2], "miles"),
        miles_arg(&args[3], "passengerMiles"),
        &money_from_value(&args[4]),
        &money_from_value(&args[5]),
        args[6].as_str(),
    ))
}