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 backpayment_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 datepayment_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.
pub fn payment_notice_dates(kind: &str, work_date: &str, claim_date: &str, contract_date: &str, region: &str) -> PaymentNoticeDates
| kind | SchemePaymentKind | interim (para 4), final (paras 5 and 6) or other (para 7) |
| work_date | date | interim: the last day of the relevant period; final and other: the day the work was completed |
| claim_date | date | the day the payee made its claim (written notice of the sum it considers due) |
| contract_date | date | the day the contract was entered into; the 2011 rules apply from 1 October 2011 (Scotland 1 November 2011) |
| region | UkRegion | england-and-wales or scotland; northern-ireland has its own Order and is refused |
| returns | PaymentNoticeDates |
The types it declares, generated into your project
// SchemePaymentKind is a string in Rust, one of: "interim", "final", "other".
// Parameters take it as &str and results hold it as String.
/// The Scheme's default deadlines for one payment, every period reckoned under s.116.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaymentNoticeDates {
/// the payment due date: the later of the period's expiry and the claim
pub due_date: String,
/// last day for the payer's payment notice, 5 days after the due date
pub payment_notice_by: String,
/// 17 days after the due date
pub final_date_for_payment: String,
/// last day for a pay-less notice, 7 days before the final date for payment
pub pay_less_notice_by: String,
}
Your code names it in one line, in the file that uses it
fune!(construction.payment-notice-dates@^1); // then call payment_notice_dates(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use std::collections::{HashMap, HashSet};
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::construction_payment_notice_dates_data::{SchemePeriod, SCHEME_PERIODS, SCHEME_PERIODS_HISTORY}; ← this capability’s own data, compiled from data/scheme-periods.json into the same file by fune build
use super::dates_add_days::add_days; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_bank_holidays::bank_holidays; ← from dates.bank-holidays ^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
const SUNDAY: i64 = 7;
fn check_date(name: &str, value: &str) {
let bytes = value.as_bytes();
let shaped = bytes.len() == 10
&& bytes[4] == b'-'
&& bytes[7] == b'-'
&& bytes
.iter()
.enumerate()
.all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit());
if !shaped {
panic!("{} must be an ISO date (YYYY-MM-DD), received \"{}\"", name, value);
}
add_days(value, 0); // rejects impossible dates such as 2026-02-30
}
fn is_sunday(iso: &str) -> bool {
day_of_week(iso) == SUNDAY
}
// 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.
fn statutory_days(region: &str, year: i64) -> Vec<String> {
let y = format!("{:04}", year);
let dec25 = format!("{}-12-25", y);
let dec26 = format!("{}-12-26", y);
let mut days = vec![dec25.clone()]; // s.116(3) names Christmas Day itself
if region == "england-and-wales" {
if !is_sunday(&dec26) {
days.push(dec26.clone());
}
if is_sunday(&dec25) || is_sunday(&dec26) {
days.push(format!("{}-12-27", y));
}
} else if region == "scotland" {
let jan1 = format!("{}-01-01", y);
let jan2 = format!("{}-01-02", y);
if !is_sunday(&jan1) {
days.push(jan1.clone());
}
if !is_sunday(&jan2) {
days.push(jan2.clone());
}
if is_sunday(&jan1) || is_sunday(&jan2) {
days.push(format!("{}-01-03", y));
}
if is_sunday(&dec25) {
days.push(dec26);
}
}
days
}
struct Reckoner<'a> {
region: &'a str,
excluded_by_year: HashMap<i64, HashSet<String>>,
}
impl<'a> Reckoner<'a> {
fn is_excluded(&mut self, iso: &str) -> bool {
let year: i64 = iso[0..4].parse().unwrap();
let region = self.region;
let set = self.excluded_by_year.entry(year).or_insert_with(|| {
let mut set: HashSet<String> = bank_holidays(region, year).into_iter().map(|h| h.date).collect();
for d in statutory_days(region, year) {
set.insert(d);
}
set
});
set.contains(iso)
}
// Count `days` reckonable days after `from` (before it when negative).
fn reckon(&mut self, from: &str, days: i64) -> String {
let step = if days < 0 { -1 } else { 1 };
let mut remaining = days.abs();
let mut day = from.to_string();
while remaining > 0 {
day = add_days(&day, step);
if !self.is_excluded(&day) {
remaining -= 1;
}
}
day
}
}
/// 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.
///
/// # Panics
/// Panics on a malformed date, a region or contract date the Scheme data does
/// not cover, or a year without published bank holidays.
pub fn payment_notice_dates(
kind: &str,
work_date: &str,
claim_date: &str,
contract_date: &str,
region: &str,
) -> PaymentNoticeDates {
check_date("workDate", work_date);
check_date("claimDate", claim_date);
check_date("contractDate", contract_date);
let mut rule: Option<&SchemePeriod> = None;
for row in SCHEME_PERIODS.iter() {
if row.region != region || row.kind != kind {
continue;
}
if contract_date < row.valid_from {
continue;
}
if let Some(valid_to) = row.valid_to {
if contract_date > valid_to {
continue;
}
}
if rule.map_or(true, |r| row.valid_from > r.valid_from) {
rule = Some(row);
}
}
let rule = match rule {
Some(rule) => rule,
None => {
if SCHEME_PERIODS_HISTORY != "full" {
let mut earliest: Option<&'static str> = None;
for row in SCHEME_PERIODS.iter() {
if row.region != region || row.kind != kind {
continue;
}
if earliest.map_or(true, |e| row.valid_from < e) {
earliest = Some(row.valid_from);
}
}
if let Some(earliest) = earliest {
if contract_date < earliest {
panic!(
"no Scheme for Construction Contracts periods for {}/{} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for older contracts.",
region, kind, contract_date, SCHEME_PERIODS_HISTORY, earliest
);
}
}
}
panic!(
"no Scheme for Construction Contracts periods for {}/{} for a contract entered into on {}",
region, kind, contract_date
);
}
};
let mut reckoner = Reckoner {
region,
excluded_by_year: HashMap::new(),
};
let expiry = reckoner.reckon(work_date, rule.due_days);
let due_date = if claim_date > expiry.as_str() {
claim_date.to_string()
} else {
expiry
};
let final_date = reckoner.reckon(&due_date, rule.final_date_days);
PaymentNoticeDates {
payment_notice_by: reckoner.reckon(&due_date, rule.payment_notice_days),
pay_less_notice_by: reckoner.reckon(&final_date, -rule.pay_less_notice_days),
due_date,
final_date_for_payment: final_date,
}
}
pub fn payment_notice_dates_to_value(dates: &PaymentNoticeDates) -> Value {
Value::obj(vec![
("dueDate", Value::str(&dates.due_date)),
("paymentNoticeBy", Value::str(&dates.payment_notice_by)),
("finalDateForPayment", Value::str(&dates.final_date_for_payment)),
("payLessNoticeBy", Value::str(&dates.pay_less_notice_by)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
payment_notice_dates_to_value(&payment_notice_dates(
args[0].as_str(),
args[1].as_str(),
args[2].as_str(),
args[3].as_str(),
args[4].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 3 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 construction.payment-notice-dates
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./construction.payment-notice-dates-1.0.1-rust.fune, or fetch it from a terminal with fune pull construction.payment-notice-dates@1.0.1:rust.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 4,486 |
| data/scheme-periods.json | 2,287 |
| impl/python.py | 4,958 |
| impl/rust.rs | 6,920 |
| impl/typescript.ts | 4,767 |
| vectors.json | 6,730 |