Files
register-referentie/docs/architecture/adr-0019-zaak-cancellation-on-timeout.md
T
notandClaude Opus 4.8 ca287eb13e
CI / lint (pull_request) Successful in 1m22s
CI / build (pull_request) Successful in 1m2s
CI / unit (pull_request) Successful in 1m14s
CI / frontend (pull_request) Successful in 2m46s
CI / mutation (pull_request) Failing after 2m30s
CI / verify-stack (pull_request) Has been cancelled
docs(acl): ADR-0019 + demo/backlog for zaak cancellation on timeout (S-10c) (refs #106)
Records the ZGW cancellation-modelling decision (distinct non-terminal
Geannuleerd status + Vervallen resultaat, resolved by name) and documents the
end-to-end timeout-cancellation path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:48:58 +02:00

5.5 KiB

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".