Functional Weave
Code in Rust

payroll.statutory-maternity-pay

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

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

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

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.

pub fn 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

/// One SMP week.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaternityPayWeek {
    /// 1 to 39
    pub week: i64,
    pub start: String,
    pub end: String,
    pub amount: Money,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaternityPay {
    /// average weekly earnings at least the LEL in force at the end of the qualifying week
    pub eligible: bool,
    /// the Sunday 15 weeks before the expected week of childbirth
    pub qualifying_week_start: String,
    /// the LEL that test used
    pub lower_earnings_limit: Money,
    /// 39 weeks when eligible, none otherwise
    pub weeks: Vec<MaternityPayWeek>,
    pub total: Money,
}

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

fune!(payroll.statutory-maternity-pay@^1);  // then call statutory_maternity_pay(…)
impl/rust.rs · 116 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::add_days;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_of_week;  ← from dates.day-of-week ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::payroll_statutory_maternity_pay_data::{
    SMP_LOWER_EARNINGS_LIMITS, SMP_LOWER_EARNINGS_LIMITS_HISTORY, SMP_RATES, SMP_RATES_HISTORY,
};

const SMP_WEEKS: i64 = 39;
const EARNINGS_RELATED_WEEKS: i64 = 6;
/// 90% of average weekly earnings (SSCBA 1992 s.166(1)(a)).
const EARNINGS_BASIS_POINTS: i64 = 9000;

fn lookup(rows: &[(&'static str, Option<&'static str>, i64)], history: &str, date: &str, what: &str) -> i64 {
    for (from, to, value) in rows {
        if date >= *from && to.map_or(true, |t| date <= t) {
            return *value;
        }
    }
    if history != "full" {
        if let Some(earliest) = rows.iter().map(|r| r.0).min() {
            if date < earliest {
                panic!(
                    "no {} for {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for older cases.",
                    what, date, history, earliest
                );
            }
        }
    }
    panic!("no {} for {}", what, date)
}

/// 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.
///
/// # Panics
/// Panics on negative or non-GBP earnings, a malformed date, or a date no rule covers.
pub fn statutory_maternity_pay(average_weekly_earnings: &Money, due_date: &str, pay_start: &str) -> MaternityPay {
    if average_weekly_earnings.currency != "GBP" {
        panic!("SMP is paid in GBP, received {}", average_weekly_earnings.currency);
    }
    if average_weekly_earnings.minor < 0 {
        panic!("averageWeeklyEarnings must not be negative");
    }
    let rates: Vec<(&'static str, Option<&'static str>, i64)> =
        SMP_RATES.iter().map(|r| (r.valid_from, r.valid_to, r.weekly_rate)).collect();
    let limits: Vec<(&'static str, Option<&'static str>, i64)> =
        SMP_LOWER_EARNINGS_LIMITS.iter().map(|r| (r.valid_from, r.valid_to, r.weekly_limit)).collect();

    // The expected week of childbirth runs Sunday to Saturday (SSCBA 1992 s.171(1)).
    let expected_week = add_days(due_date, -(day_of_week(due_date) % 7));
    let qualifying_week_start = add_days(&expected_week, -15 * 7);
    add_days(pay_start, 0);
    let lel = lookup(&limits, SMP_LOWER_EARNINGS_LIMITS_HISTORY, &add_days(&qualifying_week_start, 6), "lower earnings limit");

    let eligible = average_weekly_earnings.minor >= lel;
    let mut weeks: Vec<MaternityPayWeek> = Vec::new();
    let mut total = 0i64;
    if eligible {
        // Kept in hundredths of a penny: HMRC says not to round average weekly
        // earnings, and reg 28 rounds only the payment, up.
        let earnings_rate = average_weekly_earnings.minor * EARNINGS_BASIS_POINTS;
        for week in 1..=SMP_WEEKS {
            let start = add_days(pay_start, 7 * (week - 1));
            let mut rate = earnings_rate;
            if week > EARNINGS_RELATED_WEEKS {
                rate = rate.min(lookup(&rates, SMP_RATES_HISTORY, &start, "SMP rate") * 10000);
            }
            let amount = round_div(rate, 10000, "up");
            total += amount;
            let end = add_days(&start, 6);
            weeks.push(MaternityPayWeek { week, start, end, amount: money(amount, "GBP") });
        }
    }
    MaternityPay {
        eligible,
        qualifying_week_start,
        lower_earnings_limit: money(lel, "GBP"),
        weeks,
        total: money(total, "GBP"),
    }
}

pub fn maternity_pay_to_value(pay: &MaternityPay) -> Value {
    Value::obj(vec![
        ("eligible", Value::Bool(pay.eligible)),
        ("qualifyingWeekStart", Value::str(&pay.qualifying_week_start)),
        ("lowerEarningsLimit", money_to_value(&pay.lower_earnings_limit)),
        (
            "weeks",
            Value::Arr(
                pay.weeks
                    .iter()
                    .map(|w| {
                        Value::obj(vec![
                            ("week", Value::Int(w.week)),
                            ("start", Value::str(&w.start)),
                            ("end", Value::str(&w.end)),
                            ("amount", money_to_value(&w.amount)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("total", money_to_value(&pay.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    maternity_pay_to_value(&statutory_maternity_pay(&money_from_value(&args[0]), args[1].as_str(), args[2].as_str()))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add payroll.statutory-maternity-pay
Download for Rust payroll.statutory-maternity-pay-1.0.0-rust.fune · 60,762 bytes sha256 151e71b290a6780e2bfa5aa2707ef9b9eb444e46e8061a3ce7403177f8a571d1

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

The whole function, every language, is one file too: payroll.statutory-maternity-pay-1.0.0.fune, 67,901 bytes, sha256 396e9e1f5cc5af549e8796022dd85b45eae552c371875fbb697f8046ac33e00b. 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

Files

PathBytes
README.md3,374
data/lower-earnings-limits.json459
data/smp-rates.json512
impl/python.py3,440
impl/rust.rs4,900
impl/typescript.ts3,452
vectors.json40,164