use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; 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; // 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(), )) }