charity.gift-aid-eligibility@1.0.2
impl/python.py
3,575 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.
import re
from typing import List
from .charity_gift_aid_eligibility_data import BENEFIT_LIMITS, BENEFIT_LIMITS_HISTORY, BenefitLimit ← this capability’s own data, compiled from data/benefit-limits.json into the same file by fune build
from .charity_gift_aid_eligibility_types import GiftAidEligibility
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
def _limits_on(on_date: str) -> BenefitLimit:
for row in BENEFIT_LIMITS:
if row.valid_from <= on_date and (row.valid_to is None or on_date <= row.valid_to):
return row
if BENEFIT_LIMITS_HISTORY != "full" and len(BENEFIT_LIMITS) > 0:
earliest = min(row.valid_from for row in BENEFIT_LIMITS)
if on_date < earliest:
raise ValueError(
"no Gift Aid benefit limits 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, BENEFIT_LIMITS_HISTORY, earliest)
)
raise ValueError("no Gift Aid benefit limits on %s" % (on_date,))
def _check_gbp(name: str, amount: Money) -> None:
if amount.currency != "GBP":
raise ValueError("Gift Aid amounts must be in GBP, received %s for %s" % (amount.currency, name))
def gift_aid_eligibility(
donation: Money,
benefit_value: Money,
prior_benefits_this_year: Money,
has_declaration: bool,
donor_pays_enough_tax: bool,
on_date: str,
) -> GiftAidEligibility:
"""Whether a donation qualifies for Gift Aid (ITA 2007 ss416-418). The
benefit test is done on the exact limit, scaled by 10000, so 25% or 5% of
an odd number of pence is never rounded before the comparison.
"""
_check_gbp("donation", donation)
_check_gbp("benefitValue", benefit_value)
_check_gbp("priorBenefitsThisYear", prior_benefits_this_year)
if donation.minor <= 0:
raise ValueError("donation must be positive, received %d" % (donation.minor,))
if benefit_value.minor < 0:
raise ValueError("benefitValue must not be negative, received %d" % (benefit_value.minor,))
if prior_benefits_this_year.minor < 0:
raise ValueError("priorBenefitsThisYear must not be negative, received %d" % (prior_benefits_this_year.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,))
rule = _limits_on(on_date)
# The variable limit, in pence x 10000.
if donation.minor <= rule.small_gift_up_to:
scaled_limit = donation.minor * rule.small_gift_basis_points
elif rule.middle_gift_up_to is not None and rule.middle_gift_fixed is not None and donation.minor <= rule.middle_gift_up_to:
scaled_limit = rule.middle_gift_fixed * 10000
else:
scaled_limit = rule.excess_fixed * 10000 + (donation.minor - rule.excess_from) * rule.excess_basis_points
reasons: List[str] = []
if not has_declaration:
reasons.append("no-declaration")
if not donor_pays_enough_tax:
reasons.append("insufficient-tax")
# "exceeds" and "is more than": a benefit exactly at a limit is allowed.
if benefit_value.minor * 10000 > scaled_limit:
reasons.append("benefit-over-limit")
if benefit_value.minor + prior_benefits_this_year.minor > rule.annual_limit:
reasons.append("annual-benefits-over-limit")
return GiftAidEligibility(
eligible=len(reasons) == 0,
reasons=reasons,
benefit_limit=money(scaled_limit // 10000, "GBP"),
annual_benefit_limit=money(rule.annual_limit, "GBP"),
)