from typing import List, Optional from .charity_gift_aid import gift_aid from .charity_higher_rate_relief_types import HigherRateRelief, TaxRegion from .money_amount import Money, money from .payroll_income_tax_data import INCOME_TAX_BANDS, INCOME_TAX_BANDS_HISTORY, IncomeTaxBand 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"), )