Functional Weave
Code in Python

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"),
    )