Functional Weave
Code in Python

payroll.statutory-sick-pay Unreviewed

UK Statutory Sick Pay for sickness spells: waiting days, 8-week linking, 28-week limit and the rate by date.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

Not professional advice. This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a payroll specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

Statutory Sick Pay (SSP) due from one employer for a run of sickness spells. Pass every spell of sickness in date order (earlier ones matter: they can link to later ones), the employee's qualifying days as ISO weekdays, and their normal weekly earnings. The answer is split into SSP weeks, which run Sunday to Saturday, because that is how the daily amount is set: the weekly rate divided by the number of qualifying days in the week.

## The rules, and when they changed

For example

  • statutory_sick_pay(periods ×1, 1, 2, 3, 4, 5, £500.00) → total £166.25, paid days 7, waiting days 3, weeks ×2, entitlement exhausted false two working weeks off in 2025-26: three waiting days, then the daily rate
  • statutory_sick_pay(periods ×1, 1, 2, 3, 4, 5, £500.00) → total £0.00, paid days 0, waiting days 0, weeks , entitlement exhausted false a three-day spell before April 2026 is not a period of incapacity and pays nothing
  • statutory_sick_pay(periods ×1, 1, 2, 3, 4, 5, £500.00) → total £73.95, paid days 3, waiting days 0, weeks ×1, entitlement exhausted false the same three-day spell after the 2026 reforms is paid from day one

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

def statutory_sick_pay(periods: Sequence[SicknessPeriod], qualifying_weekdays: Sequence[int], normal_weekly_earnings: Money) -> SickPay
periodsSicknessPeriod[]every spell of sickness with this employer, oldest first, including earlier spells that may link
qualifying_weekdaysint[]ISO weekdays the employee is required to work, 1 = Monday ... 7 = Sunday
normal_weekly_earningsMoneyaverage weekly earnings over the 8-week relevant period, worked out by the caller
returnsSickPay

The types it declares, generated into your project

@dataclass(frozen=True)
class SicknessPeriod:
    """A spell of days of incapacity for work, both ends inclusive."""

    start: str
    end: str

@dataclass(frozen=True)
class SickPayWeek:
    """What is due for one SSP week (Sunday to Saturday) under one set of rules."""

    #: the Sunday the week begins
    week_start: str
    #: qualifying days paid in this week
    paid_days: int
    #: qualifying days served as waiting days in this week
    waiting_days: int
    amount: Money

@dataclass(frozen=True)
class SickPay:
    total: Money
    paid_days: int
    waiting_days: int
    #: in date order; a week spanning a rate change has one line per rule
    weeks: List[SickPayWeek]
    #: some qualifying day went unpaid because 28 weeks or 3 years were used up
    entitlement_exhausted: bool

Your code names it in one line, in the file that uses it

from fune.payroll.statutory_sick_pay import statutory_sick_pay  # payroll.statutory-sick-pay@^1
impl/python.py · 144 lines · open · 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 List, Optional, Sequence

from .dates_add_days import add_days, days_between, parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months  ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_day_of_week import day_of_week  ← from dates.day-of-week ^1.0.0 · built alongside by fune
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_statutory_sick_pay_data import SSP_RULES, SSP_RULES_HISTORY, SspRule  ← this capability’s own data, compiled from data/ssp-rules.json into the same file by fune build
from .payroll_statutory_sick_pay_types import SicknessPeriod, SickPay, SickPayWeek

# Periods of incapacity separated by no more than 8 weeks are one period (SSCBA 1992 s.152(3)).
LINK_GAP_DAYS = 56
# 28 weeks' entitlement, counted in 1/420ths of a week so any 1-7 qualifying-day week divides exactly.
WEEK_UNITS = 420
MAX_UNITS = 28 * WEEK_UNITS


def _rule_for(date: str) -> int:
    for i, rule in enumerate(SSP_RULES):
        if date >= rule.valid_from and (rule.valid_to is None or date <= rule.valid_to):
            return i
    if SSP_RULES_HISTORY != "full":
        earliest = min((r.valid_from for r in SSP_RULES), default=None)
        if earliest is not None and date < earliest:
            raise ValueError(
                "no SSP rule for %s: this build was installed with history=%s, so it only carries rules from %s. "
                "Reinstall with history=full for older sickness." % (date, SSP_RULES_HISTORY, earliest)
            )
    raise ValueError("no SSP rule for %s" % (date,))


def statutory_sick_pay(periods: Sequence[SicknessPeriod], qualifying_weekdays: Sequence[int], normal_weekly_earnings: Money) -> SickPay:
    """Statutory Sick Pay due for a run of sickness spells.

    Every day is judged by the rules in force on that day, which is how the
    6 April 2026 reforms land: a spell straddling that date serves waiting days
    before it and is paid from the first qualifying day after it.
    """
    if len(qualifying_weekdays) == 0:
        raise ValueError("there must be at least one qualifying day a week")
    qualifying = set()
    for d in qualifying_weekdays:
        if isinstance(d, bool) or not isinstance(d, int) or d < 1 or d > 7:
            raise ValueError("qualifying weekdays must be 1 (Monday) to 7 (Sunday), received %s" % (d,))
        if d in qualifying:
            raise ValueError("qualifying weekday %s is listed twice" % (d,))
        qualifying.add(d)
    per_week = len(qualifying)
    if normal_weekly_earnings.currency != "GBP":
        raise ValueError("SSP is paid in GBP, received %s" % (normal_weekly_earnings.currency,))
    if normal_weekly_earnings.minor < 0:
        raise ValueError("normalWeeklyEarnings must not be negative")

    lines: List[dict] = []

    def line_for(week_start: str, rule: int) -> dict:
        for line in lines:
            if line["week_start"] == week_start and line["rule"] == rule:
                return line
        line = {"week_start": week_start, "rule": rule, "paid": 0, "waiting": 0}
        lines.append(line)
        return line

    previous_end: Optional[str] = None
    last_piw_end: Optional[str] = None
    series_end = ""
    waiting_served = 0
    units_used = 0
    exhausted = False

    for period in periods:
        parse_iso_date(period.start)
        parse_iso_date(period.end)
        if period.end < period.start:
            raise ValueError("sickness period %s to %s ends before it starts" % (period.start, period.end))
        if previous_end is not None and period.start <= previous_end:
            raise ValueError("sickness periods must be in date order and must not overlap")
        previous_end = period.end

        # Too short to be a period of incapacity under the rules of its first
        # day: it is not paid and does not link anything.
        length = days_between(period.start, period.end) + 1
        if length < SSP_RULES[_rule_for(period.start)].minimum_period_days:
            continue

        if last_piw_end is None or days_between(last_piw_end, period.start) - 1 > LINK_GAP_DAYS:
            # A new period of entitlement: waiting days and the 28 weeks start
            # again, and it ends 3 years on at the latest (SSP (General) Regs 1982 reg 3(3)).
            waiting_served = 0
            units_used = 0
            series_end = add_months(period.start, 36)
        last_piw_end = period.end

        day = period.start
        while day <= period.end:
            weekday = day_of_week(day)
            current = day
            day = add_days(day, 1)
            if weekday not in qualifying:
                continue
            index = _rule_for(current)
            rule = SSP_RULES[index]
            if rule.lower_earnings_limit is not None and normal_weekly_earnings.minor < rule.lower_earnings_limit:
                continue
            week_start = add_days(current, -(weekday % 7))
            if waiting_served < rule.waiting_days:
                waiting_served += 1
                line_for(week_start, index)["waiting"] += 1
                continue
            units = WEEK_UNITS // per_week
            if current >= series_end or units_used + units > MAX_UNITS:
                exhausted = True
                continue
            units_used += units
            line_for(week_start, index)["paid"] += 1

    lines.sort(key=lambda l: (l["week_start"], l["rule"]))
    weeks = [
        SickPayWeek(
            week_start=l["week_start"],
            paid_days=l["paid"],
            waiting_days=l["waiting"],
            amount=money(_week_amount(SSP_RULES[l["rule"]], normal_weekly_earnings.minor, l["paid"], per_week), "GBP"),
        )
        for l in lines
    ]
    return SickPay(
        total=money(sum(w.amount.minor for w in weeks), "GBP"),
        paid_days=sum(w.paid_days for w in weeks),
        waiting_days=sum(w.waiting_days for w in weeks),
        weeks=weeks,
        entitlement_exhausted=exhausted,
    )


def _week_amount(rule: SspRule, earnings: int, days: int, per_week: int) -> int:
    """SSP for some days of one week: the weekly rate times days over
    qualifying days in the week, rounded up to the penny, as HMRC's daily-rate
    tables do. The weekly rate is kept exact (in ten-thousandths of a penny) so
    that 80% of earnings is never rounded twice."""
    rate = rule.weekly_rate * 10000
    if rule.earnings_cap_basis_points is not None:
        rate = min(rate, earnings * rule.earnings_cap_basis_points)
    return round_div(rate * days, 10000 * per_week, "up")

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add payroll.statutory-sick-pay
Download for Python payroll.statutory-sick-pay-1.0.1-python.fune · 36,894 bytes sha256 b78600f12799074c323f25438cfb606120c7c1eff28eb1b7cf34c34b9da7c174

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./payroll.statutory-sick-pay-1.0.1-python.fune, or fetch it from a terminal with fune pull payroll.statutory-sick-pay@1.0.1:python.

The whole function, every language, is one file too: payroll.statutory-sick-pay-1.0.1.fune, 52,251 bytes, sha256 aee3a09d679043e90bea8a61646a83de75fe303badb083f88bacf8051e8abbcf. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before payroll.statutory-sick-pay

after — your function gets the result and the arguments, and returns the final result.

# fune: after payroll.statutory-sick-pay

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

# fune: replace dates.add-days in payroll.statutory-sick-pay
# fune: replace dates.add-months in payroll.statutory-sick-pay
# fune: replace dates.day-of-week in payroll.statutory-sick-pay
# fune: replace math.round-div in payroll.statutory-sick-pay
# fune: replace money.amount in payroll.statutory-sick-pay

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show payroll.statutory-sick-pay --steps.

# fune: step payroll.statutory-sick-pay after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
two working weeks off in 2025-26: three waiting days, then the daily rate periods ×1, 1, 2, 3, 4, 5, £500.00 → total £166.25, paid days 7, waiting days 3, weeks ×2, entitlement exhausted false
a three-day spell before April 2026 is not a period of incapacity and pays nothing periods ×1, 1, 2, 3, 4, 5, £500.00 → total £0.00, paid days 0, waiting days 0, weeks , entitlement exhausted false
the same three-day spell after the 2026 reforms is paid from day one periods ×1, 1, 2, 3, 4, 5, £500.00 → total £73.95, paid days 3, waiting days 0, weeks ×1, entitlement exhausted false
from April 2026 low earners get 80% of earnings, kept exact and rounded up once periods ×1, 1, 2, 3, 4, 5, £123.33 → total £19.74, paid days 1, waiting days 0, weeks ×1, entitlement exhausted false
earnings below the lower earnings limit before April 2026 earn no SSP periods ×1, 1, 2, 3, 4, 5, £124.99 → total £0.00, paid days 0, waiting days 0, weeks , entitlement exhausted false
a spell 52 days after the last links to it and serves no new waiting days periods ×2, 1, 2, 3, 4, 5, £500.00 → total £118.75, paid days 5, waiting days 3, weeks ×2, entitlement exhausted false
a gap of exactly 8 weeks still links periods ×2, 1, 2, 3, 4, 5, £500.00 → total £71.25, paid days 3, waiting days 3, weeks ×3, entitlement exhausted false
a gap of 59 days does not link, so waiting days are served again periods ×2, 1, 2, 3, 4, 5, £500.00 → total £47.50, paid days 2, waiting days 6, weeks ×2, entitlement exhausted false
a short spell between two linked ones is ignored before 2026 periods ×3, 1, 2, 3, 4, 5, £500.00 → total £118.75, paid days 5, waiting days 3, weeks ×2, entitlement exhausted false
a spell across 6 April 2026 serves two old waiting days, then pays at the new rate periods ×1, 1, 2, 3, 4, 5, £500.00 → total £123.25, paid days 5, waiting days 2, weeks ×2, entitlement exhausted false
Show the other 9 tests
CaseArgumentsExpected
seven qualifying days a week: one day is a seventh of the week, rounded up periods ×1, 1, 2, 3, 4, 5, 6, 7, £500.00 → total £66.72, paid days 4, waiting days 3, weeks ×2, entitlement exhausted false
2023-24 rate on a Tuesday-and-Thursday pattern: waiting days run into the second week periods ×1, 2, 4, £400.00 → total £164.10, paid days 3, waiting days 3, weeks ×3, entitlement exhausted false
28 weeks is the most SSP a period of entitlement pays periods ×1, 1, 2, 3, 4, 5, £500.00 → total £3,451.00, paid days 140, waiting days 0, weeks ×28, entitlement exhausted true
no qualifying days is an error periods ×1, , £500.00 → error: at least one qualifying day
a weekday outside 1-7 is an error periods ×1, 0, 1, £500.00 → error: qualifying weekdays must be 1 (Monday) to 7 (Sunday)
a period that ends before it starts is an error periods ×1, 1, 2, 3, 4, 5, £500.00 → error: ends before it starts
overlapping periods are an error periods ×2, 1, 2, 3, 4, 5, £500.00 → error: must be in date order and must not overlap
sickness before the rules this capability carries is an error periods ×1, 1, 2, 3, 4, 5, £500.00 → error: no SSP rule for 2020-06-01
negative earnings are an error periods ×1, 1, 2, 3, 4, 5, -£0.01 → error: must not be negative

More from the author

Every day is judged by the rule row in force on that day (`data/ssp-rules.json`).

Up to 5 April 2026: - A spell is only a **period of incapacity for work** (PIW) if it lasts at least 4 consecutive days, counting every calendar day. Shorter spells pay nothing and link nothing. - The first **3 qualifying days** of a period of entitlement are waiting days and are not paid. - Earnings below the **lower earnings limit** (£123 in 2023-24 and 2024-25, £125 in 2025-26) mean no SSP at all. - The weekly rate is a flat £109.40 (2023-24), £116.75 (2024-25), £118.75 (2025-26).

From 6 April 2026 (Employment Rights Act 2025 ss.10 and 11, commenced by SI 2026/373 reg 2): - A PIW is **one day or more**; there are **no waiting days**; there is **no lower earnings limit**. - The weekly rate is the **lower of £123.25 and 80% of normal weekly earnings**. The 80% figure is kept exact and only the day amount is rounded, so a £123.33 earner gets £98.664 a week and £19.74 for one day of a five-day week.

Throughout: - PIWs separated by **8 weeks (56 days) or less** link into one (SSCBA 1992 s.152(3)); a gap of 57 days starts a new period of entitlement with fresh waiting days. "Separated by" counts the days in between: a spell ending on 5 June links to one starting on 1 August but not 2 August. - At most **28 weeks** of SSP per period of entitlement (s.155), counted as weeks of qualifying days, and a linked period of entitlement ends after **3 years** (SSP (General) Regulations 1982 reg 3(3)). `entitlementExhausted` says a qualifying day went unpaid for either reason. - The amount for k paid days in a week of q qualifying days is `ceil(weekly rate x k / q)` to the penny. That reproduces HMRC's published daily tables exactly (for example £47.50 for 2 of 5 days at £118.75, £16.68 for 1 of 7 days at £116.75).

## Spells across 6 April 2026

SI 2026/373 regs 3 to 5 have transitional rules. This capability models them only as far as "each day uses the rule in force on that day": waiting days served on or before 5 April stay waiting days, and qualifying days from 6 April are paid (which is what reg 3 provides). Reg 4 (employees earning £125 to £154.05 whose entitlement began before 6 April keep the flat £123.25 rather than 80%) and reg 5 (a period that did not qualify because of the LEL) are **not** modelled: the caller must handle those cases.

## What the caller must supply or decide

- Normal weekly earnings, worked out over the 8-week relevant period (SSP (General) Regulations 1982 reg 19). One figure is used for every spell. - The qualifying days: one fixed weekly pattern (at least one a week, s.154(3)). - Whether the employee qualifies at all in other respects (maternity disqualification, custody, contract ended, 12-week ESA link, agency rules): none of that is checked here. - A week in which the rule changes gets one line per rule, so a rate rise on a Monday gives the Sunday line under the old rule and the rest under the new.

A build installed with `history=current` keeps only the rule still in force and refuses earlier dates rather than paying them at today's rate.

## Sources

- HMRC, "Rates and thresholds for employers" 2023 to 2024, 2024 to 2025, 2025 to 2026 and 2026 to 2027 (SSP weekly rates, LELs and the daily-rate tables): https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2023-to-2024, https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2024-to-2025, https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2025-to-2026, https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2026-to-2027 - GOV.UK, "Statutory Sick Pay (SSP): employer guide" (£123.25 or 80% of AWE; linked periods 8 weeks or less apart; paid from the first full working day): https://www.gov.uk/employers-sick-pay - Social Security Contributions and Benefits Act 1992 ss.152-155 and 157 as amended: https://www.legislation.gov.uk/ukpga/1992/4/section/152, https://www.legislation.gov.uk/ukpga/1992/4/section/155 - Employment Rights Act 2025 ss.10 and 11: https://www.legislation.gov.uk/ukpga/2025/36/section/10, https://www.legislation.gov.uk/ukpga/2025/36/section/11 - The Employment Rights Act 2025 (Commencement No. 3 and Transitional Provisions) Regulations 2026 (SI 2026/373): https://www.legislation.gov.uk/uksi/2026/373/made - The Social Security Benefits Up-rating Order 2026 (SI 2026/148) art. 8 (£123.25): https://www.legislation.gov.uk/uksi/2026/148/made - Statutory Sick Pay (General) Regulations 1982 regs 3(3) and 19: https://www.legislation.gov.uk/uksi/1982/894/regulation/3

## Before you rely on this

**Not professional advice.** This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a payroll specialist review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.

**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

Legislation: Crown copyright and database right.

1.0.1 marks it unreviewed and adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE239
README.md6,495
data/ssp-rules.json1,166
impl/python.py6,359
impl/rust.rs8,616
impl/typescript.ts6,270
vectors.json15,544