Measured Behaviour
Published
Every deadline in payments is measured in a unit, and the unit usually is not days. Two banking days is not two days, and the difference is a list of dates that lives in a different key from the number it qualifies. We measured what happens when either one loses its name.
Nine returns, two of them genuinely late
The fixture is one day's return processing: nine entries totalling $34,215.55, each with a settlement day, a return day and a reason code. Administrative returns carry a two-banking-day deadline. The consumer's unauthorised-debit codes carry sixty calendar days. A Treasury reclamation carries forty-five.
The banking calendar holds eleven non-banking days across the period: five weekends and one federal holiday. Two of the nine entries clear their deadline only because of it - a $482.15 consumer utility return and a $15,600.00 corporate payroll return, each filed on what is, in banking days, the last day available.
The correct run accepts seven and refuses two as late: a $940.00 alarm-company debit returned seventy calendar days after settlement against a sixty-day window, and an $8,300.00 supply invoice returned seven banking days after settlement against a two-day one. $9,240.00 stays with the sender.
All five default protection profiles reproduced that exactly. As everywhere else, the damage needs a member pattern reaching the configuration; the mechanics are in Protect Members.
Lose the deadlines and nothing can be late
Renaming the window table does not make the library throw. A reason code with no stated window has nothing to be late against, so it is accepted.
Accepted goes from seven to nine. Late-refused goes from two to zero. The value accepted goes from $24,975.55 to $34,215.55, and the value refused from $9,240.00 to $0.00 - which means $9,240.00 of stale returns the bank was entitled to reject are honoured against its own customers' accounts instead.
The check passes, printing: nine entries examined, nine accepted, zero refused as late, $0.00 left with the sender. Every figure is true, and it reads better than the correct run. A return file with no late items in it looks like a well-run operation. A reviewer scanning for anomalies stops on the correct line and skims past the broken one.
Lose the calendar and good returns are dishonoured
The banking calendar is one line away in the same object, and it fails in the opposite direction.
With no list of non-banking days, a two-banking-day deadline is measured over calendar days. Weekends and the federal holiday become working days, elapsed counts rise, and returns that were filed in time are judged late. Accepted falls from seven to five. Late-refused rises from two to four. The value refused rises from $9,240.00 to $25,322.15.
The two new refusals are the $482.15 consumer return and the $15,600.00 payroll return - both filed on their last banking day, both correct, both now dishonoured. The receiving banks that sent them are told their good returns arrived too late, and the losses stay where they should not.
Nothing about any entry is wrong in that arm. Every trace number, amount, settlement day and return day is exact, and the subtraction is still a subtraction. What moved is the unit it was done in - which is the same shape as reading a weight in pounds as kilograms, and just as invisible to a range check, because a count of days is a perfectly good number in either unit.
The union is the permissive half
We ran the window table and the calendar together, expecting the union to be at least as bad as the worse half.
It is byte-identical to the window table alone: nine accepted, zero refused, $0.00 left with the sender. The reason is order of consultation. A code with no stated window is accepted before the elapsed count is ever compared to anything, so the calendar - which decides only how that count is produced - never gets to matter.
This is the third area in this pass where the wide pattern is quieter than the narrow one, and it is the practical warning for anybody reading a rename matrix: a clean combined arm is not evidence that its halves are clean. The dangerous cells here are not at the wide end. They are in the middle, under the narrow, careful, well-intentioned pattern that spares one key and takes its neighbour.
The extended window, and an arm that moves only a label
The consumer's longer window is reachable from two sides: the list of codes that get it, and the flag that says whether to honour the list. Both produce the same result, and it is not the one we expected.
Losing either accepts one entry more than the correct run, not fewer. Accepted goes from seven to eight, late-refused from two to one, and the value refused from $9,240.00 to $8,300.00. The $940.00 return that was genuinely seventy calendar days late is accepted - because dropping it out of the extended set moves it onto the administrative path, where the same elapsed period is measured in banking days, and seventy calendar days is only fifty-nine banking ones.
That result is worth keeping precisely because it contradicts the obvious prediction. Removing a consumer protection did not refuse a consumer's claim; it converted a calendar deadline into a banking-day deadline and made a late return timely.
The reclamation window is the opposite kind of cell. Renaming it leaves every counter in the file identical - nine examined, seven accepted, two refused, $9,240.00 refused - and moves only the configuration line, from 45d to no stated period. On this file the reclamation happened to arrive inside any window at all. An arm that moves only a label is not automatically cosmetic; it means the file you measured did not contain the case that name governs, and the next file will.
What to assert
Every mitigation this series keeps recommending is defined over records, and records are not what is wrong here. The entries are all genuine. What is wrong is the arithmetic's unit and the existence of a deadline to compare against.
Two assertions cover it, and both are cheap. First, never let a unit live in a different key from the number it qualifies: carry the deadline and the calendar it is measured over as one value, or normalise every elapsed period at the boundary where the entry arrives. Second, make a missing deadline an error rather than a pass - a reason code the table cannot resolve should stop the file, not sail through it.
Both are worth having whatever your build does. A truncated holiday-calendar import and a partial Nacha rules update produce exactly the same two failures, and neither of them involves obfuscation at all.
On the protection side the answer is the same narrow one as every other area we measure: keep RenameMembers off the objects that cross into an installed library, and scope the pattern to names you own on both sides. For payment operations specifically, the resilience-testing questions this tends to raise are covered in our notes on DORA and your JavaScript front end.
Frequently asked questions
Which direction does this fail in?
Both, from adjacent lines of one object. Losing the deadline table accepted $9,240.00 of stale returns the bank could have refused. Losing the banking calendar dishonoured $25,322.15 of good returns that were filed in time.
Which setting caused it?
Member renaming reaching the window table, the non-banking-day list, the extended-code list or the honour flag. All five default profiles we measured - ES5, modern, both emit-gate configurations and string encoding - produced identical behaviour with no options set.
Would a range check on the elapsed days have helped?
No. Every elapsed count in every arm is a small, plausible number of days. A count measured in calendar days is a perfectly good number; it simply is not the number the deadline is stated in.
Was the combined rename worse than the single one?
No. Renaming the window table and the calendar together was byte-identical to renaming the window table alone, because a code with no stated deadline is accepted before the elapsed count is compared to anything.
Why did losing the consumer window accept more, not fewer?
Because dropping a code out of the extended set moves it onto the administrative path, where the same elapsed period is counted in banking days. Seventy calendar days is fifty-nine banking days, so a genuinely late return became timely.
What assertion would have caught it?
Two. Carry each deadline with the calendar it is measured over rather than in a separate key, and treat a reason code that resolves to no deadline as an error that stops the file.
How do I scope the member pattern?
Restrict MemberRegexp to names your own code owns on both sides of every boundary, and keep it off objects handed to installed libraries. The reference and examples are in Protect Members.
Related reading