All checks were successful
## What & why S-14: a beoordeling a behandelaar does not pick up within **14 days** escalates to the **teamlead**. A non-interrupting `P14D` boundary timer on the `Beoordelen` user task fires an external-worker task (`BeoordelingEscaleren`); the domain's escalation worker reassigns the still-open task's candidate group from `behandelaar` to `teamlead`. The task keeps its identity — only who may claim it changes. The escalation-via-external-worker decision is recorded in **ADR-0015** (proposal #98); it upholds §8.2 (the Workflow Client stays the only code that talks to Flowable) and keeps Flowable a stock image. Closes #15 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation. - [x] Implementation makes the test pass; refactor commit if structure improved. - [x] Conventional Commits referencing the issue (`refs #NN`). - [x] CI green — all Gitea Actions jobs. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (no new services; escalation is additive to the domain worker). - [x] Docs updated (ADR-0015, demo note). - [x] ADR added (`docs/architecture/adr-0015-beoordeling-escalation.md`). - [x] Demo note in `docs/demo-script.md`. ## How it was built (TDD) - **Workflow Client** (`IBeoordelingEscalatieClient`): acquire `BeoordelingEscaleren` jobs → find the open `Beoordelen` task in the instance → add `teamlead`/remove `behandelaar` candidate group → complete the job. Red → green. - **Escalation drain loop** (`BeoordelingEscalatieProcessor`) + hosted `BeoordelingEscalatiePump`, mirroring the OpenZaak worker. Red → green. - **BPMN**: non-interrupting `P14D` boundary timer on `Beoordelen` → external task → escalation end. - **Both branches** (escalate after timeout; no-op when completed in time) covered by the `Een beoordeling escaleren` acceptance scenarios + Workflow Client unit tests. - **Live integration**: `verify-domain` fires the timer early via Flowable's management API and asserts the reassignment to teamlead. ## Notes for reviewers - Interface segregation: escalation is on `IBeoordelingEscalatieClient`, separate from the OpenZaak worker's `IExternalWorkerClient`. - Reassignment is two REST hops (add teamlead, remove behandelaar); idempotent on redelivery — see ADR-0015 consequences. - Local checks green: domain unit tests (104), acceptance (13), `dotnet format --verify-no-changes`, Release build (0 errors), **domain mutation 96.69%** (break 90). The `run-domain-check.sh` escalation path is CI-verified on verify-stack (local full-stack run is constrained here). - `BeoordelingEscalatiePump` excluded from mutation, mirroring the existing `OpenZaakJobPump` exclusion. Reviewed-on: #99
4.7 KiB
4.7 KiB
ADR-0015: Beoordeling escalation reassigns via an external-worker task
- Status: Accepted
- Date: 2026-07-17
- Deciders: Respellion engineering
- Relates to: S-14 (#15); proposal #98. Builds on ADR-0009 (external-task worker / Workflow
Client), ADR-0013 (behandel-portal wiring, the
Beoordelenuser task), ADR-0014 (the boundary-event pattern onBeoordelen).
Context
S-14 escalates a beoordeling that a behandelaar does not pick up in time: after 14 days the case must
move to the teamlead role (PRD §5, flow 5). The Beoordelen user task already exists, claimable by
the behandelaar candidate group; the teamlead role is seeded in the medewerker realm.
Two forces shape this.
- The task must stay open. Escalation changes who may claim an unclaimed beoordeling, not the
work itself — so the timer must be non-interrupting: the
Beoordelentask keeps running while escalation happens alongside it. - Reassigning an open task's candidate group needs code. Flowable cannot rewrite the candidate groups of an already-open user task from BPMN XML alone — that requires either a Java delegate/listener embedded in the engine, or an out-of-process actor driving the REST API. The repository has held a "stock Flowable image, no custom jars; the Workflow Client is the only code that talks to Flowable (§8.2)" posture since ADR-0009.
Decision
A non-interrupting P14D boundary timer on Beoordelen fires an external-worker task
(BeoordelingEscaleren); the Workflow Client reassigns the still-open Beoordelen task from the
behandelaar group to teamlead.
- Modelled in BPMN, driven by an external worker. The timer routes a parallel token to an
external-workerservice task on theBeoordelingEscalerentopic, ending at a dedicated "Beoordeling geëscaleerd" end event. The model owns when escalation happens; the Workflow Client — the only code that talks to Flowable (§8.2) — owns how the reassignment is applied, exactly asOpenZaakAanmakendelegates the ZGW call (ADR-0009). No custom code runs inside Flowable. - Reassignment is a candidate-group swap. The escalation worker finds the still-open
Beoordelentask in the escalating instance (task query byprocessInstanceId+taskDefinitionKey), addsteamleadas a candidate group via the task identity links, then removesbehandelaar. The task now belongs to the teamlead; its history and variables are untouched. - Best-effort, mirroring beoordeling and withdrawal. If the task is no longer open — the behandelaar
completed it in the window before the timer fired — the reassignment is a no-op. A failed reassignment
leaves the escalation job un-completed so Flowable redelivers it (§8.6), consistent with the
OpenZaakAanmakenworker. - Segregated interface. The escalation methods live on
IBeoordelingEscalatieClient, separate from theOpenZaakAanmakenworker'sIExternalWorkerClient, so the OpenZaak worker never sees escalation (interface segregation). Both are implemented by the oneFlowableWorkflowClient.
Consequences
Positive
- The escalation trigger is visible in
registratie.bpmn; Flowable stays a stock image, and the Workflow Client remains the sole Flowable client (§8.2 upheld, not bent). - Reuses the external-worker mechanics (topic acquire/complete, hosted pump, per-tick scope, redelivery-on-failure) wholesale — the new code is one client capability, one processor, one pump.
- Escalation latency is bounded by the worker's poll interval (seconds) — negligible against a 14-day timer.
Negative / costs
- Escalation is two REST hops (add teamlead, remove behandelaar) rather than one atomic update; between them the task is briefly claimable by both groups. Harmless at these volumes, and the pair is idempotent on redelivery.
- The Flowable identity-link and management-job REST shapes are validated live (verify-domain fires the timer early via the management API), not in the Workflow Client's unit tests, which stub the HTTP exchange and assert only the request shape — consistent with ADR-0009 and ADR-0014.
Alternatives considered
- Flowable timer/task listener (Java delegate). Reassign in-engine when the timer fires. Rejected: it needs a custom jar in Flowable, breaking the stock-image, REST-only posture and adding a build/deploy surface to the engine for no capability the external-worker route lacks.
- Interrupting timer that re-creates the task for teamlead. Cancel
Beoordelenand start a fresh teamlead task. Rejected: it loses the task's identity/history and complicates correlation, where a candidate-group swap on the same task expresses "the same work, now the teamlead's" directly.