Functional Weave
Code in Python

construction.payment-notice-dates

Construction Act default dates: payment due, payment notice, final date for payment and pay-less notice.

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

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

Not professional advice. This capability calculates legal figures from published rules. It is a software component for developers, not 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 solicitor review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

The fallback payment deadlines of the Construction Act (Housing Grants, Construction and Regeneration Act 1996, as amended from 2011 by Part 8 of the Local Democracy, Economic Development and Construction Act 2009). When a construction contract leaves out its own payment mechanism, the Scheme for Construction Contracts fills the gap, and this gives the four dates it sets:

| date | rule (Scheme, Schedule Part II) | |---|---| | due date | interim (para 4): the later of 7 days after the end of the relevant period and the payee's claim; final (paras 5 and 6): the later of 30 days after completion and the claim; other (para 7): the later of 7 days after completion of that work and the claim | | payment notice | para 9: the payer's notice, no later than 5 days after the due date | | final date for payment | para 8: 17 days after the due date | | pay-less notice | para 10: no later than 7 days before the final date for payment |

For example

  • payment_notice_dates(interim, 2026-01-31, 2026-01-31, 2025-06-01, england-and-wales) → due date 2026-02-07, payment notice by 2026-02-12, final date for payment 2026-02-24, pay less notice by 2026-02-17 interim payment: 7 days after the period, then 5, 17 and 7 days back
  • payment_notice_dates(interim, 2026-01-31, 2026-02-20, 2025-06-01, england-and-wales) → due date 2026-02-20, payment notice by 2026-02-25, final date for payment 2026-03-09, pay less notice by 2026-03-02 a claim made after the 7 days sets the due date
  • payment_notice_dates(interim, 2026-02-28, 2026-02-10, 2025-06-01, england-and-wales) → due date 2026-03-07, payment notice by 2026-03-12, final date for payment 2026-03-24, pay less notice by 2026-03-17 a claim made before the period ends does not bring the due date forward

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 payment_notice_dates(kind: SchemePaymentKind, work_date: str, claim_date: str, contract_date: str, region: UkRegion) -> PaymentNoticeDates
kindSchemePaymentKindinterim (para 4), final (paras 5 and 6) or other (para 7)
work_datedateinterim: the last day of the relevant period; final and other: the day the work was completed
claim_datedatethe day the payee made its claim (written notice of the sum it considers due)
contract_datedatethe day the contract was entered into; the 2011 rules apply from 1 October 2011 (Scotland 1 November 2011)
regionUkRegionengland-and-wales or scotland; northern-ireland has its own Order and is refused
returnsPaymentNoticeDates

The types it declares, generated into your project

SchemePaymentKind = Literal["interim", "final", "other"]

@dataclass(frozen=True)
class PaymentNoticeDates:
    """The Scheme's default deadlines for one payment, every period reckoned under s.116."""

    #: the payment due date: the later of the period's expiry and the claim
    due_date: str
    #: last day for the payer's payment notice, 5 days after the due date
    payment_notice_by: str
    #: 17 days after the due date
    final_date_for_payment: str
    #: last day for a pay-less notice, 7 days before the final date for payment
    pay_less_notice_by: str

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

from fune.construction.payment_notice_dates import payment_notice_dates  # construction.payment-notice-dates@^1
impl/python.py · 124 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.

import re
from typing import Dict, List, Set

from .construction_payment_notice_dates_data import SCHEME_PERIODS, SCHEME_PERIODS_HISTORY  ← this capability’s own data, compiled from data/scheme-periods.json into the same file by fune build
from .construction_payment_notice_dates_types import PaymentNoticeDates, SchemePaymentKind
from .dates_add_days import add_days  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_bank_holidays import UkRegion, bank_holidays  ← from dates.bank-holidays ^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

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


def _check_date(name: str, value: str) -> None:
    if not isinstance(value, str) or not ISO_DATE.fullmatch(value):
        raise ValueError('%s must be an ISO date (YYYY-MM-DD), received "%s"' % (name, value))
    add_days(value, 0)  # rejects impossible dates such as 2026-02-30


def _is_sunday(iso: str) -> bool:
    return day_of_week(iso) == SUNDAY


def _statutory_days(region: str, year: int) -> List[str]:
    # Schedule 1 of the Banking and Financial Dealings Act 1971 names some fixed
    # dates that gov.uk's list leaves out when they fall on a Saturday (it lists
    # the weekday substitute instead). s.116 excludes the statutory day as well.
    y = "%04d" % year
    dec25 = y + "-12-25"
    dec26 = y + "-12-26"
    days = [dec25]  # s.116(3) names Christmas Day itself
    if region == "england-and-wales":
        if not _is_sunday(dec26):
            days.append(dec26)
        if _is_sunday(dec25) or _is_sunday(dec26):
            days.append(y + "-12-27")
    elif region == "scotland":
        jan1 = y + "-01-01"
        jan2 = y + "-01-02"
        if not _is_sunday(jan1):
            days.append(jan1)
        if not _is_sunday(jan2):
            days.append(jan2)
        if _is_sunday(jan1) or _is_sunday(jan2):
            days.append(y + "-01-03")
        if _is_sunday(dec25):
            days.append(dec26)
    return days


def payment_notice_dates(
    kind: SchemePaymentKind, work_date: str, claim_date: str, contract_date: str, region: UkRegion
) -> PaymentNoticeDates:
    """The Scheme for Construction Contracts' default payment deadlines.

    Every period is reckoned as s.116 of the Housing Grants, Construction and
    Regeneration Act 1996 says: it starts the day after the date it runs from,
    weekends count, and Christmas Day, Good Friday and that part of the UK's
    bank holidays do not. That is why this is not dates.add-business-days.
    """
    _check_date("workDate", work_date)
    _check_date("claimDate", claim_date)
    _check_date("contractDate", contract_date)

    rule = None
    for row in SCHEME_PERIODS:
        if row.region != region or row.kind != kind:
            continue
        if contract_date < row.valid_from:
            continue
        if row.valid_to is not None and contract_date > row.valid_to:
            continue
        if rule is None or row.valid_from > rule.valid_from:
            rule = row
    if rule is None:
        if SCHEME_PERIODS_HISTORY != "full":
            earliest = None
            for row in SCHEME_PERIODS:
                if row.region != region or row.kind != kind:
                    continue
                if earliest is None or row.valid_from < earliest:
                    earliest = row.valid_from
            if earliest is not None and contract_date < earliest:
                raise ValueError(
                    "no Scheme for Construction Contracts periods for %s/%s on %s: this build was installed with history=%s, "
                    "so it only carries rules from %s. Reinstall with history=full for older contracts."
                    % (region, kind, contract_date, SCHEME_PERIODS_HISTORY, earliest)
                )
        raise ValueError(
            "no Scheme for Construction Contracts periods for %s/%s for a contract entered into on %s"
            % (region, kind, contract_date)
        )

    excluded_by_year: Dict[int, Set[str]] = {}

    def is_excluded(iso: str) -> bool:
        year = int(iso[0:4])
        excluded = excluded_by_year.get(year)
        if excluded is None:
            excluded = set(h.date for h in bank_holidays(region, year))
            excluded.update(_statutory_days(region, year))
            excluded_by_year[year] = excluded
        return iso in excluded

    def reckon(start: str, days: int) -> str:
        # Count `days` reckonable days after `start` (before it when negative).
        step = -1 if days < 0 else 1
        remaining = abs(days)
        day = start
        while remaining > 0:
            day = add_days(day, step)
            if not is_excluded(day):
                remaining -= 1
        return day

    expiry = reckon(work_date, rule.due_days)
    due_date = claim_date if claim_date > expiry else expiry
    final_date = reckon(due_date, rule.final_date_days)
    return PaymentNoticeDates(
        due_date=due_date,
        payment_notice_by=reckon(due_date, rule.payment_notice_days),
        final_date_for_payment=final_date,
        pay_less_notice_by=reckon(final_date, -rule.pay_less_notice_days),
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 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 construction.payment-notice-dates
Download for Python construction.payment-notice-dates-1.0.1-python.fune · 23,621 bytes sha256 500e9e285c30b1197da907dbb573b33e1c29ef150722802b47fb330b7d87280a

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./construction.payment-notice-dates-1.0.1-python.fune, or fetch it from a terminal with fune pull construction.payment-notice-dates@1.0.1:python.

The whole function, every language, is one file too: construction.payment-notice-dates-1.0.1.fune, 35,734 bytes, sha256 93aa612d3ecd3dda27db6e041fe09a0ad80aed7c362f01b363803c0783cfdce6. 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 construction.payment-notice-dates

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

# fune: after construction.payment-notice-dates

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 construction.payment-notice-dates
# fune: replace dates.bank-holidays in construction.payment-notice-dates
# fune: replace dates.day-of-week in construction.payment-notice-dates

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 construction.payment-notice-dates --steps.

# fune: step construction.payment-notice-dates 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
interim payment: 7 days after the period, then 5, 17 and 7 days back interim, 2026-01-31, 2026-01-31, 2025-06-01, england-and-wales → due date 2026-02-07, payment notice by 2026-02-12, final date for payment 2026-02-24, pay less notice by 2026-02-17
a claim made after the 7 days sets the due date interim, 2026-01-31, 2026-02-20, 2025-06-01, england-and-wales → due date 2026-02-20, payment notice by 2026-02-25, final date for payment 2026-03-09, pay less notice by 2026-03-02
a claim made before the period ends does not bring the due date forward interim, 2026-02-28, 2026-02-10, 2025-06-01, england-and-wales → due date 2026-03-07, payment notice by 2026-03-12, final date for payment 2026-03-24, pay less notice by 2026-03-17
weekends count: a Saturday due date is not moved interim, 2026-01-31, 2026-01-31, 2011-10-01, england-and-wales → due date 2026-02-07, payment notice by 2026-02-12, final date for payment 2026-02-24, pay less notice by 2026-02-17
Easter in England: Good Friday and Easter Monday are not counted interim, 2026-03-31, 2026-03-31, 2025-06-01, england-and-wales → due date 2026-04-09, payment notice by 2026-04-14, final date for payment 2026-04-26, pay less notice by 2026-04-19
Easter in Scotland: Good Friday is excluded but Easter Monday is not a Scottish bank holiday interim, 2026-03-31, 2026-03-31, 2025-06-01, scotland → due date 2026-04-08, payment notice by 2026-04-13, final date for payment 2026-04-25, pay less notice by 2026-04-18
Christmas in England: Saturday 26 December is a statutory bank holiday as well as the Monday substitute interim, 2026-12-18, 2026-12-18, 2025-06-01, england-and-wales → due date 2026-12-27, payment notice by 2027-01-03, final date for payment 2027-01-15, pay less notice by 2027-01-08
Christmas in Scotland: 26 December counts, Saturday 2 January does not interim, 2026-12-18, 2026-12-18, 2025-06-01, scotland → due date 2026-12-26, payment notice by 2027-01-03, final date for payment 2027-01-16, pay less notice by 2027-01-09
final payment: 30 days after completion final, 2026-06-01, 2026-06-01, 2025-06-01, england-and-wales → due date 2026-07-01, payment notice by 2026-07-06, final date for payment 2026-07-18, pay less notice by 2026-07-11
final payment claimed late final, 2026-06-01, 2026-08-10, 2025-06-01, england-and-wales → due date 2026-08-10, payment notice by 2026-08-15, final date for payment 2026-08-27, pay less notice by 2026-08-20
Show the other 11 tests
CaseArgumentsExpected
other payment: 7 days after completion, across the May bank holidays other, 2026-05-01, 2026-05-01, 2025-06-01, england-and-wales → due date 2026-05-09, payment notice by 2026-05-14, final date for payment 2026-05-27, pay less notice by 2026-05-19
a contract from before the 2011 amendments is not covered interim, 2026-01-31, 2026-01-31, 2011-09-30, england-and-wales → error: no Scheme for Construction Contracts periods
the Scottish amendments start on 1 November 2011 interim, 2026-01-31, 2026-01-31, 2011-10-15, scotland → error: no Scheme for Construction Contracts periods
Northern Ireland has its own Order and is not covered interim, 2026-01-31, 2026-01-31, 2025-06-01, northern-ireland → error: no Scheme for Construction Contracts periods
an unknown payment kind weekly, 2026-01-31, 2026-01-31, 2025-06-01, england-and-wales → error: no Scheme for Construction Contracts periods
a period running into a year without bank holiday data interim, 2028-12-20, 2028-12-20, 2025-06-01, england-and-wales → error: no bank holiday data
a malformed date interim, 2026/01/31, 2026-01-31, 2025-06-01, england-and-wales → error: workDate must be an ISO date
a trailing newline after workDate is not an ISO date interim, 2026-09-16 , 2026-01-31, 2025-06-01, england-and-wales → error: workDate must be an ISO date
Arabic-Indic digits in workDate are not an ISO date interim, ٢٠٢٦-09-16, 2026-01-31, 2025-06-01, england-and-wales → error: workDate must be an ISO date
a trailing newline after claimDate is not an ISO date interim, 2026-01-31, 2026-01-31 , 2025-06-01, england-and-wales → error: claimDate must be an ISO date
a trailing newline after contractDate is not an ISO date interim, 2026-01-31, 2026-01-31, 2025-06-01 , england-and-wales → error: contractDate must be an ISO date

More from the author

The relevant period is the one the contract sets, or 28 days if it sets none (para 12). Pass its last day as `workDate`.

## Counting days

Periods are reckoned under s.116 of the 1996 Act: a period after a date starts the day after it, weekends **count**, and Christmas Day, Good Friday and the bank holidays of that part of the UK are left out. So this is not `dates.add-business-days`. Bank holidays come from `dates.bank-holidays` (gov.uk's list, 2019 to 2028; a period reaching outside that is an error, not a guess). s.116 means holidays under the Banking and Financial Dealings Act 1971, whose Schedule 1 names some fixed days that gov.uk leaves out when they fall on a Saturday, listing only the weekday substitute. Those are excluded too: 26 December (not a Sunday) and 27 December when 25 or 26 December is a Sunday in England and Wales; 1 and 2 January (not a Sunday, else 3 January) and 26 December when Christmas is a Sunday in Scotland. So in England Saturday 26 December 2026 is not counted, which a gov.uk-only calendar gets wrong.

The pay-less deadline counts 7 reckonable days backwards from the final date. s.116(2) speaks of periods "after or from" a date; counting backwards with the same exclusions is the reading this capability takes.

## What it does not do

- Contract terms win: if the contract sets its own due date, final date or notice periods (and they comply with the Act), use those instead. - The payee's default notice (s.110B) and the postponed final date that follows when the payer gives no payment notice are not calculated. - Northern Ireland has its own Construction Contracts (NI) Order 1997 and Scheme, and is refused. - Contracts entered into before the 2011 amendments (before 1 October 2011 in England and Wales, before 1 November 2011 in Scotland) are refused.

## Data

`data/scheme-periods.json` holds the periods per region and kind, dated by the day the contract was entered into (the amendments apply only to contracts made from their commencement). It declares `effective`, so a `history=current` build keeps the rows still in force and refuses dates before its horizon.

## Sources

- The Scheme for Construction Contracts (England and Wales) Regulations 1998 (SI 1998/649), Schedule Part II, as amended: https://www.legislation.gov.uk/uksi/1998/649/schedule/part/II - The Scheme for Construction Contracts (England and Wales) Regulations 1998 (Amendment) (England) Regulations 2011 (SI 2011/2333), reg. 1 (in force 1 October 2011, contracts entered into after that): https://www.legislation.gov.uk/uksi/2011/2333/regulation/1/made ; the Wales equivalent is SI 2011/1715, same date. - The Scheme for Construction Contracts (Scotland) Regulations 1998 (SI 1998/687), Schedule Part II: https://www.legislation.gov.uk/uksi/1998/687/schedule/part/II - The Scheme for Construction Contracts (Scotland) Amendment Regulations 2011 (SSI 2011/371), regs. 1 and 4 (in force 1 November 2011, contracts entered into on or after that date; paras 9 and 10 substituted with the same 5 and 7 day periods): https://www.legislation.gov.uk/ssi/2011/371/made - Housing Grants, Construction and Regeneration Act 1996, s.116 (reckoning periods of time): https://www.legislation.gov.uk/ukpga/1996/53/section/116 - Banking and Financial Dealings Act 1971, Schedule 1: https://www.legislation.gov.uk/ukpga/1971/80/schedule/1

1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in workDate, claimDate and contractDate; adds tests.

Files

PathBytes
README.md4,486
data/scheme-periods.json2,287
impl/python.py4,958
impl/rust.rs6,920
impl/typescript.ts4,767
vectors.json6,730