## S-10c · Close the ZGW zaak on document-timeout expiry (closes #106) Completes the S-10a/S-10b boundary flagged in ADR-0017: when a registration's 30-day document term lapses, the domain now cancels the **ZGW zaak** as well as marking the aggregate `Verlopen`, so OpenZaak and the register no longer diverge. ### What it does On expiry the `ExpireRegistrationWorker` calls the ACL to set the zaak to a distinct, non-terminal **`Geannuleerd`** status with a **`Vervallen`** resultaat (vs the approval `Afgehandeld` + `Geregistreerd`), resolved **by omschrijving** in the ACL — the ACL-first ordering mirrors approval so a failed ZGW call leaves the job for redelivery rather than diverging the two. **Path:** Flowable P30D timer → `RegistratieVerlopen` job → domain `ExpireRegistrationWorker` → ACL `POST /annuleringen` → ZGW `resultaten` + `statussen` (Geannuleerd) → aggregate `Verlopen`. ### Layers touched (each red→green) - **ACL gateway** — `SetZaakToCancellationStatusAsync` (Geannuleerd + Vervallen by name); approval now resolves its `Geregistreerd` resultaat by name too (a second resultaattype now exists). - **ACL service/API** — `AclService.CancelZaakAsync` + `POST /annuleringen`. - **Domain** — `IAclClient.CancelZaakAsync` + client; expiry worker cancels the zaak before advancing to `Verlopen`, guarded against redelivery double-cancel. - **Seed** — non-terminal `Geannuleerd` statustype (volgnummer 2; `Afgehandeld` → 3) + `Vervallen` resultaattype, both idempotent by omschrijving and sharing the zaaktype's procestype. - **Verify/integration** — ACL↔OpenZaak integration test (live `Geannuleerd` + resultaat); `run-domain-check.sh` fires the real P30D timer and asserts the zaak reaches `Geannuleerd` end-to-end; BDD scenario asserts cancel-on-timeout vs untouched-when-in-time. - **Docs** — ADR-0019 (cancellation modelling decision), demo-script, BACKLOG. ### Design note (ADR-0019) ZGW allows only one eindstatus per zaaktype, so `Geannuleerd` is modelled as a **non-terminal** status (it records a cancellation status + resultaat but does not set `einddatum`). This follows the issue's explicit "distinct statustype + resultaat" outcome; the shared-eindstatus alternative is recorded in the ADR. ### Tests Unit + acceptance all green locally (Acl 38, Big 134, Acceptance 17, Bff 33, EventSubscriber 19). Integration + verify-stack run in CI (need live OpenZaak + selectielijst egress). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #109
82 lines
5.5 KiB
Markdown
82 lines
5.5 KiB
Markdown
# ADR-0019: A timed-out zaak is cancelled with a distinct status + resultaat, resolved by name
|
|
|
|
- **Status:** Accepted
|
|
- **Date:** 2026-07-21
|
|
- **Deciders:** Respellion engineering
|
|
- **Relates to:** S-10c (#106). Completes the S-10a/S-10b boundary noted in ADR-0017 (§Consequences) and
|
|
reuses the ACL close-zaak machinery from S-09b (approval) and the Documenten work in ADR-0018.
|
|
|
|
## Context
|
|
|
|
ADR-0017 (S-10a) cancels the *process* and marks the domain aggregate `Verlopen` when the 30-day
|
|
document term lapses, but explicitly deferred setting the ZGW **zaak** to a cancellation status. Left
|
|
open, a timed-out zaak stays open in OpenZaak while the register shows the registration as lapsed — the
|
|
two diverge. S-10c closes that gap: on expiry the domain must also cancel the zaak through the ACL
|
|
(§8.1, the only code that talks to ZGW).
|
|
|
|
The non-obvious part is *how to represent "cancelled" in ZGW* alongside the existing "approved" close.
|
|
The approval path (S-09b) sets the zaak's **eindstatus** (the terminal statustype) plus a resultaat. In
|
|
ZGW a zaaktype has exactly one eindstatus — the highest-`volgnummer` statustype — and setting it is what
|
|
closes the zaak (`einddatum`). A second *terminal* status would collide with that single-eindstatus rule.
|
|
|
|
## Decision
|
|
|
|
**Model cancellation as a distinct, non-terminal `Geannuleerd` statustype plus a distinct `Vervallen`
|
|
resultaat, and resolve both the approval and cancellation statustype/resultaat by their omschrijving
|
|
(name) rather than by position or the eindstatus flag alone.**
|
|
|
|
- **Seed.** `Geannuleerd` is seeded at `volgnummer` 2 — between `Ontvangen` (1) and the `Afgehandeld`
|
|
eindstatus (3) — so it is a *non-terminal* status and never displaces the eindstatus the approval path
|
|
resolves. A second resultaattype `Vervallen` (archiefnominatie `vernietigen`) is seeded beside the
|
|
approval `Geregistreerd` (`blijvend_bewaren`); both draw their `selectielijstklasse` from the
|
|
zaaktype's single `selectielijstProcestype` so they validate on publish.
|
|
- **The ACL owns the mapping.** `OpenZaakGateway.SetZaakToCancellationStatusAsync` resolves `Geannuleerd`
|
|
+ `Vervallen` by omschrijving and POSTs the resultaat then the status (OpenZaak requires a resultaat
|
|
before a closing/terminal status), mirroring `SetZaakToEindstatusAsync`. Exposed as
|
|
`AclService.CancelZaakAsync` behind the ACL endpoint `POST /annuleringen`. The omschrijvingen live as
|
|
constants in the gateway — the ACL, not the domain, knows which ZGW status means what (§8.1).
|
|
- **Approval now resolves its resultaat by name too.** With two resultaattypen present, taking the first
|
|
is ambiguous (the Zaken API does not guarantee order), so the approval path resolves `Geregistreerd`
|
|
by omschrijving. Its statustype resolution is unchanged (still the eindstatus).
|
|
- **Domain wiring.** The `ExpireRegistrationWorker` calls `IAclClient.CancelZaakAsync(zaakUrl)` **before**
|
|
advancing the aggregate to `Verlopen` (ACL-first, mirroring approval): if the ACL call fails the job is
|
|
redelivered (§8.6) rather than leaving the aggregate `Verlopen` with an open zaak. The existing
|
|
open-state guard stops a redelivered job from cancelling twice (a second resultaat would be a 400); a
|
|
registration that lapsed before its zaak was opened has nothing to cancel.
|
|
|
|
## Consequences
|
|
|
|
**Positive**
|
|
|
|
- The domain aggregate and the ZGW zaak no longer diverge on timeout — both reflect the cancellation.
|
|
- Reuses the approval close machinery (resultaat-then-status, ACL endpoint shape, ACL-first ordering), so
|
|
the change is additive and §8 stays clean (only the ACL talks to ZGW).
|
|
- Verified at two levels: an ACL↔OpenZaak integration test asserts the live zaak reaches `Geannuleerd`
|
|
with a resultaat, and the domain verify script fires the real P30D timer and confirms the zaak is
|
|
cancelled end-to-end.
|
|
|
|
**Negative / costs**
|
|
|
|
- `Geannuleerd` is non-terminal, so the cancelled zaak's `einddatum` is not set — it carries a
|
|
cancellation status + resultaat but is not formally "closed" in ZGW. Accepted: the register reads the
|
|
domain aggregate's status, and a single eindstatus per zaaktype is a ZGW constraint we chose not to
|
|
fight. Formally closing a cancelled zaak (a second eindstatus, or reusing `Afgehandeld` with a
|
|
`Vervallen` resultaat) is a possible follow-up.
|
|
- The ACL couples to the seeded omschrijvingen (`Geregistreerd`/`Geannuleerd`/`Vervallen`) by string
|
|
constants. This mirrors the existing implicit coupling to the catalogus (zaaktype URL, eindstatus) and
|
|
is documented in the gateway.
|
|
- Renumbering `Afgehandeld` from `volgnummer` 2 to 3 means a *stale* local catalogus must have its
|
|
OpenZaak volumes reset for the change to take effect; CI reseeds a fresh catalogus each run.
|
|
|
|
## Alternatives considered
|
|
|
|
- **Shared eindstatus, distinct resultaat only** (reuse `Afgehandeld`, distinguish approval vs
|
|
cancellation purely by the resultaat). ZGW-idiomatic and would set `einddatum` on cancellation too, but
|
|
the register would show no visibly distinct cancellation *status*. Rejected in favour of the issue's
|
|
explicit "distinct statustype + resultaattype" outcome, which makes the cancellation legible in ZGW.
|
|
- **A second terminal (eindstatus) `Geannuleerd`.** Rejected: ZGW allows only one eindstatus per
|
|
zaaktype (highest volgnummer); a second terminal status would either not close the zaak or collide with
|
|
the approval eindstatus resolution.
|
|
- **Passing the target omschrijvingen from the domain.** Rejected: which ZGW status means "cancelled" is
|
|
ZGW vocabulary the ACL owns (§8.1); the domain says only "cancel this zaak".
|