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_national_insurance_data::{
NiPrimaryRate, NiSecondaryRate, NiThresholds, NI_PRIMARY_RATES, NI_PRIMARY_RATES_HISTORY,
NI_PRIMARY_RATES_HORIZON, NI_SECONDARY_RATES, NI_SECONDARY_RATES_HISTORY,
NI_SECONDARY_RATES_HORIZON, NI_THRESHOLDS, NI_THRESHOLDS_HISTORY, NI_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 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)
}
// A pruned build must refuse a date it no longer carries rules for rather than
// answer it with a later year's rates.
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)
}
/// Earnings falling in (lower, upper]; upper None means no ceiling.
fn slice(earnings: i64, lower: i64, upper: Option<i64>) -> i64 {
let top = match upper {
Some(u) => earnings.min(u),
None => earnings,
};
(top - lower).max(0)
}
/// (lel, pt, st, fust, ust, uel) for the earnings period.
fn limits(t: &NiThresholds, frequency: &str) -> [i64; 6] {
match frequency {
"annual" => [t.lel_annual, t.pt_annual, t.st_annual, t.fust_annual, t.ust_annual, t.uel_annual],
"monthly" => [t.lel_monthly, t.pt_monthly, t.st_monthly, t.fust_monthly, t.ust_monthly, t.uel_monthly],
_ => {
// HMRC's CA38: for pay in multiples of a week, work on the weekly
// figures and multiply by the number of weeks.
let k = match frequency {
"weekly" => 1,
"fortnightly" => 2,
"four-weekly" => 4,
other => panic!("unknown pay frequency \"{}\"", other),
};
[
t.lel_weekly * k,
t.pt_weekly * k,
t.st_weekly * k,
t.fust_weekly * k,
t.ust_weekly * k,
t.uel_weekly * k,
]
}
}
}
// Regulation 12(1) SSCR 2001: primary and secondary are worked out separately
// and each total is rounded to the nearest penny, a half penny going up.
fn contributions(earnings: i64, l: &[i64; 6], p: &NiPrimaryRate, s: &NiSecondaryRate) -> (i64, i64) {
let [_lel, pt, st, fust, ust, uel] = *l;
let primary = slice(earnings, pt, Some(uel)) * p.pt_to_uel_basis_points
+ slice(earnings, uel, None) * p.above_uel_basis_points;
let secondary = slice(earnings, st, Some(fust)) * s.st_to_fust_basis_points
+ slice(earnings, st.max(fust), Some(ust)) * s.fust_to_ust_basis_points
+ slice(earnings, st.max(ust), None) * s.above_ust_basis_points;
(
round_div(primary, 10000, "half-up"),
round_div(secondary, 10000, "half-up"),
)
}
/// Class 1 National Insurance on one payment, by the exact percentage method.
///
/// With `director` None this is the ordinary earnings-period calculation. With
/// it, the director's annual earnings period: contributions on everything paid
/// so far this tax year at the annual thresholds, less what has already been
/// paid, so the amount can go down (or negative) as well as up.
///
/// # Panics
/// Panics on non-GBP or negative earnings, an unknown frequency or category,
/// or a date no rules cover.
pub fn national_insurance(
earnings: &Money,
category: &str,
frequency: &str,
pay_date: &str,
director: Option<&DirectorNi>,
) -> NationalInsurance {
if earnings.currency != "GBP" {
panic!("National Insurance must be in GBP, received {}", earnings.currency);
}
if earnings.minor < 0 {
panic!("earnings must not be negative, received {}", earnings.minor);
}
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 thresholds = match NI_THRESHOLDS.iter().find(|t| in_force(t.valid_from, t.valid_to, pay_date)) {
Some(t) => t,
None => missing("no National Insurance thresholds", pay_date, NI_THRESHOLDS_HISTORY, NI_THRESHOLDS_HORIZON),
};
let primary_for = |basis: &str| {
NI_PRIMARY_RATES
.iter()
.find(|r| r.category == category && r.basis == basis && in_force(r.valid_from, r.valid_to, pay_date))
};
// Directors only have rates of their own in a year the main rate changed
// mid-year (2023-24); otherwise they pay the ordinary rates.
let director_rate = if director.is_some() { primary_for("director") } else { None };
let primary = match director_rate.or_else(|| primary_for("standard")) {
Some(r) => r,
None => missing(
&format!("no National Insurance rates for category {}", category),
pay_date,
NI_PRIMARY_RATES_HISTORY,
NI_PRIMARY_RATES_HORIZON,
),
};
let secondary = match NI_SECONDARY_RATES
.iter()
.find(|r| r.category == category && in_force(r.valid_from, r.valid_to, pay_date))
{
Some(r) => r,
None => missing(
&format!("no National Insurance rates for category {}", category),
pay_date,
NI_SECONDARY_RATES_HISTORY,
NI_SECONDARY_RATES_HORIZON,
),
};
match director {
None => {
let l = limits(thresholds, frequency);
let (employee, employer) = contributions(earnings.minor, &l, primary, secondary);
NationalInsurance {
employee: money(employee, "GBP"),
employer: money(employer, "GBP"),
lower_earnings_limit_reached: earnings.minor >= l[0],
}
}
Some(d) => {
for (name, value) in [
("previousEarnings", &d.previous_earnings),
("previousEmployee", &d.previous_employee),
("previousEmployer", &d.previous_employer),
] {
if value.currency != "GBP" {
panic!("National Insurance must be in GBP, received {} for {}", value.currency, name);
}
}
let cumulative = d.previous_earnings.minor + earnings.minor;
if cumulative < 0 {
panic!("earnings must not be negative, received {} to date", cumulative);
}
let l = limits(thresholds, "annual");
let (employee, employer) = contributions(cumulative, &l, primary, secondary);
NationalInsurance {
employee: money(employee - d.previous_employee.minor, "GBP"),
employer: money(employer - d.previous_employer.minor, "GBP"),
lower_earnings_limit_reached: cumulative >= l[0],
}
}
}
}
pub fn director_ni_from_value(v: &Value) -> DirectorNi {
DirectorNi {
previous_earnings: money_from_value(v.get("previousEarnings")),
previous_employee: money_from_value(v.get("previousEmployee")),
previous_employer: money_from_value(v.get("previousEmployer")),
}
}
pub fn national_insurance_to_value(ni: &NationalInsurance) -> Value {
Value::obj(vec![
("employee", money_to_value(&ni.employee)),
("employer", money_to_value(&ni.employer)),
("lowerEarningsLimitReached", Value::Bool(ni.lower_earnings_limit_reached)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let director = if args[4].is_null() { None } else { Some(director_ni_from_value(&args[4])) };
national_insurance_to_value(&national_insurance(
&money_from_value(&args[0]),
args[1].as_str(),
args[2].as_str(),
args[3].as_str(),
director.as_ref(),
))
}