Files
register-referentie/docs/architecture/adr-0017-document-wait-timeout-cancellation.md
T
notandClaude Opus 4.8 5402bc179c docs(workflow): S-10a owns the document-provision trigger (ADR-0017, demo) (refs #102)
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>
2026-07-20 10:46:13 +02:00

6.3 KiB

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.