import re from typing import NoReturn, Optional from .math_round_div import round_div from .money_amount import Money, money from .payroll_pension_auto_enrolment_data import AE_MINIMUMS, AE_MINIMUMS_HISTORY, AE_MINIMUMS_HORIZON, AE_THRESHOLDS, AE_THRESHOLDS_HISTORY, AE_THRESHOLDS_HORIZON 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, )