Records why the provision trigger (domain + BFF + portal 'Documenten aanleveren') lives in S-10a — the WachtOpDocumenten gate would otherwise leave the e2e red — and narrows S-10b to the real ZGW document storage. Notes the withdrawal-while-waiting follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
6.3 KiB
Markdown
91 lines
6.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 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.
|