Functional Weave
Code in TypeScript

charity.higher-rate-relief@1.0.0

impl/rust.rs

5,291 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::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(&region) {
        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(),
    ))
}