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::dates_days_between::days_between; ← from dates.days-between ^1.0.0 · built alongside by fune
use super::dates_uk_tax_year::uk_tax_year; ← from dates.uk-tax-year ^1.0.0 · built alongside by fune
use super::fleet_company_car_bik_data::{
BIK_BANDS, BIK_BANDS_HISTORY, BIK_LIMITS, BIK_LIMITS_HISTORY, FUEL_MULTIPLIERS, FUEL_MULTIPLIERS_HISTORY,
};
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_add::add_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
const MAX_SAFE: i128 = 9007199254740991;
// Cars first registered before this date keep their NEDC figures, and in
// 2021-22 their own (one point higher) column of the table.
const WLTP_FROM: &str = "2020-04-06";
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 in_force(valid_from: &str, valid_to: Option<&str>, on_date: &str) -> bool {
valid_from <= on_date && valid_to.map_or(true, |to| on_date <= to)
}
// A history=current build has dropped old rows; say so rather than answering
// an old tax year with today's figures.
fn missing(what: &str, label: &str, earliest: Option<&str>, history: &str, on_date: &str) -> ! {
if history != "full" {
if let Some(earliest) = earliest {
if on_date < earliest {
panic!(
"no {} for tax year {}: this build was installed with history={}, so it only carries figures from {}. Reinstall with history=full for earlier years.",
what, label, history, earliest
);
}
}
}
panic!("no {} for tax year {}", what, label)
}
fn check_gbp(name: &str, amount: &Money) {
if amount.currency != "GBP" {
panic!("company car benefit is worked in GBP; {} is in {}", name, amount.currency);
}
if amount.minor < 0 {
panic!("{} must not be negative, received {}", name, amount.minor);
}
}
fn check_whole(name: &str, value: i64, unit: &str) {
if value < 0 {
panic!("{} must be a whole number of {}, not negative, received {}", name, unit, value);
}
}
fn check_date(name: &str, value: &str) {
if !is_iso_date(value) {
panic!("{} must be an ISO date (YYYY-MM-DD), received \"{}\"", name, value);
}
}
// price x basis points x days available / (10000 x days in year), rounded down
// to the penny: one rounding, and never more benefit than the law charges.
fn proportion(minor: i64, basis_points: i64, days_available: i64, days_in_year: i64) -> i64 {
let product = minor as i128 * basis_points as i128 * days_available as i128;
// Rust would carry on in i128, but TypeScript cannot, and all three must agree.
if product > MAX_SAFE {
panic!("the benefit calculation exceeds 2^53 - 1; the list price is too large");
}
round_div(product as i64, 10000 * days_in_year, "down")
}
/// The taxable benefit of a company car for one UK tax year (ITEPA 2003 s121).
///
/// Price less capital contributions (at most 5,000.00), times the appropriate
/// percentage for the car's CO2 figure and electric range in that year, cut
/// pro rata for days the car was unavailable, less what the employee paid for
/// private use. Free fuel adds the year's fuel benefit multiplier times the
/// same percentage, cut for the same days.
///
/// # Panics
/// Panics on a currency other than GBP, a negative amount or count, a malformed
/// date, a diesel supplement on a zero-emission car, a registration after the
/// tax year, too many unavailable days, or a tax year the tables do not cover.
#[allow(clippy::too_many_arguments)]
pub fn company_car_bik(
list_price: &Money,
capital_contributions: &Money,
co2: i64,
electric_range_miles: i64,
diesel_supplement: bool,
first_registered: &str,
on_date: &str,
unavailable_days: i64,
private_use_contributions: &Money,
free_fuel: bool,
) -> CompanyCarBik {
check_gbp("listPrice", list_price);
check_gbp("capitalContributions", capital_contributions);
check_gbp("privateUseContributions", private_use_contributions);
check_whole("co2", co2, "g/km");
check_whole("electricRangeMiles", electric_range_miles, "miles");
check_date("firstRegistered", first_registered);
check_date("onDate", on_date);
if diesel_supplement && co2 == 0 {
panic!("a zero-emission car cannot take the diesel supplement");
}
let year = uk_tax_year(on_date);
if first_registered > year.end.as_str() {
panic!(
"firstRegistered ({}) is after the end of tax year {}",
first_registered, year.label
);
}
let days_in_year = days_between(&year.start, &year.end) + 1;
if unavailable_days < 0 || unavailable_days > days_in_year {
panic!(
"unavailableDays must be a whole number from 0 to {}, received {}",
days_in_year, unavailable_days
);
}
let days_available = days_in_year - unavailable_days;
let registration = if first_registered < WLTP_FROM { "before-2020-04-06" } else { "from-2020-04-06" };
let applies = |row_registration: &str| row_registration == "any" || row_registration == registration;
let limit = match BIK_LIMITS
.iter()
.find(|row| applies(row.registration) && in_force(row.valid_from, row.valid_to, on_date))
{
Some(limit) => limit,
None => missing(
"company car benefit rates",
&year.label,
BIK_LIMITS.iter().map(|row| row.valid_from).min(),
BIK_LIMITS_HISTORY,
on_date,
),
};
let mut basis_points = if co2 >= 75 {
// s139(3): one point per whole 5 g/km above 75, up to the year's maximum.
(limit.from75_basis_points + 100 * ((co2 - 75) / 5)).min(limit.maximum_basis_points)
} else {
match BIK_BANDS.iter().find(|row| {
applies(row.registration)
&& in_force(row.valid_from, row.valid_to, on_date)
&& row.co2_from <= co2
&& co2 <= row.co2_to
&& row.range_from <= electric_range_miles
&& row.range_to.map_or(true, |to| electric_range_miles <= to)
}) {
Some(band) => band.basis_points,
None => missing(
"company car benefit band",
&year.label,
BIK_BANDS.iter().map(|row| row.valid_from).min(),
BIK_BANDS_HISTORY,
on_date,
),
}
};
if diesel_supplement {
basis_points = (basis_points + limit.diesel_supplement_basis_points).min(limit.maximum_basis_points);
}
let counted = capital_contributions.minor.min(limit.capital_contribution_cap_minor);
let price = money((list_price.minor - counted).max(0), "GBP");
let provisional = proportion(price.minor, basis_points, days_available, days_in_year);
let car_benefit = money((provisional - private_use_contributions.minor).max(0), "GBP");
let mut fuel_benefit = money(0, "GBP");
// s149(4): electricity, and energy for a car that cannot emit CO2, is not fuel.
if free_fuel && co2 > 0 {
let multiplier = match FUEL_MULTIPLIERS
.iter()
.find(|row| in_force(row.valid_from, row.valid_to, on_date))
{
Some(row) => row,
None => missing(
"car fuel benefit multiplier",
&year.label,
FUEL_MULTIPLIERS.iter().map(|row| row.valid_from).min(),
FUEL_MULTIPLIERS_HISTORY,
on_date,
),
};
fuel_benefit = money(
proportion(multiplier.pounds * 100, basis_points, days_available, days_in_year),
"GBP",
);
}
let total = add_money(&car_benefit, &fuel_benefit);
CompanyCarBik {
tax_year: year.label.clone(),
days_in_year,
days_available,
price,
appropriate_basis_points: basis_points,
car_benefit,
fuel_benefit,
total,
}
}
pub fn company_car_bik_to_value(b: &CompanyCarBik) -> Value {
Value::obj(vec![
("taxYear", Value::str(&b.tax_year)),
("daysInYear", Value::Int(b.days_in_year)),
("daysAvailable", Value::Int(b.days_available)),
("price", money_to_value(&b.price)),
("appropriateBasisPoints", Value::Int(b.appropriate_basis_points)),
("carBenefit", money_to_value(&b.car_benefit)),
("fuelBenefit", money_to_value(&b.fuel_benefit)),
("total", money_to_value(&b.total)),
])
}
fn whole(value: &Value, name: &str, unit: &str) -> i64 {
match value {
Value::Int(i) => *i,
Value::Float(f) if f.fract() == 0.0 => *f as i64,
Value::Float(f) => panic!("{} must be a whole number of {}, not negative, received {}", name, unit, f),
_ => panic!("{} must be a whole number of {}, not negative, received a non-number", name, unit),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
let co2 = whole(&args[2], "co2", "g/km");
let range = whole(&args[3], "electricRangeMiles", "miles");
let unavailable = match &args[7] {
Value::Int(i) => *i,
Value::Float(f) if f.fract() == 0.0 => *f as i64,
other => panic!(
"unavailableDays must be a whole number from 0 to 366, received {}",
match other {
Value::Float(f) => f.to_string(),
_ => "a non-number".to_string(),
}
),
};
company_car_bik_to_value(&company_car_bik(
&money_from_value(&args[0]),
&money_from_value(&args[1]),
co2,
range,
args[4].as_bool(),
args[5].as_str(),
args[6].as_str(),
unavailable,
&money_from_value(&args[8]),
args[9].as_bool(),
))
}