# ADR-0017: A document-wait task with a 30-day interrupting timer cancels the registration - **Status:** Accepted - **Date:** 2026-07-20 - **Deciders:** Respellion engineering - **Relates to:** S-10a (#102); proposal #104; split from S-10 (#11). Builds on ADR-0009 (external-task worker / Workflow Client), ADR-0014 (withdrawal cancels the process), ADR-0015 (beoordeling escalation — the boundary-timer + external-worker pattern), ADR-0016 (diploma-eligibility DMN). ## Context Flow 2 (PRD §5) requires the citizen to supply documents (their diploma) after submitting. The registratie process must park waiting for those documents and, if they do not arrive within 30 days, cancel the case. S-10 was split (§13): **S-10a** is this workflow/timeout spine (backend only); **S-10b** wires the actual upload (portal → BFF → domain → ACL → Documenten API) that completes the wait. This ADR records the spine: where the wait sits, how the timeout cancels, and how the domain aggregate stays in sync. ## Decision **A `WachtOpDocumenten` user task is inserted immediately after `OpenZaakAanmaken`, carrying an `cancelActivity="true"` (interrupting) `P30D` boundary timer. "Documents received" completes the task and the process continues into the diploma-eligibility routing; on timeout the timer cancels the task, runs a `RegistratieVerlopen` external-worker task, and ends the process at `endVerlopen`. A domain worker expires the correlated aggregate to a new terminal status `Verlopen`.** - **Where the wait sits.** Right after the zaak is opened, before the diploma-eligibility DMN: the zaak exists, then the process waits for documents; on receipt it continues to the DMN routing → Beoordelen (ADR-0016). The wait gates the whole assessment, so it precedes the routing rather than sitting between the gateway and Beoordelen. - **Interrupting timer, mirroring the existing constructs.** Unlike the S-14 escalation timer (non-interrupting — the Beoordelen task stays open), this timer is interrupting: when it fires the wait token is consumed and the case is cancelled, like the S-11 withdrawal boundary (ADR-0014). The timeout branch runs a `RegistratieVerlopen` external-worker task (topic mirrors `OpenZaakAanmaken`/`BeoordelingEscaleren`) → `endVerlopen`. - **The domain stays authoritative.** The `RegistratieVerlopen` job carries the `registrationId`; the `RegistratieVerlopenProcessor` drains it and the `ExpireRegistrationWorker` loads the aggregate and calls `Registration.Expire()`, moving it to the new terminal status `Verlopen`. This keeps the aggregate — which the projection/openbaar view reads — the source of truth, exactly as escalation and withdrawal do. Idempotent per §8.6: a redelivered job whose aggregate is already `Verlopen` completes without persisting again; an unknown registration throws so the job is redelivered. - **Documents-in-time transition.** `IWorkflowClient.CompleteDocumentWaitAsync(processInstanceId)` completes the `WachtOpDocumenten` task (the Workflow Client remains the only code that talks to Flowable, §8.2). It is best-effort — a no-op if the instance already left the wait (continued, or timed out). The trigger is wired end-to-end in S-10a: a `ProvideDocuments` application use case behind an owner-scoped domain endpoint `POST /registrations/{id}/documents`, a BFF passthrough `POST /self-service/registrations/{id}/documents` (bsn from the DigiD token), and a "Documenten aanleveren" action on the self-service page — so the walking-skeleton e2e stays green (a registration can still reach the behandelaar). **S-10b replaces the stub trigger with a real file upload stored in the ZGW Documenten (DRC) API via the ACL**; the completion of the wait is unchanged. - *Why the trigger lives here, not in S-10b:* inserting the `WachtOpDocumenten` gate without any way to pass it breaks the submit→beoordeling e2e (a merge gate). Splitting "gate" from "means to pass the gate" across slices would leave `main` red, so S-10a owns both; S-10b is purely the ZGW storage behind the same action. ## Consequences **Positive** - The wait/timeout is a first-class workflow construct that reuses the boundary-timer + external-worker pattern already proven by S-14, so the domain change is small and additive: one terminal status, one worker trio (worker + processor + pump), one Workflow Client method. - §8 stays clean: the Workflow Client is still the only Flowable caller, and no new ZGW boundary is introduced in S-10a. - The timeout is verified live (verify-domain fires the P30D timer via the management-API "move" idiom and asserts the domain reaches `Verlopen`), consistent with ADR-0009/0014/0015. **Negative / costs** - Every registration now parks at `WachtOpDocumenten` before Beoordelen, so the other flows must supply documents first: the live-check blocks (S-11/S-12b/S-13/S-14) complete the task via Flowable, and the registration e2e clicks "Documenten aanleveren". A small, explicit step, but it touches every path through the process. - On expiry S-10a cancels the *process* and marks the aggregate `Verlopen` but does **not** set the ZGW *zaak* to a cancellation status — that needs a new ACL method + statustype seeding, which overlaps S-10b's ACL/infra work. Deferred to S-10b (or a follow-up); noted here as the S-10a/S-10b boundary. - Withdrawing while parked at `WachtOpDocumenten` marks the aggregate `Ingetrokken` but does not cancel the process (the withdrawal message boundary is on `Beoordelen`); the timeout worker tolerates this by no-op'ing on an already-resolved aggregate. Extending withdrawal to the wait state is a follow-up. ## Alternatives considered - **Pure-BPMN cancellation (timer → end event, no worker).** Rejected: the domain aggregate would then be out of sync with the cancelled process, and the openbaar/projection view reads the aggregate's status — the case would still look open. - **Wait task between the gateway and Beoordelen.** Rejected: documents gate the whole assessment (including the CBGV-advies routing), so the wait belongs before the DMN, not after it. - **A dedicated timeout status per branch vs. reusing an open-state guard.** `Expire()` reuses the same `RequireOpenForDecision` guard as withdrawal/decision, so only an `INGEDIEND`/`IN_BEHANDELING` registration can lapse and the terminal states stay mutually exclusive — no new guard logic.