Functional Weave
Code in Rust

payroll.income-tax@1.0.0

impl/rust.rs

10,693 bytes · the Rust implementation · view raw

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