import re from typing import List from .charity_gift_aid_eligibility_data import BENEFIT_LIMITS, BENEFIT_LIMITS_HISTORY, BenefitLimit from .charity_gift_aid_eligibility_types import GiftAidEligibility from .money_amount import Money, money 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"), )