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::charity_gift_aid::gift_aid; ← from charity.gift-aid ^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}; ← payroll.income-tax’s rule data (^1.0.0) · built alongside by fune
const REGIONS: [&str; 3] = ["rest-of-uk", "scotland", "wales"];
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 && b.valid_from <= on_date && b.valid_to.map_or(true, |to| on_date <= to))
.collect();
if rows.is_empty() {
// A history=current build only carries this year's bands; an older year
// would otherwise be answered with the wrong bands.
if INCOME_TAX_BANDS_HISTORY != "full" {
if let Some(earliest) = INCOME_TAX_BANDS.iter().filter(|b| b.region == region).map(|b| b.valid_from).min() {
if on_date < earliest {
panic!(
"no income tax bands for {} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
region, on_date, INCOME_TAX_BANDS_HISTORY, earliest
);
}
}
}
panic!("no income tax bands for {} on {}", region, on_date);
}
rows.sort_by_key(|b| b.band);
rows
}
fn basic_band_index(bands: &[&IncomeTaxBand], region: &str, on_date: &str) -> usize {
match bands.iter().position(|b| b.basic_rate) {
Some(i) => i,
None => panic!("no basic rate band for {} on {}", region, on_date),
}
}
/// Tax on income in pence, times 10000 (pence x basis points), unrounded.
fn scaled_tax(income: i64, bands: &[&IncomeTaxBand], extend_from: usize, extension: i64) -> i64 {
let mut total = 0i64;
let mut lower = 0i64;
for (i, band) in bands.iter().enumerate() {
if income <= lower {
break;
}
let upper = band.up_to.map(|u| u * 100 + if i >= extend_from { extension } else { 0 });
let top = match upper {
Some(u) if income >= u => u,
_ => income,
};
if top > lower {
total += (top - lower) * band.basis_points;
}
match upper {
Some(u) => lower = u,
None => break,
}
}
total
}
/// The extra relief a Gift Aid donor gets through their own tax: ITA 2007
/// s414(2)(b) raises the basic rate limit and the higher rate limit (for a
/// Scottish taxpayer, the Scottish basic rate limit and every limit above it,
/// but not the starter rate limit) by the grossed up donations.
///
/// # Panics
/// Panics on non-GBP or negative amounts, an unknown region, a date with no
/// bands on file, or a devolved basic rate that differs from the UK one.
pub fn higher_rate_relief(taxable_income: &Money, donations: &Money, region: &str, on_date: &str) -> HigherRateRelief {
if taxable_income.currency != "GBP" {
panic!("taxableIncome must be in GBP, received {}", taxable_income.currency);
}
if taxable_income.minor < 0 {
panic!("taxableIncome must not be negative, received {}", taxable_income.minor);
}
if !REGIONS.contains(®ion) {
panic!("unknown tax region \"{}\": expected rest-of-uk, scotland or wales", region);
}
let grossed = gift_aid(donations, on_date);
let bands = bands_for(region, on_date);
let basic = basic_band_index(&bands, region, on_date);
let uk_bands = bands_for("rest-of-uk", on_date);
let uk_basic = uk_bands[basic_band_index(&uk_bands, "rest-of-uk", on_date)];
if bands[basic].basis_points != uk_basic.basis_points {
// s414A then adjusts the donor's tax by the rate difference; not modelled.
panic!(
"the {} basic rate differs from the UK basic rate on {}; the section 414A adjustment is not supported",
region, on_date
);
}
let before = scaled_tax(taxable_income.minor, &bands, basic, 0);
let after = scaled_tax(taxable_income.minor, &bands, basic, grossed.gross.minor);
let tax_before = before.div_euclid(10000);
let extra = (before - after).div_euclid(10000);
HigherRateRelief {
gross_donations: grossed.gross.clone(),
basic_rate_relief: grossed.tax_reclaimable.clone(),
tax_before: money(tax_before, "GBP"),
tax_after: money(tax_before - extra, "GBP"),
extra_relief: money(extra, "GBP"),
net_cost: money(donations.minor - extra, "GBP"),
}
}
pub fn higher_rate_relief_to_value(r: &HigherRateRelief) -> Value {
Value::obj(vec![
("grossDonations", money_to_value(&r.gross_donations)),
("basicRateRelief", money_to_value(&r.basic_rate_relief)),
("taxBefore", money_to_value(&r.tax_before)),
("taxAfter", money_to_value(&r.tax_after)),
("extraRelief", money_to_value(&r.extra_relief)),
("netCost", money_to_value(&r.net_cost)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
higher_rate_relief_to_value(&higher_rate_relief(
&money_from_value(&args[0]),
&money_from_value(&args[1]),
args[2].as_str(),
args[3].as_str(),
))
}