Functional Weave
Code in Rust

charity.higher-rate-relief@1.0.0

impl/python.py

4,195 bytes · the Python 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.

from typing import List, Optional

from .charity_gift_aid import gift_aid  ← from charity.gift-aid ^1.0.0 · built alongside by fune
from .charity_higher_rate_relief_types import HigherRateRelief, TaxRegion
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_income_tax_data import INCOME_TAX_BANDS, INCOME_TAX_BANDS_HISTORY, IncomeTaxBand  ← payroll.income-tax’s rule data (^1.0.0) · built alongside by fune

REGIONS = ("rest-of-uk", "scotland", "wales")


def _bands_for(region: str, on_date: str) -> List[IncomeTaxBand]:
    rows = [
        b for b in INCOME_TAX_BANDS
        if b.region == region and b.valid_from <= on_date and (b.valid_to is None or on_date <= b.valid_to)
    ]
    if not rows:
        # A history=current build only carries this year's bands; an older year
        # would otherwise be answered with the wrong bands.
        froms = [b.valid_from for b in INCOME_TAX_BANDS if b.region == region]
        if INCOME_TAX_BANDS_HISTORY != "full" and froms:
            earliest = min(froms)
            if on_date < earliest:
                raise ValueError(
                    "no income tax bands for %s on %s: this build was installed with history=%s, so it only carries rules from %s. "
                    "Reinstall with history=full for earlier tax years." % (region, on_date, INCOME_TAX_BANDS_HISTORY, earliest)
                )
        raise ValueError("no income tax bands for %s on %s" % (region, on_date))
    return sorted(rows, key=lambda b: b.band)


def _basic_band_index(bands: List[IncomeTaxBand], region: str, on_date: str) -> int:
    for i, b in enumerate(bands):
        if b.basic_rate:
            return i
    raise ValueError("no basic rate band for %s on %s" % (region, on_date))


def _scaled_tax(income: int, bands: List[IncomeTaxBand], extend_from: int, extension: int) -> int:
    """Tax on income in pence, times 10000 (pence x basis points), unrounded."""
    total = 0
    lower = 0
    for i, band in enumerate(bands):
        if income <= lower:
            break
        upper: Optional[int] = None if band.up_to is None else band.up_to * 100 + (extension if i >= extend_from else 0)
        top = income if upper is None or income < upper else upper
        if top > lower:
            total += (top - lower) * band.basis_points
        if upper is None:
            break
        lower = upper
    return total


def higher_rate_relief(taxable_income: Money, donations: Money, region: TaxRegion, on_date: str) -> HigherRateRelief:
    """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.
    """
    if taxable_income.currency != "GBP":
        raise ValueError("taxableIncome must be in GBP, received %s" % (taxable_income.currency,))
    if taxable_income.minor < 0:
        raise ValueError("taxableIncome must not be negative, received %d" % (taxable_income.minor,))
    if region not in REGIONS:
        raise ValueError('unknown tax region "%s": expected rest-of-uk, scotland or wales' % (region,))
    grossed = gift_aid(donations, on_date)
    bands = _bands_for(region, on_date)
    basic = _basic_band_index(bands, region, on_date)
    uk_bands = _bands_for("rest-of-uk", on_date)
    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.
        raise ValueError(
            "the %s basic rate differs from the UK basic rate on %s; the section 414A adjustment is not supported" % (region, on_date)
        )
    before = _scaled_tax(taxable_income.minor, bands, basic, 0)
    after = _scaled_tax(taxable_income.minor, bands, basic, grossed.gross.minor)
    tax_before = before // 10000
    extra = (before - after) // 10000
    return HigherRateRelief(
        gross_donations=grossed.gross,
        basic_rate_relief=grossed.tax_reclaimable,
        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"),
    )