CI / lint (pull_request) Successful in 1m18s
CI / build (pull_request) Successful in 59s
CI / unit (pull_request) Successful in 1m10s
CI / frontend (pull_request) Successful in 2m33s
CI / mutation (pull_request) Successful in 10m22s
CI / verify-stack (pull_request) Failing after 11m24s
Records the interrupting P30D WachtOpDocumenten timer, the RegistratieVerlopen worker, and the new terminal Verlopen status; notes the S-10a/S-10b boundary (ZGW zaak-close deferred). Demo covers both branches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
5.3 KiB
Markdown
80 lines
5.3 KiB
Markdown
# 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* that calls it (the portal upload) is wired in S-10b; S-10a builds and tests
|
|
the completion path with the trigger stubbed (the live check completes the task directly to prove the
|
|
in-time branch, and the domain acceptance drives the worker against an in-memory stand-in).
|
|
|
|
## 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 live-check blocks
|
|
(S-11/S-12b/S-13/S-14) must complete that task first — a small, explicit step standing in for the
|
|
S-10b upload until it lands.
|
|
- 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.
|
|
|
|
## 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.
|