diff --git a/docs/architecture/adr-0011-approval-status-flow.md b/docs/architecture/adr-0011-approval-status-flow.md new file mode 100644 index 0000000..b6e0441 --- /dev/null +++ b/docs/architecture/adr-0011-approval-status-flow.md @@ -0,0 +1,64 @@ +# 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. diff --git a/docs/demo-script.md b/docs/demo-script.md index 7874781..f178a00 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -195,5 +195,35 @@ curl -fsS http://localhost:8140/openbaar/register | jq # same public-safe view # → [ { "id": "", "status": "INGEDIEND" } ] ``` -> The register shows `INGEDIEND` entries today; the approval transition to a terminal status -> (e.g. `INGESCHREVEN`) lands with the approval flow (S-09b, #75). +> The register shows `INGEDIEND` on submit; approval flips it to `INGESCHREVEN` — see S-09b below. + +--- + +## S-09b — Approval flow (public visibility flips to INGESCHREVEN) + +**Outcome:** a behandelaar approves a submitted registration via a temporary admin endpoint (no +behandel-portal yet — S-12). The approval sets the zaak's final status through the ACL, which flows +back to the projection over NRC, and the openbaar register then shows the entry as `INGESCHREVEN`. + +**The path:** `POST /registrations/{id}/approve` (domain) → ACL sets the zaak eindstatus (ZGW +`/statussen`) → OpenZaak → NRC → Event Subscriber projects `INGESCHREVEN` → openbaar register. + +```bash +# 1. Full stack up, then drive submit → public INGEDIEND → approve → public INGESCHREVEN: +make up +make verify-e2e + +# 2. Or by hand: submit (as in S-09), note the reference, then approve it. +# The zaak is opened off the request path, so approve once GET shows a zaakUrl. +ref="" +curl -fsS http://localhost:8130/registrations/$ref | jq # domain (host port 8130): wait for .zaakUrl +curl -fsS -X POST http://localhost:8130/registrations/$ref/approve -i # → 204 No Content + +# 3. The public register now shows the entry as approved. +curl -fsS http://localhost:8140/openbaar/register | jq +# → [ { "id": "", "status": "INGESCHREVEN" } ] +``` + +> **End of walking skeleton** (S-09 + S-09b): submit → process → projection → public visibility, from +> INGEDIEND through approval to INGESCHREVEN. The subscriber takes any post-creation status-set as the +> approval (ADR-0011) — a walking-skeleton assumption that tightens when more transitions arrive (S-12+).