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(),
))
}