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::payroll_pension_auto_enrolment_data::{
AE_MINIMUMS, AE_MINIMUMS_HISTORY, AE_MINIMUMS_HORIZON, AE_THRESHOLDS, AE_THRESHOLDS_HISTORY,
AE_THRESHOLDS_HORIZON,
};
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 missing(what: &str, on_date: &str, history: &str, horizon: Option<&str>) -> ! {
if let Some(horizon) = horizon {
if history != "full" && on_date < horizon {
panic!(
"{} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
what, on_date, history, horizon
);
}
}
panic!("{} on {}", what, on_date)
}
fn check_rate(name: &str, value: Option<i64>) {
if let Some(v) = value {
if v < 0 {
panic!("{} must be a whole number of basis points, 0 or more, received {}", name, v);
}
}
}
fn in_force(valid_from: &str, valid_to: Option<&str>, on_date: &str) -> bool {
on_date >= valid_from && valid_to.map_or(true, |to| on_date <= to)
}
/// Workplace pension contributions for one pay reference period.
///
/// The band is applied with the published per-period figures (£520 to £4,189 a
/// month, £120 to £967 a week), never the annual band divided on the fly. Each
/// contribution is rounded once to the nearest penny, half up; under relief at
/// source the deduction from pay is then 80% of the gross contribution, again
/// rounded half up, and the difference is the relief the provider claims.
///
/// # Panics
/// Panics on non-GBP or negative earnings, negative rates, an unknown basis,
/// arrangement or frequency, or a date no thresholds cover.
pub fn pension_contributions(earnings: &Money, frequency: &str, scheme: &PensionScheme, pay_date: &str) -> PensionContributions {
if earnings.currency != "GBP" {
panic!("pension contributions must be in GBP, received {}", earnings.currency);
}
if earnings.minor < 0 {
panic!("earnings must not be negative, received {}", earnings.minor);
}
check_rate("employeeBasisPoints", scheme.employee_basis_points);
check_rate("employerBasisPoints", scheme.employer_basis_points);
if !matches!(scheme.earnings_basis.as_str(), "qualifying-earnings" | "all-earnings") {
panic!("unknown earnings basis \"{}\"", scheme.earnings_basis);
}
if !matches!(scheme.arrangement.as_str(), "relief-at-source" | "net-pay") {
panic!("unknown relief arrangement \"{}\"", scheme.arrangement);
}
if !matches!(frequency, "weekly" | "fortnightly" | "four-weekly" | "monthly") {
panic!("unknown pay frequency \"{}\"", frequency);
}
if !is_iso_date(pay_date) {
panic!("payDate must be an ISO date (YYYY-MM-DD), received \"{}\"", pay_date);
}
let t = match AE_THRESHOLDS.iter().find(|r| in_force(r.valid_from, r.valid_to, pay_date)) {
Some(t) => t,
None => missing("no auto-enrolment earnings thresholds", pay_date, AE_THRESHOLDS_HISTORY, AE_THRESHOLDS_HORIZON),
};
let m = match AE_MINIMUMS.iter().find(|r| in_force(r.valid_from, r.valid_to, pay_date)) {
Some(m) => m,
None => missing("no auto-enrolment minimum contributions", pay_date, AE_MINIMUMS_HISTORY, AE_MINIMUMS_HORIZON),
};
let (lower, trigger, upper) = match frequency {
"weekly" => (t.lower_weekly, t.trigger_weekly, t.upper_weekly),
"fortnightly" => (t.lower_fortnightly, t.trigger_fortnightly, t.upper_fortnightly),
"four-weekly" => (t.lower_four_weekly, t.trigger_four_weekly, t.upper_four_weekly),
_ => (t.lower_monthly, t.trigger_monthly, t.upper_monthly),
};
// Qualifying earnings are the part of pay above the lower level and not
// above the upper one (Pensions Act 2008 s.13).
let pensionable = if scheme.earnings_basis == "all-earnings" {
earnings.minor
} else {
(earnings.minor.min(upper) - lower).max(0)
};
let employer_rate = scheme.employer_basis_points.unwrap_or(m.employer_basis_points);
let employee_rate = scheme
.employee_basis_points
.unwrap_or_else(|| (m.total_basis_points - employer_rate).max(0));
let employee_gross = round_div(pensionable * employee_rate, 10000, "half-up");
let employer = round_div(pensionable * employer_rate, 10000, "half-up");
let net_pay = scheme.arrangement == "net-pay";
let deduction = if net_pay {
employee_gross
} else {
round_div(employee_gross * (10000 - m.relief_at_source_basis_points), 10000, "half-up")
};
PensionContributions {
pensionable_earnings: money(pensionable, "GBP"),
employee_gross: money(employee_gross, "GBP"),
employee_deduction: money(deduction, "GBP"),
employer: money(employer, "GBP"),
deduct_before_tax: net_pay,
earnings_trigger_met: earnings.minor > trigger,
}
}
fn optional_int(v: &Value) -> Option<i64> {
if v.is_null() {
None
} else {
Some(v.as_i64())
}
}
pub fn pension_scheme_from_value(v: &Value) -> PensionScheme {
PensionScheme {
employee_basis_points: optional_int(v.get("employeeBasisPoints")),
employer_basis_points: optional_int(v.get("employerBasisPoints")),
earnings_basis: v.get("earningsBasis").as_str().to_string(),
arrangement: v.get("arrangement").as_str().to_string(),
}
}
pub fn pension_contributions_to_value(p: &PensionContributions) -> Value {
Value::obj(vec![
("pensionableEarnings", money_to_value(&p.pensionable_earnings)),
("employeeGross", money_to_value(&p.employee_gross)),
("employeeDeduction", money_to_value(&p.employee_deduction)),
("employer", money_to_value(&p.employer)),
("deductBeforeTax", Value::Bool(p.deduct_before_tax)),
("earningsTriggerMet", Value::Bool(p.earnings_trigger_met)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
pension_contributions_to_value(&pension_contributions(
&money_from_value(&args[0]),
args[1].as_str(),
&pension_scheme_from_value(&args[2]),
args[3].as_str(),
))
}