Files
register-referentie/docs/demo-script.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

11 KiB

Demo script

A running log of demoable outcomes, one section per slice. Each entry is a short, copy-pasteable walkthrough against a local make up stack.


S-08d — Walking skeleton complete: browser → submit, end-to-end

Outcome: the self-service portal is served in the stack and the full front-of-house happy path runs in a real browser — mock DigiD login → submit → confirmation — closing the walking skeleton (portal → BFF → domain → Flowable → ACL → OpenZaak, with the openbaar register reading the projection).

# 1. Bring the whole stack up (portal served on :8140, BFF :8080, Keycloak :8180).
make up

# 2. Automated happy path — Playwright, inside the compose network (issuer-consistent):
make verify-e2e          # → login as jan-burger → submit → "ontvangen" confirmation

# 3. By hand: open the portal, log in as jan-burger / test123, click "Registratie indienen".
open http://localhost:8140

The portal is served same-origin with the BFF (nginx proxies /self-service + /openbaar), so no CORS; the OIDC authority comes from /config.json at runtime. See docs/frontend-decisions.md.


S-08c — Self-service submit form (NL Design System + DigiD)

Outcome: a zorgprofessional logs in via mock DigiD and submits a BIG registration through the self-service portal (NL Design System styling); the page confirms with the reference returned by the BFF. The bsn comes from the DigiD token, so it's a confirm-and-submit flow (no bsn field).

# 1. Bring the backend + Keycloak up (BFF on :8080, Keycloak on :8180).
make up

# 2. Serve the portal (dev server); it redirects to Keycloak for DigiD login.
pnpm nx serve self-service        # → http://localhost:4200

# 3. In the browser: log in as the mock DigiD user jan-burger / test123, then submit.
#    The page shows the returned registration reference.

First real UI. The full login → submit → success happy path is automated in S-08d (Playwright, against the compose-served app). Component tests + an axe WCAG 2.1 AA check on the submit page run headless in the frontend CI lane. See docs/frontend-decisions.md.


S-08a — Nx workspace + self-service portal skeleton

Outcome: the frontend foundation — an Nx (pnpm) monorepo with the self-service Angular app (standalone + signals), lint/test/build green in a CI Node lane. The login + submit form follow in S-08c.

# From a fresh clone (Node 24 + pnpm 11):
pnpm install                       # native builds are pre-approved in pnpm-workspace.yaml
pnpm nx test self-service          # Vitest component test
pnpm nx build self-service         # production build
pnpm nx serve self-service         # → http://localhost:4200  (placeholder page)
# Or the CI-equivalent one-shot:
make frontend                      # install + nx lint/test/build

Nx manages only apps/+libs/; the .NET services stay on dotnet/the Makefile. NL Design System and the real form arrive in S-08c (#67); see docs/frontend-decisions.md.


S-07 — BFF: the portals' single backend

Outcome: the BFF validates Keycloak digid tokens on the self-service submit (forwarding the bsn to the domain) and serves the openbaar register anonymously with only public-safe fields — the front door the portals (S-08/S-09) will talk to.

The path: portal → BFF POST /self-service/registrations (token-gated) → domain; and BFF GET /openbaar/register (anonymous) → projection-api. See ADR-0010.

# 1. Bring the full stack up.
make up

# 2. Drive the BFF end-to-end (401 without a token, 202 with a real digid token, anonymous openbaar).
make verify-bff        # → "OK — BFF: 401 without token, 202 with a digid token, anonymous ..."

# 3. Try it by hand (BFF on host port 8080).
#    a) A digid access token for the mock user jan-burger (bsn 123456782):
tok=$(curl -s -X POST http://localhost:8180/realms/digid/protocol/openid-connect/token \
  -d grant_type=password -d client_id=big-portal -d username=jan-burger -d password=test123 \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")

#    b) Submit — without the token it is 401; with it, 202:
curl -s -o /dev/null -w "no token  -> %{http_code}\n" -X POST http://localhost:8080/self-service/registrations
curl -s -o /dev/null -w "with token-> %{http_code}\n" -X POST http://localhost:8080/self-service/registrations \
  -H "Authorization: Bearer $tok"

#    c) The openbaar register is anonymous and exposes only id + status (never the bsn):
curl -fsS http://localhost:8080/openbaar/register | jq

The self-service token is validated against Keycloak's digid realm; the openbaar lookup needs no token (S-09). The generated contract lives at services/bff/openapi.json — S-08's client is built from it.


S-05 — BIG Domain Service: submit a registration

Outcome: submitting a registration starts a Flowable process; the external-task worker opens a zaak via the ACL and records it on the aggregate — the upstream half of the skeleton that produces the zaak S-06 then projects.

The path: domain POST /registrations → Flowable registratie process → OpenZaakAanmaken worker → ACL → OpenZaak; GET /registrations/{id} shows the opened zaak (ADR-0009).

# 1. Bring the full stack up (seeds config, builds our services, waits for health).
make up

# 2. Drive the full path end-to-end. This also seeds a published BIG zaaktype and points the
#    ACL at it (the zaak's zaaktype URL is server-assigned, so it isn't known at bring-up).
make verify-domain      # → "OK — the domain opened a zaak and recorded it on the registration"

# 3. Submit one yourself (domain on host port 8130). Returns 202 + a Location to read back.
loc=$(curl -fsS -D - -o /dev/null -X POST http://localhost:8130/registrations \
  -H 'Content-Type: application/json' -d '{"bsn":"123456782"}' | sed -n 's/\r$//; s/^[Ll]ocation: //p')

# 4. The worker opens the zaak off the request path (eventual consistency, ADR-0009); poll
#    until zaakUrl is filled. (Step 2 must have run first, so the ACL knows the zaaktype.)
curl -fsS "http://localhost:8130$loc" | jq
# → { "registrationId": "...", "status": "Ingediend", "zaakUrl": "http://.../zaken/api/v1/zaken/<uuid>" }

Registration state is in-memory for this slice (ADR-0009); the rebuildable read model is the projection (S-06), fed by the very zaak this flow opens.


S-06 — Event Subscriber + read projection

Outcome: a zaak created in OpenZaak flows through NRC to the Event Subscriber, which projects it into a rebuildable read projection the projection-api serves.

The path: OpenZaak → (notification) NRC → (abonnement callback) Event Subscriber → register_projection → projection-api GET /register.

# 1. Bring the full stack up (seeds config, builds our services, waits for health).
make up

# 2. Register the Event Subscriber's abonnement and create a zaak, then read it back.
#    (The verify-projection check does exactly this end-to-end and asserts the result.)
make verify-projection            # → "OK — projection-api serves zaak <uuid> with status INGEDIEND"

# 3. Observe the projection directly via the read API (host port 8120).
curl -fsS http://localhost:8120/register | jq
# → [ { "id": "<zaak-uuid>", "status": "INGEDIEND", "bsn": null, "naamPlaceholder": null } ]

# 4. Idempotency + rebuild: replays don't duplicate; a rebuild repopulates from the
#    notification log (no OpenZaak access needed — ADR-0008).
curl -fsS -X POST http://localhost:8110/admin/rebuild   # Event Subscriber, host port 8110
curl -fsS http://localhost:8120/register | jq 'length'  # → unchanged

bsn / naam_placeholder are deferred (ADR-0008) — the notification doesn't carry them and the subscriber may not read OpenZaak directly (§8.1). They surface in a later slice.


S-09 — Openbaar Register portal (public visibility)

Outcome: the entry a zorgprofessional submits via self-service becomes publicly visible in the anonymous openbaar register portal — closing the walking-skeleton loop (submit → process → projection → public visibility).

The path: self-service submit → BFF → domain → (zaak) OpenZaak → NRC → Event Subscriber → projection → openbaar portal reads the BFF's public-safe GET /openbaar/register.

# 1. Bring the full stack up (self-service :8140, openbaar :8141).
make up

# 2. Submit a registration via the self-service portal (mock DigiD: jan-burger / test123),
#    or drive the whole happy path automatically (login → submit → public visibility):
make verify-e2e

# 3. Open the public register — no login. It lists the submitted entry (id + status only).
#    Only public-safe fields cross the BFF: bsn / naam never appear.
open http://localhost:8141/            # search box; searches the BFF by referentie
curl -fsS http://localhost:8140/openbaar/register | jq   # same public-safe view via the BFF proxy
# → [ { "id": "<zaak-uuid>", "status": "INGEDIEND" } ]

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.

# 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="<registration-reference-from-the-confirmation>"
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": "<zaak-uuid>", "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+).