Functional Weave
Code in Rust

payroll.pension-auto-enrolment@1.0.1

impl/python.py

4,976 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 NoReturn, Optional

from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_pension_auto_enrolment_data import AE_MINIMUMS, AE_MINIMUMS_HISTORY, AE_MINIMUMS_HORIZON, AE_THRESHOLDS, AE_THRESHOLDS_HISTORY, AE_THRESHOLDS_HORIZON  ← this capability’s own data, compiled from data/ae-thresholds.json into the same file by fune build
from .payroll_pension_auto_enrolment_types import PensionContributions, PensionScheme
from .payroll_tax_period_types import PayFrequency

ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")


def _missing(what: str, on_date: str, history: str, horizon: Optional[str]) -> NoReturn:
    if history != "full" and horizon is not None and on_date < horizon:
        raise ValueError(
            "%s on %s: this build was installed with history=%s, so it only carries rules from %s. "
            "Reinstall with history=full for earlier tax years." % (what, on_date, history, horizon)
        )
    raise ValueError("%s on %s" % (what, on_date))


def _check_rate(name: str, value: Optional[int]) -> None:
    if value is not None and (isinstance(value, bool) or not isinstance(value, int) or value < 0):
        raise ValueError("%s must be a whole number of basis points, 0 or more, received %s" % (name, value))


def _in_force(r, on_date: str) -> bool:
    return on_date >= r.valid_from and (r.valid_to is None or on_date <= r.valid_to)


def pension_contributions(earnings: Money, frequency: PayFrequency, scheme: PensionScheme, pay_date: str) -> PensionContributions:
    """Workplace pension contributions for one pay reference period.

    The band is applied with the published per-period figures (£520 to £4,189
    a month, £120 to £967 a week), never the annual band divided on the fly.
    Each contribution is rounded once to the nearest penny, half up; under
    relief at source the deduction from pay is then 80% of the gross
    contribution, again rounded half up, and the difference is the relief the
    provider claims.
    """
    if earnings.currency != "GBP":
        raise ValueError("pension contributions must be in GBP, received %s" % (earnings.currency,))
    if earnings.minor < 0:
        raise ValueError("earnings must not be negative, received %s" % (earnings.minor,))
    _check_rate("employeeBasisPoints", scheme.employee_basis_points)
    _check_rate("employerBasisPoints", scheme.employer_basis_points)
    if scheme.earnings_basis not in ("qualifying-earnings", "all-earnings"):
        raise ValueError('unknown earnings basis "%s"' % (scheme.earnings_basis,))
    if scheme.arrangement not in ("relief-at-source", "net-pay"):
        raise ValueError('unknown relief arrangement "%s"' % (scheme.arrangement,))
    if frequency not in ("weekly", "fortnightly", "four-weekly", "monthly"):
        raise ValueError('unknown pay frequency "%s"' % (frequency,))
    if not isinstance(pay_date, str) or not ISO_DATE.fullmatch(pay_date):
        raise ValueError('payDate must be an ISO date (YYYY-MM-DD), received "%s"' % (pay_date,))

    t = next((r for r in AE_THRESHOLDS if _in_force(r, pay_date)), None)
    if t is None:
        _missing("no auto-enrolment earnings thresholds", pay_date, AE_THRESHOLDS_HISTORY, AE_THRESHOLDS_HORIZON)
    m = next((r for r in AE_MINIMUMS if _in_force(r, pay_date)), None)
    if m is None:
        _missing("no auto-enrolment minimum contributions", pay_date, AE_MINIMUMS_HISTORY, AE_MINIMUMS_HORIZON)

    lower, trigger, upper = {
        "weekly": (t.lower_weekly, t.trigger_weekly, t.upper_weekly),
        "fortnightly": (t.lower_fortnightly, t.trigger_fortnightly, t.upper_fortnightly),
        "four-weekly": (t.lower_four_weekly, t.trigger_four_weekly, t.upper_four_weekly),
        "monthly": (t.lower_monthly, t.trigger_monthly, t.upper_monthly),
    }[frequency]

    # Qualifying earnings are the part of pay above the lower level and not
    # above the upper one (Pensions Act 2008 s.13).
    if scheme.earnings_basis == "all-earnings":
        pensionable = earnings.minor
    else:
        pensionable = max(0, min(earnings.minor, upper) - lower)
    employer_rate = scheme.employer_basis_points if scheme.employer_basis_points is not None else m.employer_basis_points
    employee_rate = (
        scheme.employee_basis_points if scheme.employee_basis_points is not None else max(0, m.total_basis_points - employer_rate)
    )

    employee_gross = round_div(pensionable * employee_rate, 10000, "half-up")
    employer = round_div(pensionable * employer_rate, 10000, "half-up")
    net_pay = scheme.arrangement == "net-pay"
    deduction = employee_gross if net_pay else round_div(employee_gross * (10000 - m.relief_at_source_basis_points), 10000, "half-up")

    return PensionContributions(
        pensionable_earnings=money(pensionable, "GBP"),
        employee_gross=money(employee_gross, "GBP"),
        employee_deduction=money(deduction, "GBP"),
        employer=money(employer, "GBP"),
        deduct_before_tax=net_pay,
        earnings_trigger_met=earnings.minor > trigger,
    )