banking.bacs-processing-date
Bacs three-day cycle: input, processing and entry (arrival) dates for a submission, skipping weekends and bank holidays.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 18 tests, run in TypeScript, Python and Rust.
What it does
The three days of the Bacs cycle a payment file joins: the input day (day 1, the file is accepted), the processing day (day 2) and the entry day (day 3, when a Direct Credit lands in the recipient's account and a Direct Debit is taken). Payroll and supplier runs are planned backwards from day 3, and a bank holiday in the middle moves it.
## The rules
For example
bacs_processing_dates(2026-09-23, false)→ input day 2026-09-23, processing day 2026-09-24, entry day 2026-09-25 a Wednesday before the cutoffbacs_processing_dates(2026-09-23, true)→ input day 2026-09-24, processing day 2026-09-25, entry day 2026-09-28 a Wednesday after the cutoff joins Thursday's cyclebacs_processing_dates(2026-09-25, false)→ input day 2026-09-25, processing day 2026-09-28, entry day 2026-09-29 a Friday submission arrives on Tuesday
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 bacs_processing_dates(submission_iso: str, after_cutoff: bool) -> BacsCycle
| submission_iso | date | the day the file is sent to Bacs |
| after_cutoff | bool | true when it is sent after the 22:30 input deadline |
| returns | BacsCycle | day 1, day 2 and day 3 of the cycle it joins |
The type it declares, generated into your project
@dataclass(frozen=True)
class BacsCycle:
"""The three days of one Bacs cycle."""
#: day 1: the file is accepted
input_day: str
#: day 2: Bacs processes it and sends it to the receiving banks
processing_day: str
#: day 3: the money leaves one account and arrives in the other
entry_day: str
Your code names it in one line, in the file that uses it
from fune.banking.bacs_processing_date import bacs_processing_dates # banking.bacs-processing-date@^1
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, Sequence
from .banking_bacs_processing_date_types import BacsCycle
from .dates_add_business_days import add_business_days ← from dates.add-business-days ^1.0.0 · built alongside by fune
from .dates_bank_holidays import bank_holidays ← from dates.bank-holidays ^1.0.0 · built alongside by fune
from .dates_day_of_week import is_weekday ← from dates.day-of-week ^1.0.0 · built alongside by fune
def _holidays_for(years: Sequence[int]) -> List[str]:
out: List[str] = []
for year in years:
out.extend(holiday.date for holiday in bank_holidays("england-and-wales", year))
return out
def _cycle_with(submission_iso: str, after_cutoff: bool, holidays: Sequence[str]) -> BacsCycle:
processing_day = is_weekday(submission_iso) and submission_iso not in holidays
input_day = submission_iso if processing_day and not after_cutoff else add_business_days(submission_iso, 1, holidays)
return BacsCycle(
input_day=input_day,
processing_day=add_business_days(input_day, 1, holidays),
entry_day=add_business_days(input_day, 2, holidays),
)
def bacs_processing_dates(submission_iso: str, after_cutoff: bool) -> BacsCycle:
"""The Bacs cycle a file joins.
Bacs works on processing days - Monday to Friday except England and Wales
bank holidays - and a file joins the cycle whose input day is the day it
arrives, if that is a processing day and it arrived before the 22:30
deadline, or the next processing day otherwise. Processing is the next
processing day and entry the one after.
"""
if not isinstance(after_cutoff, bool):
raise TypeError("afterCutoff must be a boolean, received %r" % (after_cutoff,))
# is_weekday validates the date before its year is read.
is_weekday(submission_iso)
year = int(submission_iso[0:4])
# Only reach for next year's calendar when the cycle really runs into it,
# so a December submission in the last published year still works as long
# as its cycle ends inside that year.
cycle = _cycle_with(submission_iso, after_cutoff, _holidays_for([year]))
if cycle.entry_day[0:4] != submission_iso[0:4]:
cycle = _cycle_with(submission_iso, after_cutoff, _holidays_for([year, year + 1]))
return cycleInstall
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 banking.bacs-processing-date
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./banking.bacs-processing-date-1.0.0-python.fune, or fetch it from a terminal with fune pull banking.bacs-processing-date@1.0.0:python.
The whole function, every language, is one file too: banking.bacs-processing-date-1.0.0.fune, 16,519 bytes, sha256 d6c142dfcdfc0352df592c4b9d80d19c87383647f6a4e1a3df99f140b852d8cf. 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 banking.bacs-processing-date
after — your function gets the result and the arguments, and returns the final result.
# fune: after banking.bacs-processing-date
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-business-days in banking.bacs-processing-date
# fune: replace dates.bank-holidays in banking.bacs-processing-date
# fune: replace dates.day-of-week in banking.bacs-processing-date
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 banking.bacs-processing-date --steps.
# fune: step banking.bacs-processing-date 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 | |
|---|---|---|---|
| a Wednesday before the cutoff | 2026-09-23, false | → | input day 2026-09-23, processing day 2026-09-24, entry day 2026-09-25 |
| a Wednesday after the cutoff joins Thursday's cycle | 2026-09-23, true | → | input day 2026-09-24, processing day 2026-09-25, entry day 2026-09-28 |
| a Friday submission arrives on Tuesday | 2026-09-25, false | → | input day 2026-09-25, processing day 2026-09-28, entry day 2026-09-29 |
| a Saturday submission waits for Monday | 2026-09-26, false | → | input day 2026-09-28, processing day 2026-09-29, entry day 2026-09-30 |
| the cutoff does not matter on a non-processing day | 2026-09-26, true | → | input day 2026-09-28, processing day 2026-09-29, entry day 2026-09-30 |
| Easter: Thursday before Good Friday processes on Tuesday | 2026-04-02, false | → | input day 2026-04-02, processing day 2026-04-07, entry day 2026-04-08 |
| a file sent on Good Friday starts on the Tuesday after Easter Monday | 2026-04-03, false | → | input day 2026-04-07, processing day 2026-04-08, entry day 2026-04-09 |
| Christmas 2026: the substitute Boxing Day is skipped too | 2026-12-23, false | → | input day 2026-12-23, processing day 2026-12-24, entry day 2026-12-29 |
| Christmas Eve after the cutoff | 2026-12-24, true | → | input day 2026-12-29, processing day 2026-12-30, entry day 2026-12-31 |
| New Year's Eve runs into next year's calendar | 2026-12-31, false | → | input day 2026-12-31, processing day 2027-01-04, entry day 2027-01-05 |
Show the other 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| the summer bank holiday Monday is skipped | 2026-08-28, false | → | input day 2026-08-28, processing day 2026-09-01, entry day 2026-09-02 |
| Christmas 2027 falls at a weekend, both substitute days skipped | 2027-12-24, false | → | input day 2027-12-24, processing day 2027-12-29, entry day 2027-12-30 |
| a December cycle inside the last published year needs no later data | 2028-12-22, false | → | input day 2028-12-22, processing day 2028-12-27, entry day 2028-12-28 |
| the first published year | 2019-01-02, false | → | input day 2019-01-02, processing day 2019-01-03, entry day 2019-01-04 |
| a cycle that runs past the published calendar is an error | 2028-12-29, false | → | error: no bank holiday data for england-and-wales in 2029 |
| a year before the published calendar is an error | 2018-06-01, false | → | error: no bank holiday data for england-and-wales in 2018 |
| a malformed date is an error | 2026-9-23, false | → | error: is not an ISO date |
| an impossible date is an error | 2026-02-30, false | → | error: not a real calendar date |
More from the author
Bacs describes the cycle for Direct Credit as: "Input day is the latest day a business user/bureau may submit a payment file to Bacs for a processing cycle. Payment files must be transmitted to Bacs between 07.00 and 22.30"; on day 2 files are delivered to the receiving banks; on day 3 "payments are simultaneously credited to the recipients' accounts and debited from your account". Source: Bacs, "Bacs Direct Credit", https://www.bacs.co.uk/bacs-schemes/getting-started/bacs-direct-credit/ (read 2026-09-23).
The days are Bacs processing days: Monday to Friday, excluding bank holidays in England and Wales, which Bacs uses as its non-processing days for the whole of the UK (Scotland's and Northern Ireland's extra holidays do not stop Bacs). The published Bacs processing calendar is the authority; the bank holidays here come from `dates.bank-holidays` (gov.uk data, region `england-and-wales`), and the working-day steps from `dates.add-business-days`.
So:
- input day = the submission date if it is a processing day and the file was sent by 22:30 (`afterCutoff` false); otherwise the next processing day. A file sent at a weekend or on a bank holiday joins the next processing day whatever the time; - processing day = the next processing day after the input day; - entry day = the processing day after that.
## Why `afterCutoff` is a flag
The time of day is the caller's to read (this capability never reads a clock), and comparing a timestamp with 22:30 is simpler at the call site than passing a time zone in. The 22:30 deadline is the one Bacs publishes (quoted above); a bureau or bank may set an earlier internal deadline, in which case pass `afterCutoff` against that.
## Limits
The bank holiday data covers the years gov.uk publishes (2019 to 2028 in `dates.bank-holidays@1.0.0`). A cycle that needs a year outside it fails with that capability's error rather than treating an unknown Christmas as a working day. Next year's calendar is only consulted when the cycle actually runs into next year, so a December cycle in the last published year still works.
This gives the cycle for a submission. It does not work backwards from a required arrival date; for that, step back two processing days from the entry day with `dates.add-business-days` and the same holiday list.
Files
| Path | Bytes |
|---|---|
| README.md | 2,687 |
| impl/python.py | 2,100 |
| impl/rust.rs | 2,490 |
| impl/typescript.ts | 1,977 |
| vectors.json | 4,159 |