docs(workflow): ADR-0017 + demo note for the document-wait timeout (refs #102)
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
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>
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
# 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.
|
||||||
+52
-1
@@ -361,7 +361,8 @@ DOM=http://localhost:8080 # domain service
|
|||||||
curl -s -i -X POST "$DOM/registrations" -H 'Content-Type: application/json' \
|
curl -s -i -X POST "$DOM/registrations" -H 'Content-Type: application/json' \
|
||||||
-d '{"bsn":"123456782","diplomaOrigin":"Buitenlands"}' | grep -i '^location:'
|
-d '{"bsn":"123456782","diplomaOrigin":"Buitenlands"}' | grep -i '^location:'
|
||||||
#
|
#
|
||||||
# 2. Once the zaak is opened, the process parks at the CBGV-advies task (NOT Beoordelen). In Flowable:
|
# 2. Once the zaak is opened, the process first parks at WachtOpDocumenten (S-10a); complete that task
|
||||||
|
# (documents received) — then it parks at the CBGV-advies task (NOT Beoordelen). In Flowable:
|
||||||
FL=http://localhost:8090/flowable-rest/service
|
FL=http://localhost:8090/flowable-rest/service
|
||||||
curl -s -u rest-admin:test -X POST "$FL/query/tasks" -H 'Content-Type: application/json' \
|
curl -s -u rest-admin:test -X POST "$FL/query/tasks" -H 'Content-Type: application/json' \
|
||||||
-d '{"processDefinitionKey":"registratie","taskDefinitionKey":"CBGVAdvies"}' | python3 -m json.tool
|
-d '{"processDefinitionKey":"registratie","taskDefinitionKey":"CBGVAdvies"}' | python3 -m json.tool
|
||||||
@@ -382,3 +383,53 @@ domestic: `Beoordelen` directly (§8.2, ADR-0016).
|
|||||||
> The domestic/foreign paths are covered by the `Een diploma op herkomst routeren` acceptance
|
> The domestic/foreign paths are covered by the `Een diploma op herkomst routeren` acceptance
|
||||||
> scenarios and unit tests (the origin is carried into the process); the DMN decision and the
|
> scenarios and unit tests (the origin is carried into the process); the DMN decision and the
|
||||||
> foreign→CBGV routing are asserted live by the verify-domain check.
|
> foreign→CBGV routing are asserted live by the verify-domain check.
|
||||||
|
|
||||||
|
## S-10a — Document wait + 30-day timeout cancels the registration (#102, ADR-0017)
|
||||||
|
|
||||||
|
After the zaak is opened the registratie process parks at a **WachtOpDocumenten** user task, waiting
|
||||||
|
for the citizen's documents (their diploma). Two things can happen:
|
||||||
|
|
||||||
|
- **Documents arrive in time** → the task completes and the process continues to the diploma-eligibility
|
||||||
|
routing (S-13) → beoordeling.
|
||||||
|
- **30 days pass with no documents** → an interrupting `P30D` boundary timer cancels the wait, runs the
|
||||||
|
`RegistratieVerlopen` external task, and the domain expires the registration to the terminal status
|
||||||
|
**VERLOPEN** (the case is cancelled).
|
||||||
|
|
||||||
|
The real upload trigger (portal → BFF → domain → ACL → Documenten API) is S-10b; until then the
|
||||||
|
"documents received" step is completing the task in Flowable, and the timeout is demonstrated by
|
||||||
|
firing the timer early via the management API.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOM=http://localhost:8080 # domain service
|
||||||
|
FL=http://localhost:8090/flowable-rest/service # flowable-rest
|
||||||
|
|
||||||
|
# 1. Submit a registration; once the zaak is opened it parks at WachtOpDocumenten:
|
||||||
|
curl -s -i -X POST "$DOM/registrations" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"bsn":"123456782"}' | grep -i '^location:' # note the /registrations/<id> reference
|
||||||
|
WQ='{"processDefinitionKey":"registratie","taskDefinitionKey":"WachtOpDocumenten"}'
|
||||||
|
|
||||||
|
# 2a. Documents-in-time: complete the WachtOpDocumenten task → the process advances to beoordeling.
|
||||||
|
TID=$(curl -s -u rest-admin:test -X POST "$FL/query/tasks" -H 'Content-Type: application/json' \
|
||||||
|
-d "$WQ" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"][0]["id"])')
|
||||||
|
curl -s -u rest-admin:test -X POST "$FL/runtime/tasks/$TID" \
|
||||||
|
-H 'Content-Type: application/json' -d '{"action":"complete"}'
|
||||||
|
|
||||||
|
# 2b. Timeout: instead of completing it, fire the 30-day timer early via the management API. Find the
|
||||||
|
# instance's timer job, "move" it to executable; the async executor fires the interrupting event.
|
||||||
|
PID=$(curl -s -u rest-admin:test -X POST "$FL/query/tasks" -H 'Content-Type: application/json' \
|
||||||
|
-d "$WQ" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"][0]["processInstanceId"])')
|
||||||
|
JID=$(curl -s -u rest-admin:test "$FL/management/timer-jobs?processInstanceId=$PID" \
|
||||||
|
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"][0]["id"])')
|
||||||
|
curl -s -u rest-admin:test -X POST "$FL/management/timer-jobs/$JID" \
|
||||||
|
-H 'Content-Type: application/json' -d '{"action":"move"}'
|
||||||
|
# The RegistratieVerlopen worker then expires the aggregate — read it back as VERLOPEN:
|
||||||
|
curl -s "$DOM/registrations/<id>" # → {"status":"Verlopen", ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
**The path:** registratie process parks at `WachtOpDocumenten` → documents received completes it (→
|
||||||
|
routing → `Beoordelen`), OR the `P30D` interrupting timer fires → `RegistratieVerlopen` external task
|
||||||
|
→ domain worker expires the aggregate to `Verlopen` → `endVerlopen` (§8.2, ADR-0017).
|
||||||
|
|
||||||
|
> Both branches are covered by the `Een documenttermijn laten verlopen` acceptance scenarios (worker +
|
||||||
|
> aggregate) and unit tests; the wait completion and the 30-day timer firing are asserted live by the
|
||||||
|
> verify-domain check.
|
||||||
|
|||||||
Reference in New Issue
Block a user