import re from .charity_gift_aid_data import BASIC_RATE, BASIC_RATE_HISTORY from .charity_gift_aid_types import GiftAid from .math_round_div import round_div from .money_amount import Money, money ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}") def _basic_rate(on_date: str) -> int: for row in BASIC_RATE: if row.valid_from <= on_date and (row.valid_to is None or on_date <= row.valid_to): return row.basis_points # A history=current build carries only the rate in force; answering an old # gift with today's rate would be a quiet wrong claim, so say why instead. if BASIC_RATE_HISTORY != "full" and len(BASIC_RATE) > 0: earliest = min(row.valid_from for row in BASIC_RATE) if on_date < earliest: raise ValueError( "no Gift Aid basic rate on %s: this build was installed with history=%s, so it only carries rules from %s. " "Reinstall with history=full for earlier tax years." % (on_date, BASIC_RATE_HISTORY, earliest) ) raise ValueError("no Gift Aid basic rate on %s" % (on_date,)) def gift_aid(donation: Money, on_date: str) -> GiftAid: """The tax a charity reclaims on a Gift Aid donation. The donor is treated as having given the gift after deducting basic rate tax (ITA 2007 s414), so the charity reclaims donation x rate / (100% - rate): 25p per £1 at 20%. It is always the UK basic rate, for Scottish and Welsh taxpayers too. """ if donation.currency != "GBP": raise ValueError("Gift Aid is claimed in GBP, received %s" % (donation.currency,)) if donation.minor < 0: raise ValueError("donation must not be negative, received %d" % (donation.minor,)) if not isinstance(on_date, str) or not ISO_DATE.fullmatch(on_date): raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,)) rate = _basic_rate(on_date) # Rounded down: a claim may never exceed the tax treated as deducted. tax = round_div(donation.minor * rate, 10000 - rate, "down") return GiftAid( donation=money(donation.minor, "GBP"), tax_reclaimable=money(tax, "GBP"), gross=money(donation.minor + tax, "GBP"), basic_rate_basis_points=rate, )