Functional Weave
Code in Rust

invest.pension-annual-allowance@1.0.0

impl/python.py

7,644 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.

from typing import Any, Dict, List, Sequence, Tuple

from .dates_uk_tax_year import uk_tax_year  ← from dates.uk-tax-year ^1.0.0 · built alongside by fune
from .invest_pension_annual_allowance_data import ANNUAL_ALLOWANCE_RULES, ANNUAL_ALLOWANCE_RULES_HISTORY, ANNUAL_ALLOWANCE_RULES_HORIZON  ← this capability’s own data, compiled from data/annual-allowance.json into the same file by fune build
from .invest_pension_annual_allowance_types import AnnualAllowanceResult, CarryForwardYear, PensionInputYear
from .money_amount import Money  ← from money.amount ^1.0.0 · built alongside by fune


def _gbp(minor: int) -> Money:
    return Money(minor=minor, currency="GBP")


def _check_money(value: Money, name: str, label: str) -> int:
    if value.currency != "GBP":
        raise ValueError("%s must be in GBP, received %s for %s" % (name, value.currency, label))
    minor = value.minor
    if isinstance(minor, bool) or not isinstance(minor, int) or minor < 0:
        raise ValueError(
            "%s must be a whole number of pence, not negative, received %s for %s" % (name, minor, label)
        )
    return minor


def _rule_for(start_year: int) -> Tuple[Any, str]:
    start = "%04d-04-06" % (start_year,)
    label = uk_tax_year(start).label
    for rule in ANNUAL_ALLOWANCE_RULES:
        if rule.valid_from <= start and (rule.valid_to is None or start <= rule.valid_to):
            return rule, label
    why = ""
    horizon = ANNUAL_ALLOWANCE_RULES_HORIZON
    if horizon is not None and start < horizon:
        why = (
            ": this build was installed with history=%s, so it only carries rules from %s. "
            "Reinstall with history=full for earlier years." % (ANNUAL_ALLOWANCE_RULES_HISTORY, horizon)
        )
    raise ValueError("no annual allowance rules for the %s tax year%s" % (label, why))


def _test(start_year: int, year: PensionInputYear, carry_forward: int) -> Dict[str, Any]:
    """How one tax year comes out against its own allowance, given the
    carry-forward it can reach."""
    rule, label = _rule_for(start_year)
    mp = _check_money(year.money_purchase_input, "moneyPurchaseInput", label)
    other = _check_money(year.other_input, "otherInput", label)
    threshold = _check_money(year.threshold_income, "thresholdIncome", label)
    adjusted = _check_money(year.adjusted_income, "adjustedIncome", label)
    total = mp + other
    if not year.member and total > 0:
        raise ValueError("pension inputs in %s need membership of a registered pension scheme in that year" % (label,))

    # Taper: 1 pound off for every 2 of adjusted income over the limit, rounded
    # down to a whole pound, when threshold income is also over its limit.
    allowance = rule.annual_allowance
    tapered = threshold > rule.threshold_income and adjusted > rule.adjusted_income
    if tapered:
        reduction = (adjusted - rule.adjusted_income) // 200 * 100
        allowance = max(rule.minimum_tapered_allowance, allowance - reduction)

    out: Dict[str, Any] = {"label": label, "standard": rule.annual_allowance, "allowance": allowance, "tapered": tapered, "total": total}
    default_used = min(carry_forward, max(0, total - allowance))
    default_excess = max(0, total - allowance - default_used)
    if year.mpaa_applies and mp > rule.money_purchase_allowance:
        # Carry-forward can top up the alternative allowance, never the MPAA.
        alternative = max(0, allowance - rule.money_purchase_allowance)
        alt_used = min(carry_forward, max(0, other - alternative))
        alt_excess = mp - rule.money_purchase_allowance + max(0, other - alternative - alt_used)
        use_alternative = alt_excess > default_excess
        out.update(
            mpaa=rule.money_purchase_allowance,
            alternative=alternative,
            basis="alternative" if use_alternative else "standard",
            carry_forward_used=alt_used if use_alternative else default_used,
            excess=alt_excess if use_alternative else default_excess,
            unused=max(0, alternative - other) if year.member else 0,
            tested_allowance=alternative,
            tested_input=other,
        )
        return out
    out.update(
        mpaa=rule.money_purchase_allowance if year.mpaa_applies else None,
        alternative=None,
        basis="standard",
        carry_forward_used=default_used,
        excess=default_excess,
        unused=max(0, allowance - total) if year.member else 0,
        tested_allowance=allowance,
        tested_input=total,
    )
    return out


def _available(entry: Dict[str, Any]) -> int:
    return entry["unused"] - entry["used_by_later_years"] - entry["used_this_year"]


def _consume(entries: List[Dict[str, Any]], amount: int, field: str) -> None:
    """Take `amount` from the entries' unused allowance, earliest year first."""
    left = amount
    for entry in entries:
        take = min(left, _available(entry))
        entry[field] += take
        left -= take


def pension_annual_allowance(
    tax_year_date: str, current: PensionInputYear, previous_years: Sequence[PensionInputYear]
) -> AnnualAllowanceResult:
    """The annual allowance for a UK tax year, and how much of the year's
    pension input amounts is subject to the annual allowance charge.

    The earlier years are replayed oldest first, because an excess in one of
    them uses up unused allowance from the years before it (earliest first),
    and that is allowance the year being tested can no longer carry forward.
    A year's own allowance is used before any carried forward; carry-forward
    is then taken from the earliest of the three previous years first.
    """
    year = uk_tax_year(tax_year_date)
    start_year = int(year.start[:4])
    if len(previous_years) < 3:
        raise ValueError(
            "previousYears must hold at least the three tax years before this one, oldest first, received %d"
            % (len(previous_years),)
        )
    ledger: List[Dict[str, Any]] = []
    for i, previous in enumerate(previous_years):
        window = ledger[max(0, i - 3) : i]
        result = _test(start_year - len(previous_years) + i, previous, sum(_available(e) for e in window))
        _consume(window, result["carry_forward_used"], "used_by_later_years")
        ledger.append(
            {
                "tax_year": result["label"],
                "allowance": result["tested_allowance"],
                "input": result["tested_input"],
                "unused": result["unused"],
                "used_by_later_years": 0,
                "used_this_year": 0,
            }
        )
    window = ledger[-3:]
    carry_forward_available = sum(_available(e) for e in window)
    result = _test(start_year, current, carry_forward_available)
    _consume(window, result["carry_forward_used"], "used_this_year")

    carry_forward = [
        CarryForwardYear(
            tax_year=e["tax_year"],
            allowance=_gbp(e["allowance"]),
            input=_gbp(e["input"]),
            unused=_gbp(e["unused"]),
            used_by_later_years=_gbp(e["used_by_later_years"]),
            used_this_year=_gbp(e["used_this_year"]),
        )
        for e in window
    ]
    return AnnualAllowanceResult(
        tax_year=result["label"],
        standard_allowance=_gbp(result["standard"]),
        allowance=_gbp(result["allowance"]),
        tapered=result["tapered"],
        money_purchase_allowance=None if result["mpaa"] is None else _gbp(result["mpaa"]),
        alternative_allowance=None if result["alternative"] is None else _gbp(result["alternative"]),
        total_input=_gbp(result["total"]),
        basis=result["basis"],
        carry_forward=carry_forward,
        carry_forward_available=_gbp(carry_forward_available),
        carry_forward_used=_gbp(result["carry_forward_used"]),
        excess=_gbp(result["excess"]),
    )