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_income_tax_data::{
IncomeTaxBand, INCOME_TAX_BANDS, INCOME_TAX_BANDS_HISTORY, OVERRIDING_LIMIT, OVERRIDING_LIMIT_HISTORY,
};
use super::payroll_tax_code_parse::parse_tax_code; ← from payroll.tax-code-parse ^1.0.0 · built alongside by fune
// HMRC's routines work "to 4 decimal places of a pound without correcting the
// final place". Holding amounts as integer ten-thousandths of a pound (1 =
// £0.0001, 100 = 1p) makes every one of those truncations an exact integer
// division.
const UNITS_PER_PENNY: i64 = 100;
const UNITS_PER_POUND: i64 = 10000;
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)
}
fn no_rule(what: &str, on_date: &str, history: &str, froms: &[&str]) -> ! {
// A build installed with history=current only carries the rules still in
// force; answering an older date with this year's bands would be a quiet,
// plausible wrong answer, so say why there is nothing instead.
if history != "full" {
if let Some(earliest) = froms.iter().min() {
if on_date < *earliest {
panic!(
"no {} 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, earliest
);
}
}
}
panic!("no {} on {}", what, on_date)
}
fn bands_for(region: &str, on_date: &str) -> Vec<&'static IncomeTaxBand> {
let mut rows: Vec<&'static IncomeTaxBand> = INCOME_TAX_BANDS
.iter()
.filter(|b| b.region == region && in_force(b.valid_from, b.valid_to, on_date))
.collect();
if rows.is_empty() {
let froms: Vec<&str> = INCOME_TAX_BANDS
.iter()
.filter(|b| b.region == region)
.map(|b| b.valid_from)
.collect();
no_rule(
&format!("income tax bands for {}", region),
on_date,
INCOME_TAX_BANDS_HISTORY,
&froms,
);
}
rows.sort_by_key(|b| b.band);
rows
}
fn overriding_limit(on_date: &str) -> i64 {
match OVERRIDING_LIMIT
.iter()
.find(|r| in_force(r.valid_from, r.valid_to, on_date))
{
Some(row) => row.basis_points,
None => {
let froms: Vec<&str> = OVERRIDING_LIMIT.iter().map(|r| r.valid_from).collect();
no_rule("PAYE overriding limit", on_date, OVERRIDING_LIMIT_HISTORY, &froms)
}
}
}
/// Free pay (or K-code additional pay) for one week or month, in pence: HMRC paragraph 4.3.1.
fn period_allowance(code_number: i64, periods_per_year: i64) -> i64 {
if code_number == 0 {
return 0;
}
// Codes above 500 are split into blocks of 500 and a remainder of 1-500,
// each rounded up to the penny separately; that is how the printed tables
// were built, and computing the whole code in one division is a penny out.
let blocks = (code_number - 1) / 500;
let remainder = (code_number - 1) % 500 + 1;
let block_value = if periods_per_year == 52 { 9616 } else { 41667 };
round_div((remainder * 10 + 9) * 100, periods_per_year, "up") + blocks * block_value
}
/// Tax due to date on positive taxable pay by the banded Tax Formulae of paragraph 4.4, in pence.
fn banded_tax(taxable_pence: i64, bands: &[&IncomeTaxBand], n: i64, periods_per_year: i64) -> i64 {
let taxable_pounds = round_div(taxable_pence, 100, "down");
let mut previous_threshold = 0i64;
let mut previous_threshold_tax = 0i64;
let mut lower_pounds = 0i64;
let mut cumulative_annual_tax = 0i64;
for band in bands {
if let Some(up_to) = band.up_to {
let threshold = round_div(up_to * UNITS_PER_POUND * n, periods_per_year, "down");
// The income test compares the unrounded pay with the threshold
// rounded UP to a whole pound (the round-pound limits of Tables C),
// while the formula itself uses the exact threshold.
let cvalue = round_div(threshold, UNITS_PER_POUND, "up");
if taxable_pence > cvalue * 100 {
cumulative_annual_tax += (up_to - lower_pounds) * band.basis_points;
lower_pounds = up_to;
previous_threshold = threshold;
previous_threshold_tax = round_div(cumulative_annual_tax * n, periods_per_year, "down");
continue;
}
}
let product = (taxable_pounds as i128 * UNITS_PER_POUND as i128 - previous_threshold as i128)
* band.basis_points as i128
/ 10000;
let at_this_rate = product as i64;
return round_div(previous_threshold_tax + at_this_rate, UNITS_PER_PENNY, "down");
}
panic!("income tax bands have no top band")
}
fn check_gbp(name: &str, amount: &Money) {
if amount.currency != "GBP" {
panic!(
"payroll amounts must be in GBP, received {} for {}",
amount.currency, name
);
}
}
/// PAYE income tax for one payment, following HMRC's "Specification for PAYE
/// tax table routines" (the computerised form of the tax tables, used by
/// payroll software): cumulative or week 1 / month 1, suffix, K, BR, D and NT
/// codes, Scottish and Welsh bands, and the 50% overriding limit.
///
/// # Panics
/// Panics on a bad code, period, currency or date, or a date with no bands.
pub fn income_tax(
tax_code: &str,
frequency: &str,
period: i64,
pay: &Money,
previous_pay_to_date: &Money,
previous_tax_to_date: &Money,
pay_date: &str,
) -> IncomeTax {
check_gbp("pay", pay);
check_gbp("previousPayToDate", previous_pay_to_date);
check_gbp("previousTaxToDate", previous_tax_to_date);
if !is_iso_date(pay_date) {
panic!(
"payDate must be an ISO date (YYYY-MM-DD), received \"{}\"",
pay_date
);
}
let (weeks_in_period, valid_period) = match frequency {
"monthly" => (1, (1..=12).contains(&period)),
"weekly" => (1, (1..=53).contains(&period)),
"fortnightly" => (2, (1..=52).contains(&period) || period == 54),
"four-weekly" => (4, (1..=52).contains(&period) || period == 56),
other => panic!("unknown pay frequency \"{}\"", other),
};
if !valid_period {
panic!("period {} is not a tax period for {} pay", period, frequency);
}
let code = parse_tax_code(tax_code);
// Looked up even when no band is needed (NT, pay under the allowance), so
// a date outside the tax years on file is always refused.
let bands = bands_for(&code.region, pay_date);
let periods_per_year = if frequency == "monthly" { 12 } else { 52 };
// Weeks 53, 54 and 56 are always taxed on a week 1 basis (paragraph 14),
// using the week 1, 2 or 4 figures.
let cumulative = code.cumulative && period <= 52;
let n = if cumulative { period } else { weeks_in_period };
let pay_to_date_for_tax = if cumulative {
previous_pay_to_date.minor + pay.minor
} else {
pay.minor
};
let mut liability = 0i64;
let mut allowance_to_date = 0i64;
let mut taxable_pay = 0i64;
if code.kind == "allowance" || code.kind == "negative-allowance" {
let per_period = period_allowance(code.number, periods_per_year) * n;
allowance_to_date = if code.kind == "allowance" { per_period } else { -per_period };
let taxable_pence = pay_to_date_for_tax - allowance_to_date;
if taxable_pence > 0 {
taxable_pay = round_div(taxable_pence, 100, "down") * 100;
liability = banded_tax(taxable_pence, &bands, n, periods_per_year);
}
} else if code.kind == "basic-rate" || code.kind == "d-rate" {
let basic = bands.iter().position(|b| b.basic_rate);
let index = basic.map(|i| if code.kind == "basic-rate" { i } else { i + 1 + code.number as usize });
let band = match index {
Some(i) if i < bands.len() => bands[i],
_ => panic!("no {} rate for {} on {}", code.code, code.region, pay_date),
};
let pounds = if pay_to_date_for_tax > 0 {
round_div(pay_to_date_for_tax, 100, "down")
} else {
0
};
taxable_pay = pounds * 100;
liability = round_div(pounds * band.basis_points, 100, "down");
}
// The overriding limit: no more than 50% of this payment may go in tax. It
// never restricts a refund, and on negative pay it is zero (paragraph 4.5.4).
let limit = if pay.minor > 0 {
round_div(pay.minor * overriding_limit(pay_date), 10000, "down")
} else {
0
};
let due = if cumulative {
liability - previous_tax_to_date.minor
} else {
liability
};
let tax = if due > limit { limit } else { due };
IncomeTax {
tax: money(tax, "GBP"),
tax_to_date: money(previous_tax_to_date.minor + tax, "GBP"),
pay_to_date: money(previous_pay_to_date.minor + pay.minor, "GBP"),
allowance_to_date: money(allowance_to_date, "GBP"),
taxable_pay: money(taxable_pay, "GBP"),
cumulative,
limit_applied: due > limit,
tax_not_deducted: money(if due > limit { due - limit } else { 0 }, "GBP"),
}
}
pub fn income_tax_to_value(t: &IncomeTax) -> Value {
Value::obj(vec![
("tax", money_to_value(&t.tax)),
("taxToDate", money_to_value(&t.tax_to_date)),
("payToDate", money_to_value(&t.pay_to_date)),
("allowanceToDate", money_to_value(&t.allowance_to_date)),
("taxablePay", money_to_value(&t.taxable_pay)),
("cumulative", Value::Bool(t.cumulative)),
("limitApplied", Value::Bool(t.limit_applied)),
("taxNotDeducted", money_to_value(&t.tax_not_deducted)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
if let Value::Float(_) = args[2] {
panic!("period {} is not a tax period for {} pay", args[2].as_f64(), args[1].as_str());
}
income_tax_to_value(&income_tax(
args[0].as_str(),
args[1].as_str(),
args[2].as_i64(),
&money_from_value(&args[3]),
&money_from_value(&args[4]),
&money_from_value(&args[5]),
args[6].as_str(),
))
}