Files
register-referentie/docs/architecture/adr-0011-approval-status-flow.md
Niek Otten 1c185e6686
All checks were successful
CI / lint (push) Successful in 1m25s
CI / build (push) Successful in 1m13s
CI / unit (push) Successful in 1m26s
CI / frontend (push) Successful in 2m35s
CI / mutation (push) Successful in 6m6s
CI / verify-stack (push) Successful in 7m34s
S-09b: Approval flow — temp admin endpoint + status transition to projection (#77)
## What & why

S-09b (#75, split from #10) — the **approval flow** that completes the walking skeleton. A behandelaar can now approve a submitted registration; the entry flips from `INGEDIEND` to `INGESCHREVEN` in the public register. Flow: `POST /registrations/{id}/approve` (domain) → ACL sets the zaak eindstatus (ZGW `/statussen`) → OpenZaak → NRC → event-subscriber → projection → openbaar.

## Changes (bottom-up, each red→green TDD)

- **Domain** — `RegistrationStatus.Ingeschreven` + `Registration.Approve()` (guards: opened zaak, only from INGEDIEND); `ApproveRegistration` use case (idempotent) + temp `POST /registrations/{id}/approve` endpoint; `IAclClient.ApproveZaakAsync`.
- **ACL** — resolves the zaaktype's **eindstatus** from the catalogus (`isEindstatus` / highest volgnummer) and POSTs a ZGW status; exposed as `POST /statussen`. Unit + real-OpenZaak integration test.
- **Event-subscriber** — binds NRC `hoofdObject`, projects a `status`/`create` as `INGESCHREVEN` keyed on the zaak (updates the existing row), **without reading OpenZaak** (§8.1). Retains the ZGW `resource` in the log (new column + EF migration) so a rebuild reproduces the status.
- **e2e** — extended: submit → public INGEDIEND → approve → public INGESCHREVEN.
- **Docs** — ADR-0011 (the two non-obvious decisions + the walking-skeleton assumption) + demo note.

## Key decisions (see ADR-0011)

- **ACL discovers the eindstatus** (chosen over injecting a statustype URL): no new config/seed plumbing, domain stays ZGW-ignorant.
- **Any post-creation status-set ⇒ INGESCHREVEN**: in the walking skeleton the only status ever set after creation is the approval, and the subscriber may not read ZGW — documented to tighten when more transitions arrive (S-12+).

## Verification

- All .NET unit suites green locally (domain 47, acl 11, event-subscriber 14, bff 16, acceptance 7); Release build + `dotnet format` clean.
- No new compose config (the eindstatus-discovery approach avoided it).
- The real-OpenZaak integration test (ACL status-set) and the full submit→approve→visible e2e run in CI `verify-stack` (live NRC→projection + selectielijst egress, not reproducible locally).

closes #75

Reviewed-on: #77
2026-07-14 09:04:57 +00:00

4.2 KiB

ADR-0011: Approval sets the zaak eindstatus via the ACL and projects INGESCHREVEN from the notification alone

  • Status: Accepted
  • Date: 2026-07-13
  • Deciders: Respellion engineering
  • Relates to: S-09b (#75); split from S-09 (#10); builds on ADR-0001 (§8 loose coupling), ADR-0003 (ACL default-fill), ADR-0007 (OZ→NRC wiring), ADR-0008 (read projection), ADR-0009 (external-task worker)

Context

The walking skeleton could submit a registration (INGEDIEND) and show it in the openbaar register, but nothing could approve it. S-09b adds a behandelaar approval that must make the entry publicly visible as a terminal status. There is no behandel-portal yet (S-12), so approval is triggered by a temporary admin endpoint on the Domain Service.

Two decisions are non-obvious (§14) and cross service boundaries:

  1. Who resolves the ZGW statustype? Approval means "set the zaak to its final status", but the domain must stay ZGW-ignorant (§8.1 — only the ACL talks to ZGW) and does not know statustype URLs.
  2. How does the projection learn the new status? The status is set in OpenZaak, which notifies over NRC; the Event Subscriber projects it. But the subscriber may not read OpenZaak (§8.1), and an NRC status/create notification's resourceUrl is the status resource, not the zaak, and does not carry the statustype.

Decision

Approval flows Domain → ACL → OpenZaak → NRC → Event Subscriber → projection, using only the notification's own fields on the read side.

  • Domain. Registration.Approve() advances INGEDIEND → INGESCHREVEN (requires an opened zaak; a repeat is a no-op). The ApproveRegistration use case calls the ACL to set the zaak status, then advances the aggregate. A temporary POST /registrations/{id}/approve endpoint drives it.
  • ACL. A new POST /statussen operation takes only the zaak URL. The ACL resolves the zaaktype's eindstatus from the catalogus (isEindstatus, falling back to the highest volgnummer) and POSTs a ZGW status against the zaak. The domain never names statustypen — the ACL owns the ZGW translation (§8.1, ADR-0003).
  • Event Subscriber. It binds the NRC hoofdObject (always the zaak URL) and keys the projection on it, so a zaken/status/create notification updates the same row the zaak-create created, flipping it to INGESCHREVEN. It takes any status-create as the approval — in the walking skeleton the only status ever set after creation is the approval — so it never has to read OpenZaak to learn the statustype. The ZGW resource is retained in the notification log (new column) so a rebuild reproduces the right status.

Consequences

  • The domain↔ACL boundary stays clean: the domain hands over a zaak URL and says "approve"; ZGW statustype knowledge lives only in the ACL.
  • The projection remains rebuildable without OpenZaak (§8.1, ADR-0008): the log now records the ZGW resource, which is all a rebuild needs to reproject the status.
  • The openbaar register shows real lifecycle: INGEDIEND on submit, INGESCHREVEN on approval.
  • Walking-skeleton assumption: "any status-create ⇒ INGESCHREVEN" holds only while approval is the sole post-creation status transition. When more transitions arrive (beoordeling, afwijzing — S-12+), the subscriber must distinguish statustypen. The honest options then are to carry the statustype omschrijving in the notification kenmerken, or to have the ACL resolve it and re-notify — recorded here so future-me revisits this rather than assuming it generalises.

Alternatives considered

  • Inject the approved statustype URL into the ACL as config (like the zaaktype URL). Rejected: couples ACL config to seed output and adds compose/run-domain-check plumbing; runtime eindstatus discovery keeps the ACL self-contained for one extra ZGW GET per approval.
  • Have the Event Subscriber GET the status/statustype from OpenZaak to map precisely. Rejected: violates §8.1 (only the ACL talks to ZGW) and makes the projection depend on OpenZaak being up.
  • Record the derived status in the notification log instead of the ZGW resource. Rejected: the log should retain notification facts, not projection semantics; the mapping stays in the projector.