use super::funejson::Value; use super::charity_gift_aid::gift_aid; 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}; 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(), )) }