Ga naar hoofdinhoud

Date arithmetic under lib, audited file by file

Change: every-term-on-the-engine-calendar, task 1.1. Row Q8.23 of the competitor gap register, "is the calendar the deadline engine reads the same calendar the organisation administers".

This file is the deliverable the proposal asked for: not a count, but a verdict per file. The structural test tests/Unit/Architecture/EveryTermOnTheCalendarTest.php reads the verdict column of the table below, so a row here is a fact the build keeps true rather than a note somebody wrote once.

How the set was built​

Patterns, over lib/**/*.php, file names only:

grep -rlE "modify\('[+-]|new DateInterval|->add\(|->sub\(|strtotime\('[+-]" lib/ --include=*.php

Read at 16f00124a (development, 2026-09-14). The same five patterns return the same 37 files at 9c478d810, the commit the proposal counted at, so the set has not drifted since the row was rated.

Two of the patterns, ->add( and ->sub(, are not anchored to a date type. They match IJobList::add() as readily as DateTimeImmutable::add(), which is why two of the 37 rows below have the verdict neither with the reason "not date arithmetic". The count is an upper bound on the problem, never the problem itself.

The verdicts​

  • statutory: a period the law names, ending on a date somebody can be held to. The beslistermijn, the bezwaartermijn, the ingebrekestelling grace, the hersteltermijn, the dwangsom window, the Woo decision term. Algemene termijnenwet art. 1 moves such a date off a Saturday, a Sunday or a recognised holiday, so it has to reach the working calendar.
  • business: a date the organisation is judged on but the law does not name. It reads better on a working day and nobody is in breach when it does not.
  • neither: a report window, a throughput bucket, a retention period in years, demo seed data, an intraday service norm, or a pattern false positive. A weekend cannot change the answer, so the calendar is not on its path.

Counts​

at 9c478d810 (proposal)at 16f00124a (this change, before)after this change (task 3.1)
files matching the patterns373738 (39 since case-recycle-window)
of those, reaching a working calendar5519
of those, verdict statutorynot measured1616
statutory files reaching a calendarnot measured315
statutory files allowlisted with a named ownernot measuredn/a1

case-recycle-window added a thirty-ninth, lib/Service/Recycle/RetentionClocks.php: it counts a lawful-purpose retention in months, which is the AVG side of the same question ArchivalNominationDeriver answers for the Archiefwet, and it carries the same verdict.

lifecycle-acts-on-the-case added a fortieth, lib/Service/Lifecycle/SilenceCloseService.php. It counts the administered period of silence a case type declares (REQ-LIFE-04) and the date the applicant is told the case will close on. It is deliberately NOT statutory: nothing in the Awb counts sixty days of nobody answering, the period is a gemeente's own setting, and rolling it to a working day would move a date nobody is owed. Every other date in that change goes through CaseDateNormaliser, which is why only this one line matches the patterns.

The set grew by one file, and by nothing else: lib/Service/TermijnTimerService.php now calls the engine's SlaCalculator::add(), which the ->add( pattern matches. It is the bridge, not a term, and its row says so.

The proposal counted five files referencing WorkingDayCalculator, and those five are all inside the 37. A sixth file references it at 16f00124a, lib/Service/Milestone/StalledCaseDetector.php, which does no date arithmetic of its own and so is not one of the 37. Two of the five, lib/Service/Kcc/SlaCalculator.php (since retired) and the calculator itself, have the verdict neither, which is why three rather than five statutory files reached a calendar before this change.

The table​

Lines are the matching lines at 16f00124a. Reaches names what the file consults for the day a date lands on, after this change.

filelinesverdictreachesreason
lib/BackgroundJob/AcknowledgementDispatchJob.php136neitherthe retry counter for an acknowledgement that failed to send, attempt + 1 passed to IJobList::add(). It is one of the ->add( matches the pattern cannot anchor to a date type
lib/BackgroundJob/LinkCollectionToCaseJob.php90neitherqueues the same dossier link again when the service account is missing, through IJobList::add(). One of the ->add( matches the pattern cannot anchor to a date type
lib/BackgroundJob/AdviceDeadlineJob.php89neithera look-ahead window the daily scan uses to pick which advices to remind on, not a date anyone is held to
lib/Service/Setup/ExampleDataFragments.php177neitherexample data dates relative to the moment the set is loaded (@today+2d), so a fresh install shows deadlines this week; fictional data, not a term anyone is held to
lib/BackgroundJob/DsoDeadlineJob.php196, 205statutoryWorkingDayCalculatorthe Omgevingswet decision term; the day walk is already the calculator's
lib/Flow/DossiqAskPersonNode.php648, 669neithera flow task due date and a node timeout in minutes, both process plumbing
lib/Service/Milestone/MilestoneStallTimer.php154businessWorkingDayCalculatorthe day after a milestone's scheduled deadline, when its stall timer breaches (termijnbewaking-op-engine-timers 3.1). The deadline itself comes from StalledCaseDetector and MilestoneSchedule, which count working days; the +1 day only moves the breach to the first day the case is late, the day BottleneckDetectionJob reported it. A milestone is a house norm, not a term the Awb names
lib/Service/Pause/ChaseSchedule.php105businessWorkingDayCalculatorthe interval between two reminders on a suspended term (pause-reason-with-chasing). The hersteltermijn ITSELF is statutory and DeadlinePauseService rolls it; a reminder inside that window is the gemeente's own schedule and nobody is in breach when one lands late. It still reaches the calendar, because a reason may declare countsWorkingDays and a reminder on a Sunday is a reminder nobody reads
lib/Portal/PortalSubmissionLedger.php154neitherclaims a portal submission key with IMemcache::add(); the ttl is cache plumbing, not a date anyone is held to. One of the ->add( matches
lib/Service/Intake/CollectionLinkQueue.php61neitherputs the dossier link on the job list with IJobList::add(). One of the ->add( matches
lib/Service/Session/SessionRules.php67, 84, 134neitherthe generic form of the hearing planner's rules (C12): the inspection floor is a minimum notice counted backwards from the session, and rolling it forward would shorten it; the attendance grace window is hours after the session start, process plumbing
lib/Service/Queue/UrgencyScorer.php253businessWorkingDayRollthe business days until a deadline, for the urgency tier of the work queue. It is a display count, not a term anyone is held to, and it asks the administered calendar first (one-term-engine, REQ-OTE-04); the day shift only aligns its bounds with the Monday to Friday fallback walk
lib/Service/Queue/PersonalAgendaItemService.php177neitherthe fortnight the personal queue looks ahead for items a person planned on their own calendar. It is a display window on a list, not a date anybody is held to, and moving it off a Saturday would hide a Saturday appointment from the person who booked it (one-personal-queue)
lib/Service/Queue/QueueFigures.php65neitherthe coming week the team queue counts cases due within, for its "Deadline within 7 days" tile. A display window on a count, not a date anybody is held to; the days a case has waited are counted by DayCount
lib/Flow/DossiqRequestDecisionNode.php615neithera node timeout in minutes
lib/Listener/AcknowledgementOnCreateListener.php113neitherqueues the acknowledgement with attempt 1 through IJobList::add(), the same unanchored match. No date is computed here
lib/BackgroundJob/ReportGroupingJob.php93neitherrequeues the report grouping of a case through IJobList::add() when no service account is set, the same unanchored match. No date is computed here
lib/Listener/ReportGroupingOnCreateListener.php88neitherqueues the report grouping of a created case through IJobList::add(), the same unanchored match. No date is computed here
lib/Listener/CaseInheritedDeadlineListener.php181statutoryengine calendara deelzaak inherits the parent case type's term, so it inherits the term's end date
lib/Service/Actions/ScheduleReminderHandler.php162, 167neitherwhen a reminder background job runs
lib/Service/Archival/ArchivalNominationDeriver.php259neithera retention period counted in years, where a weekend cannot move the answer
lib/Service/Beschikking/BezwaarTermijnScheduler.php76, 77, 114statutoryengine calendarAwb 6:7, the six week bezwaartermijn and its reminder; line 114 is the archive trigger the day after and follows the rolled end
lib/Service/Beschikking/OpenRegisterArchivalAdapter.php121, 153neithera retention period from the selectielijst, in years
lib/Service/Bezwaar/AdvisoryCommitteeService.php178businessthe committee's own advice deadline, a house norm the Awb does not name
lib/Service/Bezwaar/HearingSchedulePlanner.php106, 153neitherthe Awb 7:4 lid 2 inzage floor is a minimum notice counted backwards from the hearing; rolling an end date forward would shorten it
lib/Service/Bezwaar/HearingService.php570neithera proposed hearing date two weeks out, which the planner then reschedules
lib/Service/CaseLifecycleService.php516, 521statutoryengine calendarextends a case end date by the case type's period, which is the term a handler is judged on
lib/Service/CaseTermsService.php249statutoryengine calendarbinds the planned end, the internal target, the phase term and a case type's fixed closing date; every one of them is a date somebody is held to, and endAfter() is the single line that turns a day count into one
lib/Service/ComplaintAnalyticsService.php247neithera six month reporting window
lib/Service/Termijn/TermDashboardFigures.php83neithera seven day window on the deadline dashboard: which running terms end this week. It compares against each term's own endDateCurrent, which the engine already rolled; the quarter it reports on is a reporting window
lib/Service/ComplaintService.php408statutoryWorkingDayCalculatorAwb 9:11 klachttermijn in weeks; the service already consults the calculator for working days
lib/Service/DeadlineExtensionService.php150statutoryrollTermEndForan extension named in days (Woo art. 4.4 lid 2) asks for the current end plus those days; the asked-for date then goes through the same Algemene termijnenwet roll as every extension (one-term-engine)
lib/Service/DeadlinePauseService.php93, 172statutoryengine calendarAwb 4:5 and 4:15: the credited suspension and the unused remainder both move endDateCurrent
lib/Service/DemoCaseloadReport.php86neithera three day horizon in a demo report
lib/Service/DemoCaseloadSeedDataService.php343neitherdemo seed data
lib/Service/Doorlooptijd/CaseEnricher.php233businessengine calendarthe dashboard's derived expected end; it mirrors a statutory term, so it rolls with it or it contradicts it on screen
lib/Service/Doorlooptijd/DeadlineComplianceCalculator.php168neithermonth buckets for a compliance trend
lib/Service/DsoCaseService.php310statutoryWorkingDayCalculatorthe Omgevingswet term; the day walk is already the calculator's
lib/Service/Money/PayoutService.php133statutoryengine calendarAwb 4:17: paymentDateLatest is the date the dwangsom payment is late after
lib/Service/Kcc/CallbackRetrySchedule.php71neitherthe wait before the next callback attempt, 15 minutes doubling to a day. It replaced Kcc/SlaCalculator.php (retired by termijnbewaking-op-engine-timers 4.1), whose channel SLAs nothing read; a retry wait is not a deadline and has no calendar
lib/Service/Money/AccrualNoticeService.php188statutoryengine calendarAwb 4:17: the grace period whose end opens the dwangsom window
lib/Service/ProcessMining/ThroughputTrendCalculator.php109neitherweekly buckets for a throughput trend
lib/Service/ProcessMiningService.php93, 96neithera twelve month reporting window
lib/Service/Recycle/RetentionClocks.php144neithera lawful-purpose retention counted in months (AVG art. 5.1e), where a weekend cannot move the answer; the same verdict ArchivalNominationDeriver carries for the Archiefwet side
lib/Service/Import/CaseRecordImport.php311-312neitherconverts the case type's declared ISO extension period (P14D) into a number of days, measured from a fixed day, to hand to the term carry-over. No date anyone is held to is computed here: the extended end is the carried source term's own, and the term engine rolls it (case-record-import, REQ-CRI-002)
lib/Service/Lifecycle/SilenceCloseService.php131neitheran ADMINISTERED period of silence a case type declares, after which the product closes the case, and the date it announces. Not a term: nothing in the Awb counts sixty days of nobody answering, the number is a gemeente's own setting, and a weekend cannot move an answer to "has anything happened". Same verdict as RetentionClocks for the same reason
lib/Service/QuickActionService.php158statutoryengine calendarAwb 9:11: the six week klacht decision term, written on intake from the KCC
lib/Service/Status/StatusDwellService.php135businessWorkingDayCalculatorhow long a case has sat in one status, a service level and never a term. The BREACH is the engine's: StatusDwellTimer arms it in the engine's own businessDays unit over the calendar the organisation administers. The count held on the case is the local calculator's, because the engine exposes projection and no count between two dates, so the two can differ by a day on a custom calendar. Closing that needs a count operation in openregister working-calendar-admin
lib/Service/Remedy/RemedyLodging.php195statutoryengine calendarAwb 6:7 and 6:8, the term a remedy is lodged within, counted from the day the decision was made known; the end rolls to the next working day (Algemene termijnenwet art. 1) before a lodging is judged timely
lib/Service/Stuf/StufOutboundTransport.php298neithernot date arithmetic: IJobList::add() matched the ->add( pattern
lib/Service/Subsidie/BeschikkingService.php82statutoryengine calendarthe bezwaartermijn of a subsidy beschikking
lib/Service/Subsidie/BewijsstukService.php136neithera record retention period in years
lib/Service/Subsidie/SubsidieService.php151statutoryengine calendarthe Awb decision term of a subsidy application
lib/Service/Subsidie/TerugvorderingService.php88, 101statutoryengine calendarthe bezwaartermijn and the payment term of a clawback decision
lib/Service/Subsidie/TussenrapportageService.php99statutoryengine calendarthe assessment term of an interim report, the date the applicant is answered by
lib/Service/TermijnNotificationService.php97neithernot date arithmetic: IJobList::add() matched the ->add( pattern
lib/Service/Term/FirstResponseOutcome.php243businessWorkingDayCalculatorthe overrun on a first-response term (term-configuration-beyond-the-case-type). The +1 day is not a term end: it is where the count of days we were late STARTS, because the day we promised by is not a day we were late. The counting itself is the calculator's, so this file keeps no holiday list of its own. It moves to the engine calendar when one answers "how many working days between these two dates"; WorkingDayRoll rolls a single date and cannot answer that yet
lib/Service/TermijnTimerService.php481neitheritselfnot date arithmetic: the engine's SlaCalculator::add() matched the ->add( pattern. This file IS the bridge; it asks the engine for the roll and computes no date of its own
lib/Service/TermijnService.php109statutoryengine calendarendDateCalculated. Taken by terms-on-the-engine-calendar: createTermijnInstance() routes through TermijnTimerService::rollTermEndFor(), the one call every other term site makes, so the stored date and the armed timer come from one computation
lib/Service/WOODeadlineService.php101, 172statutoryengine calendarWoo art. 4.4: the decision term and its statutory extension
lib/Service/Custody/CaseTakeoverRequest.php299businessthe answer period a handler has before a takeover request escalates. An internal service clock, not a term the applicant is owed, so it counts calendar days on purpose and reaches no calendar: the escalation is about how long the request has sat, and a weekend it sat through is time it sat
lib/Service/Term/DependentTermOffer.php228statutoryallowlistedthe end date a dependent-term offer PROPOSES, endDateCurrent + N days, which a handler then approves. It is a statutory end date and it does not reach the calendar, so an offer can propose a Saturday. Owned by dependent-term-follows-predecessor; the one-line fix is to route it through TermijnTimerService::rollTermEndFor(), in that change rather than in a build fix
lib/Service/Termijn/WorkingDayRoll.php196neitheritselfnot date arithmetic of its own: it ASKS the engine calendar (SlaCalculator::add() of zero business days, which matched the ->add( pattern) for the first working moment at or after an instant. This file is the calendar accessor the intake start uses; it holds no list, decides no policy, and since #2951 it does not read rollToWorkingDay either, because TermijnTimerService::rollEnabled() is the one place that decides
lib/Service/Termijn/TermEndRoll.php221neitheritselfnot date arithmetic of its own: it ASKS the engine calendar (SlaCalculator::add() of zero business days, which matched the ->add( pattern) to roll a statutory end date off a weekend or a holiday. This is the Algemene termijnenwet roll, split out of TermijnTimerService so the admin screen can ask the roll whether a calendar answers without asking the timer service. The computation did not change when it moved, and the file it moved out of already carried this verdict
lib/Service/Termijn/TermDefinitions.php247, 260statutoryengine calendarendDateCalculated, which is the row lib/Service/TermijnService.php carried at line 109 until the definitions got their own class. The computation did not change when it moved: TermijnService::createTermijnInstance() still routes the answer through TermijnTimerService::rollTermEndFor(), the one call every other term site makes, so the stored date and the armed timer come from one computation. Working-day definitions reach WorkingDayRoll from here, and a calendar that does not answer is logged at warning as a term that is SHORTER than it is owed
lib/Service/WorkingDayCalculator.php170, 204neitheritselfthe calendar. It is the fallback the three sites use when the engine is absent, and the only holiday list dossiq is allowed to hold
lib/Woo/WooThroughputReport.php164neitherthe throughput report adds one verdict to a per-reviewer, per-day tally through its own add() method. It counts work done on a day; no term is computed and nobody is held to a date
lib/Service/Term/ApplicantTermNote.php256neithernot date arithmetic: it parses the case type's extensionPeriod (ISO 8601) into a DateInterval only to say the period in words ("twee weken") in the sentence the applicant reads. No date is computed; the day the decision is due is copied from the case deadline the term engine keeps

Rows added after the reading​

lib/BackgroundJob/AcknowledgementDispatchJob.php and lib/Listener/AcknowledgementOnCreateListener.php did not exist at 16f00124a and so are not in the 37 counted above. The change ontvangstbevestiging added them, and both are ->add( matches on IJobList, not on a date. They carry the verdict neither for the reason this file already names: two of the five patterns cannot be anchored to a date type. lib/BackgroundJob/ReportGroupingJob.php and lib/Listener/ReportGroupingOnCreateListener.php arrived later the same way, with ai-features-on-the-case-consume-hermiq, and carry the same verdict for the same reason. The counts below describe the reading, not the table.

lib/Service/Termijn/TermDefinitions.php and lib/Service/Termijn/TermEndRoll.php are the third and the fourth. Neither existed at 16f00124a: the roll moved out of lib/Service/TermijnTimerService.php and the definitions out of lib/Service/TermijnService.php, both when those classes went over their complexity ceiling. Each row carries the verdict the file it left already carried, because each call is the same call.

What the table is not​

It is not a defect list. Twenty of the 37 have the verdict neither, and for each of them the reason is the point: saying once, in writing, that a throughput bucket does not need a working day roll is what stops the next reader running the same grep and reaching the same uncertainty.

Two rows deserve their own sentence.

  • lib/Service/Bezwaar/HearingSchedulePlanner.php looks statutory and is not, in the sense this audit means. Awb 7:4 lid 2 gives a minimum of seven days between the inzage and the hearing. Algemene termijnenwet art. 1 lengthens a term by moving its end forward; applied to a floor counted backwards it would shorten the citizen's inzage, which is the opposite of what the Awt is for. The floor stays raw, deliberately.
  • lib/Service/TermijnService.php is the one statutory file this change does not fix. It is the file terms-on-the-engine-calendar names as its motivating defect, and taking it here would edit another lane's open change. It is allowlisted with that change as its owner.