Functional Weave
Code in Python

payroll.statutory-maternity-pay Unreviewed

UK Statutory Maternity Pay: LEL eligibility and 39 weekly amounts, 90% then the lower of 90% or the flat rate.

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

Pinned by 12 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 Maternity Pay (SMP): whether an employee's earnings qualify, and the 39 weekly amounts that follow.

- Weeks 1 to 6: **90% of average weekly earnings** (AWE), with no cap. - Weeks 7 to 39: the **lower of 90% of AWE and the flat weekly rate** in force. - Every payment is **rounded up** to the next penny (SMP (General) Regulations 1986 reg 28). AWE itself is not rounded first (HMRC: "Do not round the figure up or down to whole pence"), so 90% of £123.45 is £111.105, paid as £111.11.

For example

  • statutory_maternity_pay(£600.00, 2026-09-15, 2026-08-30) → eligible true, qualifying week start 2026-05-31, lower earnings limit £129.00, weeks ×39, total £9,652.56 £600 a week: six weeks at £540, then 33 at the flat £194.32
  • statutory_maternity_pay(£300.00, 2026-01-20, 2026-01-05) → eligible true, qualifying week start 2025-10-05, lower earnings limit £125.00, weeks ×39, total £7,982.58 leave across April 2026: the flat rate rises for weeks starting on or after 5 April
  • statutory_maternity_pay(£150.00, 2025-11-12, 2025-10-29) → eligible true, qualifying week start 2025-07-27, lower earnings limit £125.00, weeks ×39, total £5,265.00 earnings under the flat rate: 90% for all 39 weeks

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_maternity_pay(average_weekly_earnings: Money, due_date: str, pay_start: str) -> MaternityPay
average_weekly_earningsMoneyaverage weekly earnings over the relevant period, unrounded to the penny as HMRC says, worked out by the caller
due_datedateexpected date of childbirth from the MATB1; its Sunday-to-Saturday week is the expected week of childbirth
pay_startdatefirst day of the maternity pay period; SMP weeks start on this weekday
returnsMaternityPay

The types it declares, generated into your project

@dataclass(frozen=True)
class MaternityPayWeek:
    """One SMP week."""

    #: 1 to 39
    week: int
    start: str
    end: str
    amount: Money

@dataclass(frozen=True)
class MaternityPay:
    #: average weekly earnings at least the LEL in force at the end of the qualifying week
    eligible: bool
    #: the Sunday 15 weeks before the expected week of childbirth
    qualifying_week_start: str
    #: the LEL that test used
    lower_earnings_limit: Money
    #: 39 weeks when eligible, none otherwise
    weeks: List[MaternityPayWeek]
    total: Money

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

from fune.payroll.statutory_maternity_pay import statutory_maternity_pay  # payroll.statutory-maternity-pay@^1
impl/python.py · 76 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, TypeVar

from .dates_add_days import add_days  ← from dates.add-days ^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_maternity_pay_data import (
    SMP_LOWER_EARNINGS_LIMITS,
    SMP_LOWER_EARNINGS_LIMITS_HISTORY,
    SMP_RATES,
    SMP_RATES_HISTORY,
)
from .payroll_statutory_maternity_pay_types import MaternityPay, MaternityPayWeek

SMP_WEEKS = 39
EARNINGS_RELATED_WEEKS = 6
# 90% of average weekly earnings (SSCBA 1992 s.166(1)(a)).
EARNINGS_BASIS_POINTS = 9000

T = TypeVar("T")


def _lookup(rows: Sequence[T], history: str, date: str, what: str) -> T:
    for row in rows:
        if date >= row.valid_from and (row.valid_to is None or date <= row.valid_to):
            return row
    if history != "full":
        earliest = min((r.valid_from for r in rows), default=None)
        if earliest is not None and date < earliest:
            raise ValueError(
                "no %s for %s: this build was installed with history=%s, so it only carries rules from %s. "
                "Reinstall with history=full for older cases." % (what, date, history, earliest)
            )
    raise ValueError("no %s for %s" % (what, date))


def statutory_maternity_pay(average_weekly_earnings: Money, due_date: str, pay_start: str) -> MaternityPay:
    """Statutory Maternity Pay: whether it is due, and the 39 weekly amounts.

    Two dates decide the figures and are easy to confuse. Eligibility uses the
    lower earnings limit in force at the end of the qualifying week, which can
    be in an earlier tax year than the pay; each week's flat rate is the one in
    force on the day that SMP week starts.
    """
    if average_weekly_earnings.currency != "GBP":
        raise ValueError("SMP is paid in GBP, received %s" % (average_weekly_earnings.currency,))
    if average_weekly_earnings.minor < 0:
        raise ValueError("averageWeeklyEarnings must not be negative")

    # The expected week of childbirth runs Sunday to Saturday (SSCBA 1992 s.171(1)).
    expected_week = add_days(due_date, -(day_of_week(due_date) % 7))
    qualifying_week_start = add_days(expected_week, -15 * 7)
    add_days(pay_start, 0)
    lel = _lookup(SMP_LOWER_EARNINGS_LIMITS, SMP_LOWER_EARNINGS_LIMITS_HISTORY, add_days(qualifying_week_start, 6), "lower earnings limit").weekly_limit

    weeks: List[MaternityPayWeek] = []
    total = 0
    if average_weekly_earnings.minor >= lel:
        # Kept in hundredths of a penny: HMRC says not to round average weekly
        # earnings, and reg 28 rounds only the payment, up.
        earnings_rate = average_weekly_earnings.minor * EARNINGS_BASIS_POINTS
        for week in range(1, SMP_WEEKS + 1):
            start = add_days(pay_start, 7 * (week - 1))
            rate = earnings_rate
            if week > EARNINGS_RELATED_WEEKS:
                rate = min(rate, _lookup(SMP_RATES, SMP_RATES_HISTORY, start, "SMP rate").weekly_rate * 10000)
            amount = round_div(rate, 10000, "up")
            total += amount
            weeks.append(MaternityPayWeek(week=week, start=start, end=add_days(start, 6), amount=money(amount, "GBP")))
    return MaternityPay(
        eligible=average_weekly_earnings.minor >= lel,
        qualifying_week_start=qualifying_week_start,
        lower_earnings_limit=money(lel, "GBP"),
        weeks=weeks,
        total=money(total, "GBP"),
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 4 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-maternity-pay
Download for Python payroll.statutory-maternity-pay-1.0.1-python.fune · 60,932 bytes sha256 1f935900494d0c03ef98b3f42f725580d4f2c8ff1c8834c17802e2dcfdd93c9d

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

The whole function, every language, is one file too: payroll.statutory-maternity-pay-1.0.1.fune, 69,575 bytes, sha256 3cddc9f24aa78a6d4b30c05b3acf924169bb925796c1bf01b73c69bff6001869. 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-maternity-pay

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

# fune: after payroll.statutory-maternity-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-maternity-pay
# fune: replace dates.day-of-week in payroll.statutory-maternity-pay
# fune: replace math.round-div in payroll.statutory-maternity-pay
# fune: replace money.amount in payroll.statutory-maternity-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-maternity-pay --steps.

# fune: step payroll.statutory-maternity-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
£600 a week: six weeks at £540, then 33 at the flat £194.32 £600.00, 2026-09-15, 2026-08-30 → eligible true, qualifying week start 2026-05-31, lower earnings limit £129.00, weeks ×39, total £9,652.56
leave across April 2026: the flat rate rises for weeks starting on or after 5 April £300.00, 2026-01-20, 2026-01-05 → eligible true, qualifying week start 2025-10-05, lower earnings limit £125.00, weeks ×39, total £7,982.58
earnings under the flat rate: 90% for all 39 weeks £150.00, 2025-11-12, 2025-10-29 → eligible true, qualifying week start 2025-07-27, lower earnings limit £125.00, weeks ×39, total £5,265.00
90% of £123.45 is £111.105, rounded up to £111.11 £123.45, 2023-12-01, 2023-11-20 → eligible true, qualifying week start 2023-08-13, lower earnings limit £123.00, weeks ×39, total £4,333.29
earnings a penny under the LEL: no SMP £124.99, 2025-11-12, 2025-10-29 → eligible false, qualifying week start 2025-07-27, lower earnings limit £125.00, weeks , total £0.00
earnings exactly at the LEL qualify £125.00, 2025-11-12, 2025-10-29 → eligible true, qualifying week start 2025-07-27, lower earnings limit £125.00, weeks ×39, total £4,387.50
a qualifying week ending after 6 April uses the new, higher LEL £126.00, 2026-07-19, 2026-07-05 → eligible false, qualifying week start 2026-04-05, lower earnings limit £129.00, weeks , total £0.00
a due date on a Saturday belongs to the week that began the Sunday before £126.00, 2026-07-18, 2026-07-05 → eligible true, qualifying week start 2026-03-29, lower earnings limit £125.00, weeks ×39, total £4,422.60
negative earnings are an error -£0.01, 2026-09-15, 2026-08-30 → error: must not be negative
a malformed due date is an error £600.00, 15/09/2026, 2026-08-30 → error: is not an ISO date
Show the other 2 tests
CaseArgumentsExpected
a qualifying week before the carried rules is an error £600.00, 2022-09-15, 2022-08-30 → error: no lower earnings limit for 2022-06-04
pay weeks before the carried SMP rates are an error £600.00, 2023-07-31, 2023-01-02 → error: no SMP rate for 2023-02-13

More from the author

## Which date decides which figure

- **Expected week of childbirth** (EWC): the Sunday-to-Saturday week containing the due date on the MATB1 (SSCBA 1992 s.171 defines a week as starting on Sunday). - **Qualifying week**: the 15th week before the EWC, i.e. the Sunday 105 days before the EWC begins. - **Eligibility**: AWE must be at least the NI lower earnings limit that applies **at the end of the qualifying week** (its Saturday). A qualifying week ending on 11 April 2026 uses the 2026-27 LEL of £129 even though it started in 2025-26, when the LEL was £125. - **Flat rate**: SMP weeks begin on the weekday the maternity pay period starts. Each week is paid at the flat rate in force on the **day that week starts**. Rates change on the first Sunday in April (2 April 2023, 7 April 2024, 6 April 2025, 5 April 2026), a day before the other statutory payments, and the dated rows in `data/smp-rates.json` carry those exact dates.

## Not covered

The other qualifying conditions are the caller's to check: 26 weeks' continuous employment into the qualifying week, being on the payroll in the qualifying week, notice and medical evidence (MATB1). AWE is an input: work it out over the relevant period as HMRC's guide describes. Births before the qualifying week, part-week payments aligned to a payroll period, and Statutory Paternity, Adoption and Shared Parental Pay are not modelled. The start date is not checked against the earliest allowed start (11 weeks before the EWC), because an early birth moves it.

A `history=current` build keeps only the rates in force and refuses older dates rather than answering them with today's figures.

## Sources

- GOV.UK, "Statutory Maternity Pay and Leave: employer guide" (39 weeks, 90% for 6 weeks, then the lower of the flat rate or 90%; eligibility): https://www.gov.uk/employers-maternity-pay-leave - HMRC, "Work out your employee's payments for Statutory Maternity Pay" (qualifying week, LEL at the end of the qualifying week, do not round AWE, SMP weeks start on the first day of the pay period): https://www.gov.uk/guidance/statutory-maternity-pay-manually-calculate-your-employees-payments - HMRC, "Rates and thresholds for employers" 2023 to 2024 through 2026 to 2027 (flat rates £172.48, £184.03, £187.18, £194.32 and their start dates; LELs £123, £123, £125, £129): https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2026-to-2027 and the pages for the earlier years at the same address with the years changed. - The Statutory Maternity Pay (General) Regulations 1986, reg 28 (rounding up): https://www.legislation.gov.uk/uksi/1986/1960/regulation/28 - The Social Security Benefits Up-rating Order 2026 (SI 2026/148), arts 1 and 9 (£194.32 in force 5 April 2026): https://www.legislation.gov.uk/uksi/2026/148/made

## 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
NOTICE244
README.md4,735
data/lower-earnings-limits.json459
data/smp-rates.json512
impl/python.py3,440
impl/rust.rs4,900
impl/typescript.ts3,452
vectors.json40,164