Files
register-referentie/docs/architecture/adr-0015-beoordeling-escalation.md
Niek Otten 7bcbc726ce
All checks were successful
CI / lint (push) Successful in 1m14s
CI / build (push) Successful in 56s
CI / unit (push) Successful in 1m9s
CI / frontend (push) Successful in 2m27s
CI / mutation (push) Successful in 5m11s
CI / verify-stack (push) Successful in 7m30s
feat(workflow): beoordeling escalation to teamlead after 14 days (S-14, closes #15) (#99)
## 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
2026-07-17 09:45:36 +00:00

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 Beoordelen user task), ADR-0014 (the boundary-event pattern on Beoordelen).

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.

  1. 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 Beoordelen task keeps running while escalation happens alongside it.
  2. 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-worker service task on the BeoordelingEscaleren topic, 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 as OpenZaakAanmaken delegates the ZGW call (ADR-0009). No custom code runs inside Flowable.
  • Reassignment is a candidate-group swap. The escalation worker finds the still-open Beoordelen task in the escalating instance (task query by processInstanceId + taskDefinitionKey), adds teamlead as a candidate group via the task identity links, then removes behandelaar. 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 OpenZaakAanmaken worker.
  • Segregated interface. The escalation methods live on IBeoordelingEscalatieClient, separate from the OpenZaakAanmaken worker's IExternalWorkerClient, so the OpenZaak worker never sees escalation (interface segregation). Both are implemented by the one FlowableWorkflowClient.

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 Beoordelen and 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.