4a047c618ccc1014088c80e479689b3a65b4c35b
163
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4a047c618c |
fix(infra): let Objecten actually accept the register record (refs #149)
Replaying the gateway's calls against a live Objecten + Objecttypen pair turned up two blockers CI would only have found after the fact: - Objecten rejects an objecttype it has not been configured with, and it identifies one by uuid — assigned at seed time by a one-shot that runs after Objecten's static setup_configuration. Pin the uuid on both sides instead. - Objecten notifies on every write and notifications_api_common *raises* when that config is absent, so every POST 500'd after rolling the object back. Objecten → NRC has no broker, worker, kanaal or abonnement yet, so disable notifications rather than wire a client that drops every message; S-19b turns them on for real. With both in place the full exchange verifies end to end: lookup → version → search → create → update (still one object), and a record carrying a bsn is rejected by the schema. ADR-0028 records both. |
||
|
|
43b45ad756 |
fix(acl): read each objecttype version instead of the versions collection (refs #149)
The version resolve assumed `GET {objecttype}/versions` returns a bare list.
Every other collection in the Objecttypen API returns a paginated envelope, and
nothing in the repo exercises that endpoint, so the shape was a guess. Follow
the path infra/registerrecord-check.py already proves against the real API
instead: read the `versions` URLs off the objecttype and fetch each for its
status. Costs a request per version, once per gateway instance.
ACL mutation score 92.23% (baseline 91.37%).
|
||
|
|
5502e4c099 |
test(acl): raise the Objecten gateway above the mutation ratchet (refs #149)
The new gateway landed at 77.6%, dragging the ACL score under its 90 break threshold. The gaps were all real behaviour nobody was asserting: a failed or empty read being mistaken for "nothing there yet" and followed by a blind write, a `results`-less response taking down the resolve with an ArgumentNullException, the CRS headers going to Objecttypen (which is not a geo API), and the write body being sent chunked. ACL score 86.63% → 92.08%. |
||
|
|
3705a18e18 |
docs: ADR-0028 + demo note — Objecten holds the register (refs #149)
ADR-0028 records why the register record lives in Objecten rather than as zaak eigenschappen, why the ACL owns the hop, and how two non-atomic writes are made to converge instead. Also retires the PRD §15 out-of-scope line the slice supersedes. |
||
|
|
400bdcafc4 |
test(infra): assert the approval wrote the register record to Objecten (refs #149)
verify-domain already drives a full approval; it now also asserts Objecten holds exactly one RegisterRecord for that registration — matched on its own reference, because the shared verify stack carries records from earlier runs. The check covers the three things that can silently go wrong: the record is missing (the ACL's Objecten hop never ran), duplicated (the upsert is not idempotent), or carries a field outside the public-safe schema. |
||
|
|
c67ee7d3f5 |
refactor(acl): resolve the objecttype's highest published version (refs #149)
Counting the `versions` URLs assumed a contiguous, all-published list. Read the objecttype's versions collection instead and take the highest one whose status is `published`, so a draft version — whose schema is still being shaped — is never written against. |
||
|
|
d14f379358 |
feat(acl): write the RegisterRecord to Objecten on approval (refs #149)
ApproveZaakAsync now does two writes: the ZGW eindstatus (the process) and the register record in Objecten (the register). The record is keyed on the zaak UUID — the same key the read projection rows carry — and its reference is the zaak's identificatie, so nothing personal crosses into the world-readable register (ADR-0027). ObjectenGateway resolves the objecttype by name (its URL and version are assigned at seed time, as with ADR-0021), searches for an existing object by data attribute, then POSTs or PATCHes. Resolution is lazy, so the ACL needs no depends_on on Objecten and does not crash-loop when it boots first. |
||
|
|
66f8322580 |
test(acl): approval writes the RegisterRecord to Objecten (refs #149)
Ports and failing tests for the Objecten hop, ahead of the implementation: - IRegisterRecordGateway + RegisterRecord — the Application-side port; the record mirrors the objecttype schema registered in S-18c (ADR-0027). - AclService takes the port but does not yet call it, so the approval test fails on an empty upsert list. - ObjectenGateway is a shell throwing NotImplementedException; its tests pin the contract: resolve the objecttype by name, search by data attribute, POST when absent / PATCH when present, static Token auth per API, the CRS headers the geo API requires, and a surfaced error body. Also splits S-19 (#20) into #149/#150 in BACKLOG.md — the approval-side write and the projection re-sourcing are independently deployable (CLAUDE.md §13). |
||
|
|
d37d4c96c6 |
S-18c · RegisterRecord objecttype defined + registered (closes #141) (#146)
## What & why S-18c, the **final** slice of the S-18 (#19) split (after S-18a #142, S-18b #143). Defines the **RegisterRecord** objecttype — the schema S-19 (#20) will write canonical register records against on approval — and registers it in the Objecttypen API at startup. Closes #141 ### What - **Schema** (`infra/objecttypen-registerrecord/registerrecord.schema.json`): public-safe by construction — `id`, `status` (enum `INGEDIEND`/`INGESCHREVEN`), `reference` only, `additionalProperties: false`, `dataClassification: open`. Mirrors the BFF's `OpenbaarEntry` — **no `bsn`/`naam`** (ADR-0027). - **Registration**: a `registerrecord-init` compose one-shot (stdlib Python on the stack network) POSTs the objecttype + a **published** version over the API once Objecttypen is healthy. The Objecttypen `setup_configuration` (3.4.2) only provisions tokens — no declarative objecttype step — so this follows the ADR-0020 self-seed pattern. **Idempotent**: if a `RegisterRecord` with a version already exists it is a no-op. - **Wiring**: schema + `register.py` streamed into the external `rr-registerrecord-config` volume by `seed-config.sh registerrecord` (main) / bind-mounted (local); added to `SEED`, `CFG_VOLS`, and the CI log-dump. `registerrecord-init` is a one-shot (not in `WAIT_SVCS`). - **Smoke**: `verify-registerrecord` (`run-registerrecord-check.sh` + `registerrecord-check.py`) asserts the objecttype exists, has a **published** version, and that version's schema carries `id`/`status`/`reference`; added as a verify-stack step + a row in the #136 summary. - **ADR-0027**: records the public-safe schema decision (mirror the BFF public view, not the internal projection; API-seeded one-shot). The slice issue #141 flagged the schema as ADR-worthy, so no separate adr-proposal issue was opened. ## Verified locally (end to end, real compose) Seeded `rr-registerrecord-config`, brought Objecttypen up, ran `registerrecord-init` → `registered RegisterRecord <uuid> v1 (published)`. `make verify-registerrecord` → **OK — RegisterRecord v1 published, fields=['id', 'reference', 'status']**. Re-running the one-shot → **no-op** (idempotent). `docker compose config` clean on both files; schema + script + ci.yaml validated. ## Definition of Done - [x] Failing smoke committed first (`test(infra): …`, "no objecttype named RegisterRecord"); implementation makes it pass. - [x] Conventional Commits referencing #141. - [x] CI green (verify-stack registerrecord step — validated locally; runner already unstarved by #145). - [x] `docker compose up` reaches health (one-shot registers after Objecttypen healthy). - [x] Docs: ADR-0027 + demo note. - [x] Closed by the merging PR (`closes #141`). This closes out the S-18 (#19) split — Objecttypen (S-18a) + Objecten (S-18b) + RegisterRecord (S-18c) are all up. Next: **S-19 (#20)** — ACL writes the register record to Objecten on approval, against this schema. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #146 |
||
|
|
159f014c1e |
perf: cap OpenZaak + NRC uWSGI workers — shrink verify-stack footprint (closes #147) (#148)
## What & why Closes #147. Follow-up to #144/#145. As the stack grew to **37 services** on one runner, `verify-stack` is under memory pressure. #145 capped Objecten/Objecttypen; this caps the two biggest remaining uncapped Django apps. **OpenZaak** and **NRC** (`nrc-web`) are Maykin/vng uWSGI images running the image default of **4 processes × 4 threads** — ~4 full-Django worker processes (~800 MB) each, idle, serving only single-request smoke checks. ### What - `UWSGI_PROCESSES: "1"` + `UWSGI_THREADS: "2"` on the `&oz-env` and `&nrc-env` anchors, in both compose files. Frees ~1.2 GB. The anchors are shared with the `-init` (setup_configuration) and `-celery` containers, which ignore the var — they don't run uwsgi. ### Not included (considered, deferred to #147 notes) JVM heap caps on Keycloak/Flowable; compose profiles to boot per-check subsets. ## Verified locally OpenZaak brought up healthy with the cap; uwsgi processes **6 → 3** (master + http-router + 1 worker); `/admin/` still 302. `docker compose config` clean on both files. (Full NRC bring-up needs OpenZaak + the seed chain — same image family/lever, validated via OpenZaak.) ## Definition of Done - [x] Linked issue (#147). - [x] Conventional Commit referencing #147. - [x] Verified locally (OpenZaak healthy + worker count dropped + still serving). - [x] Closed by the merging PR (`closes #147`). No ADR: config-only tuning of existing services, same class as #145. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #148 |
||
|
|
dd54688f86 |
fix: cap Objecten/Objecttypen uWSGI to 1 worker — unstarve verify-stack e2e (closes #144) (#145)
## What & why **P0 — red `main`.** Fixes #144: `verify-stack` fails on the Playwright e2e step (main runs 2177 after #142, 2190 after #143), while the PR runs passed. Closes #144 ### Root cause The Maykin **Objecttypen** (S-18a) and **Objecten** (S-18b) images run their `web` under uWSGI with **4 processes × 4 threads by default** (`UWSGI_PROCESSES:-4`). Two web services × 4 idle Django workers (~200 MB each) sat idle during the e2e step and starved the single shared self-hosted runner — Keycloak and the portals stopped responding (the login `#username` never appeared) and Chromium hit `Target crashed`. The runner margin was already thin; the second chain tipped it over (main green through run 2159, red from 2177). ### Fix Cap `UWSGI_PROCESSES: "1"` + `UWSGI_THREADS: "2"` on both `objecten` and `objecttypen` in both compose files. These APIs only serve single-request smoke checks and are idle during e2e, so 1 worker is plenty — it frees ~1–1.5 GB. The `-init` containers ignore it (they run `setup_configuration`, not uwsgi). ## Verified locally Brought the objecten chain up with the cap: both services reach healthy, worker count drops from 6 (master + http + 4 workers) to 3 (master + http + 1 worker) per service, and `make verify-objecten` / `make verify-objecttypen` both still → **OK — no-auth 401, token 200**. `docker compose config` clean on both files. ## Definition of Done - [x] Linked issue (#144). - [x] Conventional Commit referencing #144. - [x] Verified locally (both APIs healthy + smoke green with 1 worker). - [x] Closed by the merging PR (`closes #144`). No ADR: config-only tuning of existing services — no boundary, dependency, or coupling change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #145 |
||
|
|
0a97fa4bf7 |
S-18b · Objecten API up in compose, wired to Objecttypen (closes #140) (#143)
## What & why S-18b, second of the S-18 (#19) split (after S-18a #139/#142). Stands up the upstream Maykin **Objecten API** in the compose stack and wires it to the Objecttypen API — the authoritative object store the ACL will write register records to (S-19). Closes #140 ### What - **Compose** (main + local): `objecten-db` (**PostGIS** — objects carry geometry), `objecten-redis`, `objecten-init` (RUN_SETUP_CONFIG → migrate + provision token + register the Objecttypen service), `objecten` web (health on `/admin/`, host `:8021`). Verbatim upstream image `maykinmedia/objects-api` pinned to `3.4.0` (nearest release to objecttypes-api `3.4.2`; the two speak over the stable Objecttypes API v2). - **Seed**: `infra/seed-config.sh objecten` streams `infra/objecten/setup_configuration/data.yaml` into the external `rr-objecten-config` volume — same pattern as S-18a. The data.yaml (1) registers **Objecttypen** as a trusted `zgw_consumers` service (`api_type: orc`, api-key auth with the S-18a dev token) so an object can reference its objecttype, and (2) provisions a dev **static API token** so peers (the ACL, S-19) can write objects. - **Wiring**: added to `WAIT_SVCS`, `CFG_VOLS`, the `SEED` invocations, `seed-config.sh`, and the CI log-dump. `objecten-init` waits on `objecttypen` being healthy so the service registration is meaningful end to end. - **Smoke**: `verify-objecten` (`infra/run-objecten-check.sh` + `objecten-check.py`) asserts unauth → 401, token → 200 on `/api/v2/objects`; added as a verify-stack step + a row in the #136 check-summary table. ## Verified locally (end to end, real compose) Seeded + brought up the real `infra/docker-compose.yml` objecten chain (pulls in objecttypen via `depends_on`): `objecten-init` ran setup_configuration — `token_configuration_success` **and** "Successfully executed step: Configuration to connect with external services" — the web reached healthy, and `make verify-objecten` → **"OK — no-auth 401, token 200"**. Confirmed the registered service via the Objecten django shell: ``` objecttypen | orc | http://objecttypen:8000/api/v2/ | api_key ``` YAML (both compose files + ci.yaml) + shell + python all validated; `docker compose config` clean on both files. ## Definition of Done - [x] Failing smoke committed first (`test(infra): …`, "no running objecten container"); implementation makes it pass. - [x] Conventional Commits referencing #140. - [ ] CI green (verify-stack objecten step). - [x] `docker compose up` reaches health (objecten healthy on first poll locally). - [x] Demo note in `docs/demo-script.md`. - [x] Closed by the merging PR (`closes #140`). No new ADR: follows the established verbatim-image + seed-config CG-module pattern (S-18a/ADR-0023-era). 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #143 |
||
|
|
23ea91de32 |
feat(infra): Objecttypen API up in compose with a seeded static token (closes #139) (#142)
## What & why S-18a, first of the S-18 (#19) split. Stands up the upstream Maykin **Objecttypen API** in the compose stack — the objecttype catalogue the register record (S-18b/S-18c, S-19) will build on. Closes #139 ### What - **Compose** (main + local): `objecttypen-db` (Postgres), `objecttypen-redis`, `objecttypen-init` (RUN_SETUP_CONFIG → migrate + provision token), `objecttypen` web (health on `/admin/`, host `:8020`). Verbatim upstream image `maykinmedia/objecttypes-api` pinned to `3.4.2`. - **Seed**: `infra/seed-config.sh objecttypen` streams `infra/objecttypen/setup_configuration/data.yaml` into the external `rr-objecttypen-config` volume — same pattern as OpenZaak/NRC. The data.yaml provisions a dev **static API token** (`tokenauth` setup_configuration step) so peers (Objecten, ACL) can authenticate. - **Wiring**: added to `WAIT_SVCS`, `CFG_VOLS`, the `SEED` invocations, and the CI log-dump. - **Smoke**: `verify-objecttypen` (`infra/run-objecttypen-check.sh` + `objecttypen-check.py`) asserts unauth → 401, token → 200; added as a verify-stack step + a row in the #136 check-summary table. ### Split note #19 was oversized (two CG modules + config + objecttype) → split (§13) into **S-18a** (this), **S-18b** (#140, Objecten wired to Objecttypen), **S-18c** (#141, RegisterRecord objecttype). ## Verified locally (end to end, real compose) Seeded + brought up the real `infra/docker-compose.yml` objecttypen chain: `objecttypen-init` ran setup_configuration (`token_configuration_success`), the web reached healthy, and `make verify-objecttypen` → **"OK — no-auth 401, token 200"**. YAML (both compose files + ci.yaml) + shell + python all validated. ## Definition of Done - [x] Smoke check validates the outcome (live, against the running stack). - [x] Conventional Commits referencing #139. - [ ] CI green — see note. - [x] `docker compose up` reaches health (objecttypen healthy on first poll locally). - [x] Demo note in `docs/demo-script.md`. ## Note on CI Additive (a new service + its own smoke step). The fast jobs are unaffected. The **verify-stack** job still can't go green until the pre-existing 1.27/act_runner-2.0.0 bring-up P0 is resolved (fails on plain `main` too) — but the objecttypen bring-up itself is validated locally above. No new ADR: this follows the established verbatim-image + seed-config CG-module pattern (ADR-0023-era). 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #142 |
||
|
|
0494730223 |
feat(portal-beheer): ACL default-fill configuration editor (closes #131) (#138)
## What & why S-15b, second of the S-15 (#16) split, on top of S-15a (#133). A beheerder edits the ACL's ZGW **default-fill** values from the beheer portal, and the next zaak is stamped with the new values — no restart. Closes #131 ### The vertical portal → BFF `GET/PUT /beheer/default-fill` (medewerker realm + `beheerder` role) → ACL `GET/PUT /default-fill` → a runtime-mutable in-memory store the ACL reads **per zaak**. - **ACL**: `IDefaultFillStore` / `InMemoryDefaultFillStore` (thread-safe, seeded from `Acl:Defaults`); `AclService` reads `fill.Current` per zaak (not cached at construction); `GET`/`PUT /default-fill` with required-field validation. - **BFF**: `IAclClient` gains `GetDefaultFillAsync`/`UpdateDefaultFillAsync`; `GET`/`PUT /beheer/default-fill` behind the `beheerder` policy. OpenAPI + generated client regenerated. - **Frontend**: a *Default-fill* editor page in the beheer app (load → edit → save, with saved/failure states) + nav between Catalogus and Default-fill. ### Scope decision → ADR-0026 Only the **three ZGW fill fields** (bronorganisatie, verantwoordelijke organisatie, vertrouwelijkheidaanduiding) are editable. The S-27 catalog-resolution keys stay **static config** — editing them would desync the zaaktype-URL cache (ADR-0021), and they're catalogus wiring, not "default fill". The store is **in-memory** (seeded from config): an edit reverts on restart. That's the reference-app-appropriate ceiling (no DB added to the stateless ACL); upgrade path documented. Recorded in **ADR-0026**. ## Verified locally lint (`dotnet format`) ✓ · .NET unit — acl 60 / bff 45 / domain 152 / event-subscriber 19 / acceptance 17 ✓ · frontend lint+test (8 projects) ✓ · beheer build ✓. Clean full-solution build (caught + fixed the acceptance `AclService` ctor drift). TDD red→green per layer (ACL store, ACL endpoints, BFF, frontend). ## Definition of Done - [x] Failing test committed before each implementation (red→green per layer). - [x] Conventional Commits referencing #131. - [ ] CI green — see note below. - [x] Docs: ADR-0026 + S-15b demo note. - [x] Demo note in `docs/demo-script.md`. ## Note on CI The bulk validates in the fast jobs (lint/build/unit/frontend/mutation). The **verify-stack e2e** (incl. the new `default-fill.spec.ts`) can't go green until the pre-existing **verify-stack bring-up failure on the 1.27/2.0.0 runner** is resolved (that fails on plain `main` too — unrelated to this PR). Additive change; no existing e2e touched. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #138 |
||
|
|
fff88ca23d |
ci: richer step reports via Gitea 1.27 job summaries (closes #136) (#137)
## What & why Use the standard `$GITHUB_STEP_SUMMARY` (Gitea 1.27 + act_runner 2.0.0) to surface on the run page what was previously buried in logs or download-only artifacts. All five quick wins from #136, **reporting-only** — no job's pass/fail gating changes. Closes #136 ### Items 1. **Mutation scores** — added the `markdown` reporter to each `stryker-config.json`; the `mutation` job concatenates each service's `mutation-report.md` into the summary (`if: always()`). Also reveals where `make mutation` stopped on a ratchet break. 2. **Per-frontend tests** — the 4 apps' `test` targets emit vitest JSON to `test-output/{projectName}.json` (Nx token interpolation); `infra/vitest-summary.py` renders a per-frontend table. 3. **Per-service unit tests** — `make unit` now writes TRX; `infra/trx-summary.py` renders a per-service table (service name derived from the `services/<name>/` path, so `domain` shows, not `big.tests`). 4. **e2e per-spec results** — Playwright writes `playwright-report.json`; `run-e2e-check.sh` copies it out of the container (capturing the exit code first); `infra/playwright-summary.py` renders a per-spec table. Turns a red e2e into a one-glance "which spec". 5. **verify-stack check table** — each live-stack check has an `id`; a final `if: always()` step tabulates each check's ✅/❌/⏭️. Docs: `gitea-actions-gotchas.md` §8 (version requirement + `$GITHUB_STEP_SUMMARY` guard + step-level `always()` note). ### Notes - Every summary write is guarded with `[ -n "${GITHUB_STEP_SUMMARY:-}" ]`, so it no-ops on an unsupported runner / locally. - New helper scripts are stdlib-only Python, matching the existing `infra/*.py` check scripts (no new dependency — a few lines of parsing rather than a test-logger package). - `TestResults/` and `test-output/` gitignored. - This is also the first PR-run exercising the #135 verify-stack fix end to end. ## Verified locally `make unit` (TRX) ✓ · 4 apps' vitest JSON ✓ · ACL Stryker markdown report ✓ · all four parsers + the two summary shell blocks ✓ · `ci.yaml` + `run-e2e-check.sh` syntax ✓. The rendered summaries themselves only appear on the run page — this PR's CI run is the end-to-end check. ## Definition of Done - [x] Each item writes to `$GITHUB_STEP_SUMMARY` (guarded), renders on the run page. - [x] No change to any job's pass/fail gating. - [x] Conventional Commits referencing #136 (one per item + docs). - [ ] CI green; summaries visible on the run. - [x] Runbook note (gotchas §8). 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #137 |
||
|
|
849bf4723b |
ci: unstick verify-stack on Gitea 1.27 + act_runner 2.0.0 (closes #134) (#135)
## What & why After the Gitea 1.27 + act_runner 2.0.0 upgrade, `verify-stack` never starts: the run sits in `waiting` forever with no logs for that job, while the other five jobs pass — so `main` stays pending/red (P0). See #134. Closes #134 ### Root cause `verify-stack` was the only job gated by a status-function `if` on top of `needs`: ```yaml verify-stack: needs: [mutation] if: ${{ !cancelled() }} ``` Gitea 1.27 reworked cancellation/aggregation so that `always()`/`cancelled()`-gated `needs` jobs route through a new transitional **`Cancelling`** state + server↔runner **capability negotiation** ("Requires Gitea Runner 2.0.0"). On this 1.27 + 2.0.0 pairing that handshake doesn't resolve, so the job is never dispatched and never leaves `waiting`. Plain jobs (no `if`/`needs`) are unaffected — exactly the observed pattern. It worked pre-upgrade (old runner). ### Fix Drop the `if: ${{ !cancelled() }}`; keep `needs: [mutation]`. Default `if: success()` dispatches normally and still serialises the two memory-heavy jobs (OOM avoidance, #126). **Trade-off:** the `!cancelled()` (added in #127) let verify-stack run even when the mutation ratchet fails. Now a failing mutation skips verify-stack; the fix-and-re-push re-run exercises it, so the signal isn't lost — just deferred to the green-mutation run. If we later want both signals on one run, serialise via a `concurrency` group rather than `needs` + `always()`. Documented as §7 in `docs/runbooks/gitea-actions-gotchas.md`. ## Note on the stuck run Run 582 (the #133 merge) will **not** clear itself and must be force-cancelled from the Actions UI (plain cancel can also stall on this version, gitea#35782). This PR's own run is the first real test of the fix — if `verify-stack` dispatches and runs here, the fix holds. ## Definition of Done - [x] Linked issue (#134). - [x] Conventional Commit referencing the issue. - [ ] CI green — this PR's run is the verification (verify-stack must dispatch). - [x] Runbook updated (gotchas §7). - [ ] Closed by the merging PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #135 |
||
|
|
4698c869f3 |
feat(portal-beheer): beheer portal + read-only catalogus viewer (closes #130) (#133)
## What & why S-15a, the first of the S-15 (#16) split. A new **beheer** portal (medewerker realm, like behandel) shows the ZTC catalogus — the published zaaktypen — **read-only**. A beheerder logs in and sees the seeded BIG-REGISTRATIE zaaktype. Closes #130 ### The vertical portal → BFF `GET /beheer/catalogi/zaaktypen` (medewerker realm + `beheerder` role) → ACL `GET /catalogi/zaaktypen` → ZGW Catalogi API. - **ACL**: new read-only `GET /catalogi/zaaktypen` listing published zaaktypen (reuses the ADR-0021 Catalogi client; public-safe `identificatie`/`omschrijving`). - **BFF**: new typed `IAclClient` + `Downstream:Acl:BaseUrl`, and `GET /beheer/catalogi/zaaktypen` behind a new `beheerder` policy (reuses the medewerker bearer scheme + realm-role lifting). OpenAPI spec + generated Angular client regenerated. - **Keycloak**: `beheerder` realm role + `bram-beheerder` test user in the medewerker realm. - **Frontend**: new `apps/beheer` Angular app (copied from behandel) with a read-only catalogus page; `SECURE_API_ROUTES=['/beheer/']`. - **Infra**: `beheer` compose service (port 8143), added to `WAIT_SVCS` + CI log-dump; a Playwright e2e (beheerder login → catalogus shows BIG-REGISTRATIE). ### New boundary → ADR-0025 The BFF now reaches the **ACL directly** for the catalogus read — a new service-to-service edge (§14). The catalogus is neither a domain nor a projection concern, and §8.1 means only the ACL may read ZGW; routing through the domain would pollute it with a non-domain passthrough. §8.1/§8.3 stay intact. Recorded in **ADR-0025**. ## Definition of Done - [x] Failing test committed before each implementation (red→green per layer: ACL, BFF, frontend). - [x] Conventional Commits referencing #130. - [ ] CI green — pending Gitea Actions run. - [x] `docker compose up` brings up `beheer` (health-gated in `WAIT_SVCS`). - [x] Docs — ADR-0025 + demo-script S-15a note. - [x] Demo note in `docs/demo-script.md`. ## Verified locally lint (`dotnet format`) ✓ · .NET unit (Acl 57 / Big 152 / EventSubscriber 19 / Bff 40) ✓ · frontend lint+test (8 projects) ✓ · frontend build (4 apps) ✓. Mutation ratchet: added a gateway unit test for the new `ListZaaktypenAsync` mapping so the ACL score holds. verify-stack (compose smoke + e2e) runs in CI. ## Notes for reviewers - The BFF drops the ZGW URL from `BeheerZaaktype` (public-safe: identificatie + omschrijving only). - The catalogus e2e asserts on the stable seeded `BIG-REGISTRATIE` (not a per-test reference), safe on the shared verify stack. - Follow-ups: **S-15b** (#131) default-fill CRUD, **S-15c** (#132) medewerker-realm MFA. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #133 |
||
|
|
d5dfbdc0b2 |
feat(obs): Prometheus metrics on /metrics + golden-signal Grafana dashboard (closes #124) (#129)
## What & why S-16c, the last of the S-16 (#17) split, on top of the backplane (#122) and distributed tracing (#123). The five .NET services now expose OpenTelemetry **metrics** in Prometheus format at `/metrics`; Prometheus scrapes each (one job per service); and Grafana ships a pre-built **Request path — golden signals** dashboard (traffic / errors / latency / saturation), split by service. Closes #124 ### How - Each service adds `.WithMetrics(AddAspNetCoreInstrumentation + AddHttpClientInstrumentation + AddMeter("System.Runtime") + AddPrometheusExporter)` and maps `/metrics`. Same shape as the S-16b tracing wiring already in these `Program.cs` files. - `infra/observability/prometheus/prometheus.yml`: one scrape job per service (`acl`, `domain`, `bff`, `event-subscriber`, `projection-api`), reached by compose service name. - `infra/observability/grafana/provisioning/dashboards/`: dashboard provider + `golden-signals.json` (baked into the Grafana image by the existing `COPY provisioning/`). - `verify-metrics` (new CI verify-stack step + Makefile target): generates BFF traffic and asserts Prometheus scraped the golden-signal metric from every service. Mirrors `verify-tracing`. ### Dependency (CLAUDE.md §13/§14) Adds `OpenTelemetry.Exporter.Prometheus.AspNetCore` `1.17.0-beta.1` (matched to the `1.17.0` core already in use). It gives the OTel-native `/metrics` pull endpoint; replacing it would mean hand-rolling Prometheus exposition over a `MeterListener`; the risk is that it is a **prerelease** package (the whole OTel .NET Prometheus line is `-beta`) — pinned, wired only in `Program.cs`, and gated by `verify-metrics`. Recorded in **ADR-0024**. ## Definition of Done - [x] Linked Gitea issue (#124). - [x] Failing test committed before the implementation (`test(bff): /metrics exposes http-server request duration`). - [x] Implementation makes the test pass. - [ ] CI green — pending Gitea Actions run. - [x] `docker compose up` reaches green health within 3 min (backplane images unchanged in shape; not on the health gate, ADR-0023). - [x] Docs updated — demo-script S-16c entry. - [x] ADR added — ADR-0024. - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - `/health` polls are counted as traffic (metrics aren't path-filtered, unlike traces). Fine for a demo dashboard and honest — real load stacks on top. - `projection-api` has no Stryker config (unchanged); the four mutated services carry the metrics wiring in `Program.cs`, same as the merged S-16b tracing code. - Metric names verified against a live service: `http_server_request_duration_seconds{,_bucket,_count}`, label `http_response_status_code`, `dotnet_process_cpu_time_seconds_total`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #129 |
||
|
|
6771fccf47 |
ci: parallelise jobs at runner capacity >1, keep heavy jobs apart (closes #127) (#128)
## What & why
The runner's `capacity` was raised to 2. The six CI jobs have no `needs:` between them, so they already schedule concurrently now — this PR makes that safe and tidy rather than enabling it.
- **Keep the two memory-heavy jobs apart.** `verify-stack` now `needs: [mutation]` — not a data dependency, but so Stryker and the full-stack-bring-up + Playwright browser never run at once on the one host and re-trigger the e2e OOM (#126, commit
|
||
|
|
88338396f6 |
feat(obs): distributed traces across the .NET services (S-16b, closes #123) (#126)
## What & why S-16b, second of the S-16 split, on top of the #125 backplane. The five .NET services now emit OpenTelemetry traces so a request is **one connected trace** across them. - Each host wires `AddOpenTelemetry().WithTracing(...)` with `AddAspNetCoreInstrumentation` (incoming) + `AddHttpClientInstrumentation` (outgoing) + `AddOtlpExporter` to **Tempo**. - Because every cross-service call already goes through a typed `HttpClient` (§8 boundaries), the W3C `traceparent` propagates with no manual code — bff → domain → acl → openzaak and bff → projection-api stitch into a single trace. - Service name + OTLP endpoint come from `OTEL_*` env set per app service in compose. `/health` is filtered out so liveness polls don't flood the traces. No new ADR — ADR-0023 already records the stack + the two documented gaps (browser-side tracing is out of scope, so the trace begins at the BFF; the async Flowable-poll boundary is a separate trace). Closes #123 ## Definition of Done - [x] Failing test committed first (`verify-tracing` fails with no instrumentation). - [x] Implementation makes it pass — **validated locally end to end**: a real connected trace spanning `bff` + `projection-api` was found in Tempo (BFF→projection→db + Tempo subset, no OpenZaak/egress). - [x] Conventional Commits referencing the issue (`refs #123`). - [ ] CI green — awaiting Gitea Actions (verify-tracing added to verify-stack after verify-bff). - [x] `docker compose up` health unaffected — services boot healthy even when Tempo is unreachable (exporter no-ops; verified). - [x] Docs — demo-script + BACKLOG. - [x] ADR — none needed (covered by ADR-0023). ## Notes for reviewers - **Per-service wiring, no shared lib:** the block is duplicated across the five hosts by design — services don't share code across boundaries here (§8), same as the duplicated typed clients. - **Packages:** OpenTelemetry.Extensions.Hosting / Instrumentation.AspNetCore / Instrumentation.Http / Exporter.OpenTelemetryProtocol, all 1.17.0, pinned per-csproj (no central props file). - **The check** generates anonymous BFF→projection traffic (no auth, no OpenZaak), then queries Tempo (TraceQL search → fetch trace → assert both service.names present) from a python:3-slim container in-network — same idiom as run-projection-check.sh. - **Next:** #124 (S-16c) adds `/metrics` + Prometheus scrape targets + golden-signal Grafana dashboards. Reviewed-on: #126 |
||
|
|
4274fd30d1 |
feat(infra): observability backplane — Tempo + Prometheus + Grafana (S-16a, closes #122) (#125)
## What & why S-16a, the first of the **S-16 split** (#17 closed → #122/#123/#124, §13). Stands up a local, CI-friendly observability backplane so traces (S-16b) and metrics (S-16c) have somewhere to land, viewable in one Grafana. - **Grafana Tempo** — OTLP trace ingest (gRPC 4317 / HTTP 4318), local storage. - **Prometheus** — scrapes itself for now; service `/metrics` targets arrive in S-16c. - **Grafana** — Tempo + Prometheus datasources auto-provisioned with fixed uids (`tempo`, `prometheus`), exposed on :3000. All three are small **built images** with config baked in (`infra/observability/`), on the existing `cg` network. **No OTLP collector** (Tempo ingests OTLP directly; Prometheus scrapes) and **no config-volume seeding** — the tools aren't verbatim CG peer modules, so a 3-line `COPY` Dockerfile is the simpler path that still reaches sibling containers on the CI runner (**ADR-0023**). ### Verified, not assumed `make verify-observability` (new CI `verify-stack` step, run early) asks Grafana to reach both datasources — Prometheus via its health method, Tempo via the datasource proxy (Tempo's plugin implements no health method) — so it proves the datasources are wired, not merely that containers booted. Validated locally against the three containers (no external egress): Grafana healthy, both datasources reachable. Closes #122 ## Definition of Done - [x] Failing test committed first (`verify-observability` fails with no backplane). - [x] Implementation makes it pass; verified locally. - [x] Conventional Commits referencing the issue (`refs #122`). - [ ] CI green — awaiting Gitea Actions (verify-stack now includes the observability step; `docker compose config` validates locally). - [ ] `docker compose up` reaches green health within 3 min — new containers are lightweight and off the health-gate list. - [x] Docs — ADR-0023, demo-script, BACKLOG sync. - [x] ADR added — `docs/architecture/adr-0023-observability-stack.md`. - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - **No app changes** — this is pure infra; the five services are untouched (instrumentation is #123/#124). - **Ports:** Grafana 3000 (admin/admin, anonymous viewer on), Prometheus 9090; Tempo internal to `cg`. - **CI:** the three containers are added to the failure log-dump list; deliberately **not** added to `WAIT_SVCS` (the check polls Grafana itself, so no in-image healthcheck tool is needed). Trades ~3 small image builds per run. - **Next:** #123 wires OTLP export + `AddAspNetCoreInstrumentation`/`AddHttpClientInstrumentation` into the five hosts so a request becomes one connected trace in Tempo. Reviewed-on: #125 |
||
|
|
4fe9915816 |
feat(domain): herregistratie reminder sweep on a Quartz cron (S-17, closes #18) (#121)
## What & why
S-17: a BIG inscription is valid for a fixed term; before it lapses the zorgprofessional must herregistreren. This adds a **daily herregistratie reminder sweep**.
- **Domain:** `Approve(ingeschrevenOp)` now stamps the inscription moment; `HerregistratieVoor` derives the deadline (inscription + 5-year validity); `HerregistratieReminderDue(asOf)` is the single rule (inside the 90-day window, inscribed, not yet reminded); `MarkHerregistratieReminderVerstuurd()` is idempotent.
- **Store:** `FindDueForHerregistratieReminderAsync(asOf)` — the sweep's candidate set, filtered on the aggregate's own rule (no duplicated policy).
- **Application:** `HerregistratieReminderSweep` — pure over the store + an injected `TimeProvider`; flags + persists each due inscription, returns the reminded ids.
- **Infra/API:** `HerregistratieReminderJob` (Quartz `IJob`) fires the sweep on a daily cron (03:00, overridable via `Quartz__Cron`) and logs the count. `GET /registrations/{id}` surfaces `herregistratieVoor` + `herregistratieReminderVerstuurd`.
**Decisions (both raised with you before coding):** use Quartz.NET as the PRD names it — a genuine cron concern, distinct from the queue-draining pumps, which stay as-is (**ADR-0022**, proposal #120); and the reminder's observable effect is a flag on the aggregate + a log line (no outbound notification infra in v1). No coupling rule (§8) is touched — Quartz is internal to the Domain Service.
Closes #18
Closes #120
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation (red→green per layer: domain rule, store query, sweep).
- [x] Implementation makes the test pass; refactor commit for the 90-day knob.
- [x] Conventional Commits referencing the issue (`refs #18`).
- [ ] CI green — awaiting Gitea Actions.
- [ ] `docker compose up` reaches green health checks within 3 minutes — API boots locally with Quartz initialised; verified in CI compose smoke.
- [x] Docs updated — ADR-0022, demo-script, BACKLOG.
- [x] ADR added — `docs/architecture/adr-0022-quartz-scheduler.md`.
- [x] Demo note in `docs/demo-script.md`.
## Notes for reviewers
- **Ripple:** `Approve()` gained the inscription moment, so the two approving handlers (`ApproveRegistration`, `BeoordeelRegistratie`) now take an injected `TimeProvider`; existing tests pass a fixed clock. All three `IRegistrationStore` implementers (prod, unit fake, acceptance) got the new query.
- **Calibration knobs:** validity (5y) and reminder lead time (90d) are domain constants marked with `ponytail:` comments; promotion path to beheer config (S-15) noted in the ADR.
- **Mutation:** the Quartz job shell is excluded from Stryker, mirroring the pumps; all rule/sweep/query logic is covered.
- Local: 152 domain unit tests green; API boots with the Quartz scheduler and `/health` green.
Reviewed-on: #121
|
||
|
|
5f8ab4dbcd |
feat: self-service resume of an existing registration after refresh (S-26, closes #111) (#119)
## What & why After submitting, the self-service portal held the registration only in in-memory signals, so a **page refresh stranded an in-flight registration** — the reference and its "Documenten aanleveren" / "Trek aanvraag in" actions were lost, with no way back (the reference wasn't in the URL and there was no read endpoint). This is the gap a citizen hit in testing. Now the portal **resumes on load**: - **Domain:** `IRegistrationStore.FindOpenByBsnAsync` (the citizen's non-terminal INGEDIEND/IN_BEHANDELING registration) + `GET /registrations/current?bsn=`. - **BFF:** owner-scoped `GET /self-service/registrations` (bsn from the DigiD token) → the current registration, or **204** when none. Regenerated `services/bff/openapi.json`. - **Frontend:** `registration-page` calls it on init and restores the submitted view (reference + actions); 204 shows the submit form as before. api-client regenerated (orval). Closes #111 ## Definition of Done - [x] Linked issue (#111). - [x] TDD — store `FindOpenByBsnAsync` tests, BFF endpoint tests, an Angular component test (resume-on-load), a Playwright e2e (submit → reload → restored). - [x] Conventional Commits referencing #111. - [ ] CI green — validated locally (below); runner CI running. - [x] `docker compose up` reaches green health — fresh stack + full e2e (3 specs) green. - [x] Docs — `docs/synthetic-data.md` (new e2e users). - [ ] ADR — N/A (follows existing BFF/domain patterns; no boundary change). - [ ] Demo note — the flow is unchanged for the demo; no new demo-script section (happy to add one if wanted). ## Verified locally - Unit: Big 141 (+7 store tests), Bff 36 (+3 endpoint tests), all suites green. - Frontend: 12 self-service component tests (incl. resume-on-load); lint + build green. - **e2e (fresh CI stack): all 3 specs pass** — `registration`, `resume`, `withdrawal` (29.5s, single worker). - Mutation: domain **91.04%**, bff **100%** (break 90%). `make lint` clean. ## Notes for reviewers - **Shared-stack isolation:** resume-on-load restores any open registration for the logged-in bsn, so the self-service e2e specs can no longer share `jan-burger` (the verify-* API checks submit as `jan-burger`/`123456782` before the e2e). Each spec now has its own DigiD citizen (`emma`/`sanne`/`lars`-burger); `jan-burger` stays the documented citizen for the verify checks. This is the fix for the two intermittent e2e failures seen during development. - **Scope:** resumes the current **in-flight** registration only (terminal ones aren't resumed), per the issue's out-of-scope note. Reviewed-on: #119 |
||
|
|
5de8c1e292 |
feat(acl): resolve the zaaktype by identificatie, not a pinned URL (S-27, closes #113) (#118)
## What & why The ACL was handed a **pinned zaaktype URL** (`Acl__Defaults__ZaaktypeUrl`) + informatieobjecttype URL. OpenZaak assigns those UUIDs at creation, so every stack had to seed the catalogus and then capture + inject the resulting URLs out of band (CI's `run-domain-check.sh`; the local `local-seed`→`acl.env` bootstrap from ADR-0020). Brittle, and a stale/placeholder URL failed opaquely (OpenZaak 400). Now **the ACL resolves them itself** from OpenZaak's Catalogi API by stable business key: - config `ZaaktypeIdentificatie` (`BIG-REGISTRATIE`) / `InformatieobjecttypeOmschrijving` (`Diploma`); - a `CachedZaaktypeCatalog` resolves **lazily on first use** and caches (success only, so a pre-publish miss is retried — no startup ordering coupling); - a clear "No published … found" error replaces the opaque placeholder 400. Design in **ADR-0021** (proposed in #117). Closes #113 Closes #117 ## Consequences (the payoff) No stack captures/injects a server-assigned URL any more — `docker-compose.yml`/`.local.yml`, `run-domain-check.sh` and `local-seed` all drop it; the local `acl.env` shrinks to a single line. **One thing S-27 can't remove** (confirmed empirically during this work): OpenZaak validates the `zaaktype` field on zaak-create with Django's URLValidator and **rejects a single-label host** (`http://openzaak:8000/…` → `zaaktype: bad-url`). So the ACL's **base URL** must still point at a URL-valid host (a container IP); that base-URL injection from ADR-0020 stays (local `acl.env` now carries only it; CI keeps `ACL_OPENZAAK_BASEURL`). ADR-0021 records this. ## Definition of Done - [x] Linked issues (#113 slice, #117 adr-proposal). - [x] TDD — resolver + gateway-lookup unit tests, updated `AclService` tests (50 unit tests green). - [x] Implementation makes them pass; refactor of both compose stacks + verify scripts follows. - [x] Conventional Commits referencing #113. - [ ] CI green — see below. - [x] `docker compose up` reaches green health — verified: fresh `make local` + `make verify-local` green with **no zaaktype-URL injection**; `acl.env` is base-URL-only. - [x] Docs — ADR-0021 + demo-script S-27 note. - [x] ADR added (ADR-0021). - [x] Demo note appended. ## Verification done locally - **50 unit tests** pass (resolver resolve/cache/retry-on-failure; gateway match/miss/blank-key; all `AclService` paths). - **6 ACL integration tests** pass against a live seeded OpenZaak — incl. resolving the zaaktype + Diploma iot by business key, and a clear error for an unknown identificatie. - **Fresh `make local` + `make verify-local`**: full flow (submit → werkbak → openbaar) green; `acl.env` = `Acl__OpenZaak__BaseUrl` only. - `make lint` clean; ACL mutation ratchet run locally (see checks). ## Notes for reviewers - `IZaakGateway` gains two resolve methods; `AclService` depends on the new `IZaaktypeCatalog` (singleton, so the cache persists). - Supersedes the pinned-URL mechanism; ADR-0021 documents that ADR-0020's `seed-env`/entrypoint shim are **simplified** (base-URL only), not deleted, because of the URLValidator constraint above. Reviewed-on: #118 |
||
|
|
183d0bce31 |
fix(infra): docker-compose.local self-seeds zaaktype, DMN + NRC abonnement (closes #110) (#114)
## What & why The host-browser stack (`make local`) had drifted behind three slices, so a fresh bring-up couldn't complete the flow: registrations stuck at `OpenZaakAanmaken`, the behandel werkbak stayed empty, and the openbaar register showed nothing. The `verify-*` scripts do this setup for CI at test time; `make local` had no equivalent. This makes the local stack **self-seed at bring-up** so it just works in a browser: - **DMN** — `flowable-init` now also deploys `diploma-eligibility.dmn` (was BPMN-only), so completing `WachtOpDocumenten` routes through the DMN to `Beoordelen` instead of 404ing. - **Zaaktype + ACL** — a `local-seed` one-shot publishes the BIG zaaktype (whose UUID is server-assigned, hence not static in the compose file) and writes the real URLs to `seed-env:/acl.env`; the ACL sources it on startup via an entrypoint override. - **NRC abonnement** — an `nrc-subscribe` one-shot registers the `zaken` subscription at the event-subscriber callback, so notifications reach the projection/openbaar register. Both one-shots reach OpenZaak/NRC by **container IP** (a single-label host fails their Django URLValidator), mirroring the CI verify scripts. Design + trade-offs in **ADR-0020**. Closes #110 ## Definition of Done - [x] Linked Gitea issue (#110). - [x] Failing test committed before the implementation — `test(infra): …` adds `infra/run-local-flow-check.sh` / `make verify-local`; the three gaps' failures were observed live on a fresh `make local` (red), and the fix turns it green. - [x] Implementation makes the test pass; docs commit follows. - [x] Conventional Commits referencing the issue (`refs #110`). - [ ] CI green — running on the restored runner. Infra-only change; the CI `verify-stack` job uses `docker-compose.yml` (untouched). Also validated locally: `make verify-local` passes against a fresh `make local` (see below). - [x] `docker compose up` from a fresh clone reaches green health checks — verified: `make local` healthy in ~2m20s, then `make verify-local` green. - [x] Docs updated — ADR-0020 + demo-script note. - [x] ADR added in `docs/architecture/` — ADR-0020. - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - **Infra-only** — no service code changes; the ACL image and the CI stack (`docker-compose.yml`) are untouched. - **Verified end-to-end on a fresh stack** (`make local-down && make local && make verify-local`): ``` >> 2. zaak opened (zaaktype seeded + wired) >> 3. documents accepted 204 (DMN deployed) >> 4. in the werkbak (DMN routing → Beoordelen) >> 5. visible in the openbaar register (NRC abonnement) OK — a fresh local stack completed the flow with no manual seeding ``` - **Follow-up:** the cleaner design — ACL resolving its zaaktype by `identificatie` instead of a pinned server-assigned URL — is split out as **S-27 (#113)**; landing it would remove the `acl.env` injection here. ADR-0020 records this. - The `seed-env` volume carries the generated `acl.env` from `local-seed` to the ACL; a `down --volumes` (as `make local-down` does) resets it cleanly. Reviewed-on: #114 |
||
|
|
d5e5fa254c |
fix(e2e): run Playwright single-worker to stop OOM page-crash in verify-stack (closes #115) (#116)
## What & why `verify-stack` was failing intermittently on the Playwright e2e with `Page crashed` mid-action (`locator.fill`) and 90s timeouts — the run logged **"2 workers"**, i.e. two full `channel: 'chromium'` browsers running alongside the entire compose stack on the 8 GB self-hosted runner. The renderer gets OOM-killed. Tests passed only when a retry happened to run alone. Fix: pin `workers: 1` in `tests/e2e/playwright.config.ts` (there are only two long-running happy-path specs, so serial costs little) and add `--disable-dev-shm-usage`. This removes the memory contention at the source rather than leaning on `retries` (CLAUDE.md §15 — flaky tests are fixed, not retried). Closes #115 ## Definition of Done - [x] Linked Gitea issue (#115). - [ ] Failing test committed first — N/A: the "red" is the observed `verify-stack` e2e crash (`Page crashed`, 2 workers); this changes test-harness config to fix it. Verified green by re-running the e2e (see notes). - [x] Conventional Commit referencing the issue (`refs #115`). - [ ] CI green — the point of the change; `verify-stack` e2e should stop OOM-crashing. - [x] Docs — none needed (test-config only; rationale in an inline comment). - [ ] ADR — N/A. ## Notes for reviewers - One-line-of-behaviour change: `workers: 1` + `--disable-dev-shm-usage`; no product or spec changes. - `Page crashed` is a renderer OOM, not a product defect — the happy path passes when a browser runs alone (the flaky retries already showed this). Single-worker makes that the normal case. - Independent of #110 (that PR fixes `docker-compose.local.yml`; this fixes the CI `verify-stack` e2e). Landing this first unblocks #110's `verify-stack`. Reviewed-on: #116 |
||
|
|
bf234e1322 |
docs(backlog): add S-26 self-service resume slice (refs #111) (#112)
## What & why Mirror the new self-service **"resume after refresh"** slice into the Iteration 2 section of the curated backlog (`BACKLOG.md`), keeping it in sync with Gitea. Tracked as #111 (S-26). Refs #111 — **does not close it**: the backlog is the curated mirror, the slice itself stays open for implementation. ## Definition of Done - [x] Linked Gitea issue (#111). - [ ] Failing test committed before the implementation — N/A (docs-only backlog mirror). - [ ] Implementation makes the test pass; refactor commit if structure improved — N/A. - [x] Conventional Commits referencing the issue (`refs #111`). - [ ] CI green — no code paths touched; only `BACKLOG.md`. - [ ] `docker compose up` reaches green health checks — N/A. - [x] Docs updated (this IS the docs change). - [ ] ADR added — N/A. - [ ] Demo note in `docs/demo-script.md` — N/A (backlog entry, not a shipped user-visible change). ## Notes for reviewers Single-file change: adds the `S-26` entry (Outcome + Acceptance) after S-14 in Iteration 2, matching the surrounding slice format. The `S-B04` local-stack bug (#110) is intentionally **not** added — the `S-B0N` bug-slices have never been mirrored in `BACKLOG.md` (they live only in Gitea). Reviewed-on: #112 |
||
|
|
c8fdfbb699 |
feat(acl,domain): cancel the ZGW zaak on document-timeout expiry (S-10c, closes #106) (#109)
## 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 |
||
|
|
0904df8db0 |
feat(acl): diploma upload stored in the ZGW Documenten API (S-10b, closes #103) (#108)
## What & why S-10b: the self-service **diploma upload** is now real. After submitting, the citizen picks a PDF and uploads it; the portal base64-encodes it client-side → BFF → domain → **ACL**, which stores it in the ZGW **Documenten (DRC) API** as an `enkelvoudiginformatieobject` and relates it to the zaak, then the `WachtOpDocumenten` wait completes and the case advances to beoordeling. Per §8.1 only the ACL talks to ZGW. Closes #103 Mechanism in **ADR-0018** (proposal #107). Builds on S-10a (#102). The zaak-close-on-expiry item is carved to **#106 (S-10c)**. ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation (red→green per layer). - [x] Conventional Commits referencing the issue (`refs #103`). - [ ] CI green — all Gitea Actions jobs (pending on this PR). - [x] `docker compose up` health unaffected (ACL boots on a placeholder informatieobjecttype URL; the real one is injected by verify-domain). - [x] Docs updated (ADR-0018, demo-script, BACKLOG + S-10c). - [x] ADR added (`docs/architecture/adr-0018-diploma-upload-via-acl-documenten.md`). - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - **ACL** (`OpenZaakGateway.StoreDocumentAsync` + `AclService.StoreDiplomaAsync` + `POST /documenten`) reuses the existing gateway patterns (ZGW Bearer, buffered non-chunked body, **no CRS** — Documenten isn't geo). Unit-tested via the stub handler; an **integration test** stores a real document against live OpenZaak (verify-acl). - **Transport:** base64 JSON on every hop (portal encodes client-side) — I deviated from proposal #107's multipart to keep one contract shape and avoid `IFormFile`/antiforgery/multipart-client plumbing; fine at diploma size (ADR-0018 §Alternatives). - **Infra:** `seed_catalogus.py` seeds + publishes a "Diploma" `informatieobjecttype` and relates it to the zaaktype (while both concept); `verify-domain` injects its URL into the ACL. No new ZGW scopes (seed applicatie has `heeft_alle_autorisaties`). - **e2e:** uploads a real PDF (`setInputFiles`) after the openbaar INGEDIEND row confirms the zaak is open (so storage doesn't race the OpenZaak worker). - **Scope boundary:** the ZGW zaak is not set to a cancellation status on 30-day expiry — that's #106 (S-10c). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #108 |
||
|
|
4777ff2b1d |
feat(workflow): document-wait task + 30-day timeout cancellation (S-10a, closes #102) (#105)
## What & why S-10a, the **workflow/timeout spine** of the (split) document-upload slice: the registratie process now parks at a **`WachtOpDocumenten`** user task with an **interrupting `P30D` boundary timer**. When the documents arrive the task completes and the process continues into the diploma routing (S-13) → Beoordelen; if the 30 days lapse, the timer cancels the wait, runs a `RegistratieVerlopen` external-worker task, and the domain expires the aggregate to a new terminal status **`Verlopen`**. Backend only — the real upload trigger (portal → BFF → ACL → Documenten API) is S-10b (#103). Closes #102 Mechanism recorded in **ADR-0017**; opened as proposal #104. Mirrors the S-14 escalation (boundary-timer + external-worker) and S-11 withdrawal (interrupting cancel) patterns. ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation (red→green pairs per layer). - [x] Implementation makes the test pass. - [x] Conventional Commits referencing the issue (`refs #102`). - [ ] CI green — all Gitea Actions jobs (pending on this PR). - [x] `docker compose up` health unaffected (no new services; deploy path unchanged). - [x] Docs updated (ADR-0017, demo-script, BACKLOG split). - [x] ADR added (`docs/architecture/adr-0017-document-wait-timeout-cancellation.md`). - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - **Domain** (`Registration.Expire()` + `Verlopen`), **application** (`ExpireRegistrationWorker`), **infra** (`RegistratieVerlopenProcessor`/`Pump`, `IRegistratieVerlopenClient`, Flowable acquire/complete + `CompleteDocumentWaitAsync`) — the timeout counterpart to the OpenZaak/escalation worker trios; idempotent per §8.6. - **BPMN** verified live against a `flowable-rest` probe: complete `WachtOpDocumenten` → routes to Beoordelen; fire the P30D timer → `RegistratieVerlopen` job (carrying `registrationId`) + the wait task cancelled. `verify-domain` exercises both branches in-stack (completes the wait in every existing block; fires the timer and asserts `Verlopen` in a new block). - **Scope boundary:** on expiry the aggregate goes `Verlopen` and the process ends, but the ZGW *zaak* is not yet set to a cancellation status — that needs a new ACL method + statustype seeding and is folded into S-10b (noted in ADR-0017). - `CompleteDocumentWaitAsync` is built and HTTP-tested here but not yet called from a domain endpoint; S-10b wires the upload trigger to it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #105 |
||
|
|
ccae27b3da |
feat(workflow): diploma-eligibility DMN routes foreign diplomas via CBGV-advies (S-13, closes #14) (#101)
## What & why S-13: a diploma's origin decides its route. A **DMN** (`diploma-eligibility`) is evaluated inline by the registratie process as a **`businessRuleTask`**; an exclusive gateway routes a **foreign** (Buitenlands) diploma through a new **CBGVAdvies** user task before `Beoordelen`, a **domestic** one straight there (PRD flow 4). The domain's only new job is carrying the diploma origin and passing it as a process start variable. Chose **Option B (DMN in the BPMN)** over the issue's literal "evaluated by the Domain Service via Workflow Client" wording — keeps the decision a first-class workflow artefact and §8.2 clean. Rationale in **ADR-0016** (proposal #100); noted on this issue. Closes #14 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation. - [x] Implementation makes the test pass. - [x] Conventional Commits referencing the issue (`refs #14`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (additive; DMN deployed by flowable-init). - [x] Docs updated (ADR-0016, demo note). - [x] ADR added (`docs/architecture/adr-0016-diploma-eligibility-dmn.md`). - [x] Demo note in `docs/demo-script.md`. ## How it was built (TDD) - **Domain**: `DiplomaOrigin` on the aggregate + submit command; threaded through the process-start port so the Workflow Client emits a `diplomaOrigin` start variable. Red → green. - **DMN + BPMN**: `workflows/diploma-eligibility.dmn` (origin → route); `businessRuleTask` + exclusive gateway + `CBGVAdvies` user task in `registratie.bpmn`; DMN deployed to Flowable's DMN engine by `flowable-init`. - **Both paths**: `Een diploma op herkomst routeren` acceptance scenarios (origin carried into the process) + unit tests; verify-domain drives a foreign registration through CBGVAdvies→Beoordelen and the domestic one straight to Beoordelen — exercising both DMN branches live. ## Notes for reviewers - Deviation from the issue's Option-A wording is deliberate and recorded (ADR-0016); the outcome is unchanged. - The self-service eIDAS→foreign wiring is out of scope here (this slice is area:domain + area:workflow); the domain submit accepts an optional `diplomaOrigin` so the foreign path is drivable. - Local green: domain unit 109, acceptance 15, `dotnet format`, Release build (0 errors), **domain mutation 95.39%** (break 90). The DMN/`businessRuleTask` REST wiring is CI-verified on verify-stack (no local full-stack run here). Reviewed-on: #101 |
||
|
|
7bcbc726ce |
feat(workflow): beoordeling escalation to teamlead after 14 days (S-14, closes #15) (#99)
## What & why S-14: a beoordeling a behandelaar does not pick up within **14 days** escalates to the **teamlead**. A non-interrupting `P14D` boundary timer on the `Beoordelen` user task fires an external-worker task (`BeoordelingEscaleren`); the domain's escalation worker reassigns the still-open task's candidate group from `behandelaar` to `teamlead`. The task keeps its identity — only who may claim it changes. The escalation-via-external-worker decision is recorded in **ADR-0015** (proposal #98); it upholds §8.2 (the Workflow Client stays the only code that talks to Flowable) and keeps Flowable a stock image. Closes #15 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation. - [x] Implementation makes the test pass; refactor commit if structure improved. - [x] Conventional Commits referencing the issue (`refs #NN`). - [x] CI green — all Gitea Actions jobs. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (no new services; escalation is additive to the domain worker). - [x] Docs updated (ADR-0015, demo note). - [x] ADR added (`docs/architecture/adr-0015-beoordeling-escalation.md`). - [x] Demo note in `docs/demo-script.md`. ## How it was built (TDD) - **Workflow Client** (`IBeoordelingEscalatieClient`): acquire `BeoordelingEscaleren` jobs → find the open `Beoordelen` task in the instance → add `teamlead`/remove `behandelaar` candidate group → complete the job. Red → green. - **Escalation drain loop** (`BeoordelingEscalatieProcessor`) + hosted `BeoordelingEscalatiePump`, mirroring the OpenZaak worker. Red → green. - **BPMN**: non-interrupting `P14D` boundary timer on `Beoordelen` → external task → escalation end. - **Both branches** (escalate after timeout; no-op when completed in time) covered by the `Een beoordeling escaleren` acceptance scenarios + Workflow Client unit tests. - **Live integration**: `verify-domain` fires the timer early via Flowable's management API and asserts the reassignment to teamlead. ## Notes for reviewers - Interface segregation: escalation is on `IBeoordelingEscalatieClient`, separate from the OpenZaak worker's `IExternalWorkerClient`. - Reassignment is two REST hops (add teamlead, remove behandelaar); idempotent on redelivery — see ADR-0015 consequences. - Local checks green: domain unit tests (104), acceptance (13), `dotnet format --verify-no-changes`, Release build (0 errors), **domain mutation 96.69%** (break 90). The `run-domain-check.sh` escalation path is CI-verified on verify-stack (local full-stack run is constrained here). - `BeoordelingEscalatiePump` excluded from mutation, mirroring the existing `OpenZaakJobPump` exclusion. Reviewed-on: #99 |
||
|
|
8a537edd6c |
fix(infra): engine-portable portal nginx resolver (closes #96) (#97)
## What & why Closes #96. The portal nginx configs hardcode `resolver 127.0.0.11` (Docker's embedded DNS) for their variable `proxy_pass` to the BFF, so on rootless **podman** (network-specific aardvark DNS) every proxied call 502'd — the portals loaded and login worked, but no in-app data flowed. Add a shared `/docker-entrypoint.d` hook (`apps/portal-nginx-resolver.sh`, wired into all three portal Dockerfiles) that rewrites the resolver from the container's own `/etc/resolv.conf` at startup: a **no-op on Docker** (nameserver *is* 127.0.0.11) and **correct on podman** (rewrites to e.g. 10.89.0.1). nginx.conf is unchanged (the hardcoded value is the substitution anchor). ## How verified Built the behandel image and ran it on the compose network under podman: the hook rewrote the config to `resolver 10.89.0.1`, and `GET /behandel/werkbak` proxied to the BFF returning **401** (auth), not 502. On Docker the nameserver is 127.0.0.11 so the substitution is a no-op and CI/e2e behaviour is unchanged. Reviewed-on: #97 |
||
|
|
e7bed37cda |
fix(infra): local event-subscriber Acl:BaseUrl parity (closes #94) (#95)
## What & why Closes #94. The local compose's `event-subscriber` lacked `Acl__BaseUrl` (and the `acl` dependency) that the canonical compose sets (#78) — so it threw `Missing configuration 'Acl:BaseUrl'` and exited on startup, which also knocked over podman-compose's bring-up of the rest of the stack (the frontends were left uncreated). Adds the env + dependency, matching `docker-compose.yml`. ## How verified Recreated `event-subscriber` from the fixed compose locally — it now starts healthy, and the three portals come up (self-service :8140, openbaar :8141, behandel :8142). `docker compose config` valid. ## Note (separate, not fixed here) On **rootless podman** the portal→BFF nginx proxy still 502s (`resolver 127.0.0.11` is Docker's embedded DNS; podman uses its own), and podman-compose orchestration of this dependency graph is flaky — both are pre-existing local-engine limitations, clean on Docker Desktop / CI. Tracking separately. Reviewed-on: #95 |
||
|
|
94699f3603 |
feat(self-service): trek aanvraag in — withdrawal action (S-11c-2, closes #12) (#93)
## What & why Final sub-slice of **S-11 · Withdrawal (Flow 3)** — the user-facing "trek aanvraag in" action, which **closes #12**. - **self-service portal**: the submit confirmation gains a **"Trek aanvraag in"** button. It withdraws the just-submitted registration via `postSelfServiceRegistrationsIdWithdraw(reference)`; success shows an *ingetrokken* confirmation, a failure is surfaced (`role="alert"`) and the action stays available — same confirm-and-surface pattern as submit. - **acceptance**: `Een registratie intrekken` — owner withdraws → INGETROKKEN + workflow cancelled; a different bsn is reported not-found. - **e2e**: `withdrawal.spec.ts` — DigiD submit → trek aanvraag in → the portal confirms ingetrokken. - **docs**: demo-script + frontend-decisions. Together with S-11a (#88), S-11b (#89), S-11c-1 (#90), this completes the flow: citizen withdraws → domain INGETROKKEN → BPMN message event cancels the process → the case leaves the behandelaar's werkbak. Closes #12 ## Definition of Done - [x] Linked Gitea issue (#12). - [x] Failing tests committed before the implementation. - [x] Implementation makes the tests pass. - [x] Conventional Commits referencing the issue (`refs #12`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` unaffected. - [x] Docs updated (demo-script + frontend-decisions). - [x] ADR — ADR-0014 (from S-11b) covers the cancellation decision; nothing new here. ## Notes for reviewers - Full local gate run before pushing: `dotnet format --verify-no-changes` clean; `make unit` green (Acceptance **11** incl. the 2 new withdrawal scenarios, Big 95, BFF 30, Acl 27, EventSubscriber 19); self-service lint/test/build green (9 tests, incl. the 2 new withdraw tests). - `withdrawal.spec.ts` waits on the *ingetrokken* confirmation (which only renders after the withdraw POST returns), so it can't cancel the request early (the 499 lesson from #87). Live-validated by verify-stack. Reviewed-on: #93 |
||
|
|
951bdd8364 |
fix(infra): local compose parity + host-browser OIDC (closes #91) (#92)
## What & why Closes #91. `infra/docker-compose.local.yml` (the no-make local stack) was missing the `domain` service and all three portals, and never wired host-browser OIDC — so browsing the behandel portal redirected to `http://keycloak:8080/…`, which a host browser can't resolve. - **Parity**: add `domain`, `self-service`, `openbaar`, `behandel` (local now matches the CI-canonical `docker-compose.yml` service-for-service). - **BFF**: give it the Keycloak + downstream env it was missing (it previously fell back to appsettings and couldn't reach Keycloak). - **Host-browser OIDC**: pin Keycloak's frontend/issuer URL to `http://localhost:8180` (`KC_HOSTNAME`) with `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true`, so a host browser logs in on `localhost:8180` while the BFF still validates in-network via `keycloak:8080`. - **Portals**: bind-mount a `localhost:8180` `config.json` over the image's baked `keycloak:8080` one (`infra/local-config/*`). openbaar is anonymous, no config. ## How verified - `docker compose -f infra/docker-compose.local.yml config` valid; parity check shows nothing missing. - Started Keycloak from the local compose and confirmed the discovery document: - **host view** (`localhost:8180`): `issuer` + all endpoints on `localhost:8180` (what the browser uses). - **in-network view** (`keycloak:8080`): `issuer` stays `http://localhost:8180/...` (matches browser tokens) while `jwks_uri`/`token_endpoint` resolve to `keycloak:8080` (reachable by the BFF). ## Notes for reviewers - The full portal→BFF→Keycloak login round-trip should get a quick browser smoke test on a real engine (I validated the Keycloak issuer/backchannel split and compose validity, but can't drive a browser here). Ports: self-service :8140, openbaar :8141, behandel :8142; users in `docs/synthetic-data.md`. - On rootless podman the portal→BFF nginx proxy (`resolver 127.0.0.11`) may 502 (a separate known podman-vs-docker DNS quirk); login is a browser redirect and is unaffected. Works on Docker Desktop. - No app-code change; `docker-compose.yml` (CI-canonical) is untouched. Reviewed-on: #92 |
||
|
|
2397d9196a |
feat(bff): owner-scoped self-service withdraw endpoint (S-11c-1, refs #12) (#90)
## What & why Third sub-slice of **S-11 · Withdrawal (Flow 3)** (#12) — the **owner-scoped BFF withdraw endpoint** (backend). S-11a/b made a withdrawal transition the aggregate and cancel the workflow; this adds the citizen-facing entry point through the BFF, gated to the registration's owner. - **Domain**: `WithdrawRegistrationCommand` carries the caller's `bsn`; the handler returns a `WithdrawOutcome` and refuses a bsn that doesn't own the registration. Unknown and not-owned are **both 404** (indistinguishable — ownership isn't revealed). `POST /registrations/{id}/withdraw` takes `{bsn}` and maps the outcome (204/404). - **BFF**: `POST /self-service/registrations/{id}/withdraw` (DigiD-authenticated) forwards the token's `bsn` to the domain and relays 204/404. The BFF authenticates; the domain owner-scopes (an aggregate invariant, not the domain doing auth). - OpenAPI spec + Angular client regenerated for the new endpoint. - `run-domain-check.sh` withdrawal step now sends the owner `bsn` (verify-stack). Refs #12 — the self-service "trek aanvraag in" button + e2e (S-11c-2) closes it. ## Definition of Done - [x] Linked Gitea issue (#12). - [x] Failing tests committed before the implementation. - [x] Implementation makes the tests pass. - [x] Conventional Commits referencing the issue (`refs #12`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` unaffected. - [x] No ADR needed (owner-scoping is an aggregate invariant; no boundary change). - [x] Docs — the user-visible demo note lands with S-11c-2. ## Notes for reviewers - **Full local gate run before pushing this time** (lessons from #89): `dotnet format --verify-no-changes` clean; `make unit` green — Acl 27, EventSubscriber 19, BFF 30, Acceptance 9, Big 95; `api-client` lint+test green. - Owner mismatch returns 404 (not 403) so the portal can't be used to probe which references exist. Reviewed-on: #90 |
||
|
|
a34caba9ea |
feat(domain): withdrawal cancels the registratie process (S-11b, refs #12) (#89)
## What & why Second sub-slice of **S-11 · Withdrawal (Flow 3)** (#12). S-11a (#88) made a withdrawal advance the aggregate to INGETROKKEN; this sub-slice **cancels the running Flowable process** so the withdrawn case leaves the behandelaar's werkbak. - **BPMN** (`registratie.bpmn`): an interrupting message boundary event (`RegistratieIngetrokken`) on the `Beoordelen` task, routing to a dedicated "Registratie ingetrokken" end event. - **Workflow Client**: `WithdrawBeoordelingAsync(executionId)` delivers `messageEventReceived` to the task's execution (PUT); `BeoordelingTask` now carries its `executionId`. - **`WithdrawRegistration` handler**: after the domain transition, finds the open `Beoordelen` task for the registration and delivers the withdrawal message — best-effort, mirroring how the beoordeling completes its task. - **Werkbak**: also filters out registrations that are no longer open, so a withdrawn case never surfaces even in the brief window before cancellation lands. - **ADR-0014** records the decision (message event in BPMN vs. deleting the instance from code). - **verify (`run-domain-check.sh`)**: a second registration parks at `Beoordelen`, is withdrawn via the domain, and the check asserts its `Beoordelen` task disappears — so verify-stack validates the live Flowable message correlation. Refs #12 (S-11c — the BFF + self-service "trek aanvraag in" button + e2e — closes it). ## Definition of Done - [x] Linked Gitea issue (#12). - [x] Failing tests committed before the implementation (red → green per commit). - [x] Implementation makes the tests pass. - [x] Conventional Commits referencing the issue (`refs #12`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` unaffected (BPMN redeploys on a fresh CI DB via flowable-init). - [x] ADR added (ADR-0014). - [x] Docs — the user-visible demo note lands with S-11c. ## Notes for reviewers - Verified locally: `Big.Tests` 94/94 pass; `Big.Api` builds; `registratie.bpmn` is well-formed. - The Flowable message-correlation REST shape is validated **live** by verify-stack (the Workflow Client unit tests stub the exchange and assert only the request shape, per ADR-0009) — the new `run-domain-check.sh` withdrawal step is that live check. - Known gap (ADR-0014): a withdrawal that races ahead of the process reaching `Beoordelen` finds no task to cancel; the aggregate is still INGETROKKEN and the werkbak filter hides it, but that instance parks unattended. A process-level event subprocess would close the gap — deferred. Reviewed-on: #89 |
||
|
|
1f1c944a8b |
feat(domain): withdrawal — INGETROKKEN transition + endpoint (S-11a, refs #12) (#88)
## What & why First sub-slice of **S-11 · Withdrawal (Flow 3)** (#12). A zorgprofessional can withdraw a still-open registration ("trek aanvraag in"); this sub-slice delivers the **domain transition + endpoint**, mirroring how S-12a shipped the beoordeling decision model on its own (#82). - `RegistrationStatus.Ingetrokken` (terminal). - `Registration.Withdraw()` — allowed from INGEDIEND or IN_BEHANDELING, needs no zaak, idempotent, and rejected once the registration has been decided (INGESCHREVEN/AFGEWEZEN). - `WithdrawRegistration` application handler (load → withdraw → persist; repeated withdrawal is a no-op). - `POST /registrations/{id}/withdraw` on the domain API. Demoable: `POST /registrations/{id}/withdraw` → `GET /registrations/{id}` shows `INGETROKKEN`. Refs #12 (not closing — see below). ## Scope / follow-ups S-11 is bigger than one slice, so it is split (CLAUDE.md §13), like S-12 was: - **S-11a (this PR)** — domain withdrawal transition + endpoint. - **S-11b** — cancel the running Flowable process via a BPMN message event, so a withdrawn case leaves the behandelaar's werkbak. - **S-11c** — owner-scoped BFF self-service withdraw endpoint + "trek aanvraag in" button + e2e. Cancelling the Flowable process is deliberately deferred (documented in `WithdrawRegistration`), exactly as the beoordeling's rejection deferred its zaak propagation. #12 stays open until S-11c. ## Definition of Done - [x] Linked Gitea issue (#12). - [x] Failing test committed before the implementation. - [x] Implementation makes the test pass. - [x] Conventional Commits referencing the issue (`refs #12`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` unaffected (no infra/contract change). - [x] Docs — none needed for this backend sub-slice; the user-visible demo note lands with S-11c. - [x] No ADR needed — mirrors existing aggregate/handler/endpoint patterns; no boundary change. ## Notes for reviewers - Verified locally: `Big.Tests` 89/89 pass; `Big.Api` builds clean. - The domain trusts its callers (§8.3); owner-scoping by the caller's bsn is enforced at the BFF in S-11c. Reviewed-on: #88 |
||
|
|
3abf8f7ccf |
feat(behandel): behandel-portal — werkbak + beoordeling (closes #13) (#87)
## What & why
Finishes **S-12 · Behandel-portal — werkbak + beoordeling**. The backend sub-slices (S-12a/b/c-1/c-2) were merged, but the slice's stated outcome — a behandel *portal* with medewerker login, a werkbak, and decide — had no frontend. This adds it.
- **`libs/auth`**: `MedewerkerAuthService` + `provideMedewerkerAuth` (Keycloak `medewerker` realm), a `roles`/`hasRole` surface on the shared `AuthService`, and a realm-roles protocol mapper so the SPA can read `behandelaar`/`teamlead` from the token. The BFF remains the security boundary (ADR-0013).
- **`apps/behandel`**: a new Nx Angular app mirroring self-service — medewerker OIDC login and a **werkbak** page listing registrations awaiting beoordeling (`GET /behandel/werkbak`) with per-row **Goedkeuren/Afwijzen** actions (`POST /behandel/registrations/{id}/decide`) that refresh the list. NL DS/Utrecht, standalone + signals.
- **e2e**: the walking-skeleton happy path now approves through the real portal (behandelaar logs in, finds the row by reference, clicks Goedkeuren) instead of the temporary admin endpoint.
- **infra/docs**: behandel service in compose (`:8142`, depends on Keycloak); added to the smoke `WAIT_SVCS` + CI log dump; `frontend-decisions.md` and `demo-script.md` updated.
Closes #13
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass; refactor commit if structure improved.
- [x] Conventional Commits referencing the issue (`refs #13`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(behandel image + container verified locally; full stack gated in CI.)*
- [x] Docs updated if behaviour, contracts, or operations changed.
- [x] ADR added — ADR-0013 (merged with the backend sub-slices) already covers the wiring; no new decision here.
- [x] Demo note in `docs/demo-script.md`.
## Notes for reviewers
- Verified locally: auth + behandel + all frontend projects pass lint & unit tests (incl. axe WCAG 2.1 AA); production build green; the behandel Docker image builds and serves with the correct baked `medewerker` config + SPA fallback.
- The full compose-up smoke, e2e, and mutation are CI-gated (known local full-stack verify limits).
- **Follow-ups (not in scope):** the `WerkbakItem` contract has no citizen name (werkbak shows the BSN) — adding one is a BFF+domain contract change; and the domain's temporary admin `approve` endpoint is now unused by the e2e and could be removed.
Reviewed-on: #87
|
||
|
|
d226b6402d |
feat(#13): S-12c-2 — behandel decide → domain + complete workflow task (#86)
## What & why
Second half of **S-12c** (behandel-portal backend), completing the decision path per **ADR-0013**:
- **Domain:** `BeoordeelRegistratie` now, after applying the decision (aggregate + ACL for approval), **completes the open Flowable `Beoordelen` task** for that registration (found by registrationId) with the besluit, so the workflow advances. No open task → the decision still stands (completes nothing); idempotent.
- **BFF:** `POST /behandel/registrations/{id}/decide` behind the medewerker/`behandelaar` policy, forwarding `goedkeuren`/`afwijzen` to the domain. Validates the besluit vocabulary (400 on unknown) without troubling the domain.
Behavior: decide is **401** without a token, **403** without the role, **400** for an unknown besluit, **204** (forwarded) for a behandelaar.
This completes the behandel backend. **S-12d** (the Angular behandel-portal + Playwright e2e) closes umbrella #13 and retires the temporary `/approve`.
## Definition of Done
- [x] Linked issue: #13 (umbrella, `refs`)
- [x] Tests first; red → green per layer
- [x] Unit + acceptance green (`make unit`): domain 79, bff 27, acceptance 9 (acl/event-subscriber unaffected)
- [x] Beoordeling acceptance scenario asserts task completion (goedkeuren + afwijzen)
- [x] openapi.json + api-client regenerated (drift guard passes)
- [x] Mutation ≥ break(90): **domain 100%, bff 100%**
- [ ] CI green (pending)
Part of #13.
Reviewed-on: #86
|
||
|
|
9c3da48d8e |
feat(#13): S-12c-1 — behandel BFF auth + werkbak (ADR-0013) (#85)
## What & why First half of **S-12c** (behandel-portal backend), per **ADR-0013** (decisions recorded in #84): - **BFF multi-realm auth.** A second JWT bearer scheme (`medewerker`) alongside the default `digid` scheme. On validation it lifts Keycloak's `realm_access.roles` onto the principal, and a `behandelaar` policy (medewerker scheme + `behandelaar` role) gates `/behandel/*`. Self-service keeps the digid scheme. - **Werkbak = Flowable tasks.** The domain `Werkbak` query reads the open `Beoordelen` tasks (§8.2, S-12b's `IUserTaskClient`) and enriches each with its aggregate's bsn + status; `GET /behandel/werkbak` (domain) is proxied by the BFF `GET /behandel/werkbak` behind the behandelaar policy. The read projection stays the anonymous openbaar model (no premature `IN_BEHANDELING`/personal-data plumbing — deferred in ADR-0008). Behavior: `/behandel/werkbak` is **401** without a token, **403** for a medewerker lacking the role, **200 + werkbak** for a behandelaar. **S-12c-2** (next): `POST /behandel/registrations/{id}/decide` → domain decision + complete the Flowable task. ## Definition of Done - [x] Linked issue: #13 (umbrella, `refs`); closes the adr-proposal #84 - [x] Tests first; red → green per layer - [x] Unit + acceptance green (`make unit`): domain 78, bff 23, acceptance 9 (+ acl/event-subscriber unaffected) - [x] api-client `test` green; openapi.json regenerated (drift guard passes) - [x] Mutation ≥ break(90): **domain 100%, bff 100%** - [x] ADR-0013 added; `Keycloak__MedewerkerAuthority` wired into compose - [ ] CI green (pending) Part of #13. closes #84 Reviewed-on: #85 |
||
|
|
4085bdead7 |
feat(#13): S-12b — Workflow Client user-tasks + Beoordelen userTask (#83)
## What & why Second sub-slice of **S-12 (#13)** — the **Workflow Client gains behandelaar user-task operations**, and the process model gains the beoordeling step. - **BPMN:** `registratie.bpmn` now parks at a `Beoordelen` **userTask** (candidate group `behandelaar`) after `OpenZaakAanmaken`; `registrationId` rides along as a process variable so the werkbak can correlate each task to its aggregate. - **Workflow Client** (`IUserTaskClient`, the only code that talks to Flowable §8.2): - `GetOpenBeoordelingenAsync()` — the werkbak (open `Beoordelen` tasks + their `registrationId`) - `ClaimAsync(taskId, behandelaar)` - `CompleteBeoordelingAsync(taskId, besluit)` — carries the decision into the process as the `besluit` variable - **Live integration:** `verify-domain` now drives the full user-task lifecycle against a real Flowable — after the worker opens the zaak, it polls for the task, claims it as `merel-behandelaar`, completes it (`goedkeuren`), and asserts the process finishes. This proves the exact REST contract (`service/runtime/tasks/query` + `…/{id}` claim/complete) the client depends on. The walking skeleton is unaffected: the temporary `/approve` path still sets the zaak status directly; wiring the domain decision to *complete this task* (and driving the werkbak from the BFF) lands in **S-12c**. ## Definition of Done - [x] Linked issue: #13 (umbrella; `refs`, does not close) - [x] Tests first; red → green - [x] Unit + acceptance green (`make unit`): domain 76, acceptance 9 (acl/event-subscriber/bff unaffected) - [x] Mutation ≥ break(90): **domain 100%** (killed the new survivors *and* the pre-existing `FlowableWorkflowClient` baseline) - [x] Live Flowable user-task lifecycle asserted in `verify-domain` - [ ] CI green (pending) Part of #13. Reviewed-on: #83 |
||
|
|
d4ed0ffc22 |
feat(#13): S-12a — beoordeling decision model (domain) (#82)
## What & why First sub-slice of **S-12 (#13)** — the **beoordeling decision model** in the Domain Service. Foundation for the behandel-portal: it gives the domain a proper decision lifecycle before any UI/Flowable/BFF work. - **Statuses:** add `InBehandeling` and `Afgewezen` to `RegistrationStatus`. - **Aggregate:** `TakeIntoBehandeling()` (`Ingediend → InBehandeling`, idempotent, guards terminal states); generalise the behandelaar decision — `Approve()` (requires a zaak) and new `Reject()` both act on an `Ingediend`/`InBehandeling` registration → `Ingeschreven`/`Afgewezen`. - **Use-case:** `BeoordeelRegistratie` (`goedkeuren` sets the zaak's final status via the ACL §8.1 → `Ingeschreven`; `afwijzen` → `Afgewezen`, domain-only for now). Idempotent. - **Endpoint:** `POST /registrations/{id}/decide` (`{ "besluit": "goedkeuren" | "afwijzen" }`), superseding the temporary `/approve` (retired when the portal lands, S-12d). - **BDD:** `EenRegistratieBeoordelen.feature` — goedkeuren + afwijzen scenarios (feature-scoped bindings). **Scoped out** to later S-12 sub-slices: Flowable user-task claim/complete + BPMN `userTask` (S-12b), BFF `/behandel/*` + medewerker authz (S-12c), the Angular behandel-portal + e2e (S-12d), and propagating a *rejection* to the zaak/projection via the ACL. ## Definition of Done - [x] Linked issue: #13 (umbrella; this PR `refs`, does not close) - [x] Tests first; red → green per behaviour - [x] Unit + acceptance green (`make unit`): domain 65, acceptance 9 - [x] Mutation ≥ break(90): domain 98.77%, no survivors in new code (the one unkilled mutant is the pre-existing `FlowableWorkflowClient` baseline) - [ ] CI green (pending) Part of #13. Reviewed-on: #82 |
||
|
|
3023bb6fbe |
chore(release): 2026.07.0 (#81)
Cuts the first CalVer release **2026.07.0** (tag ), marking the end of **Iteration 1 — Walking Skeleton**. regenerated from Conventional Commits by git-cliff (covers Iterations 0 and 1, through #79). After merge: tag on main and publish the Gitea Release. closes #80 Reviewed-on: #81v2026.07.0 |
||
|
|
9997da8beb |
feat(#78): one citizen reference across self-service and the openbaar register (#79)
## What & why Before this change the self-service confirmation and the openbaar register showed **different** identifiers, so a citizen could not look their registration back up (#78). Now both surface the same **reference**: - **domain → ACL (write):** the domain `registrationId` is set as the zaak's `identificatie` on `POST /zaken`. - **event-subscriber → ACL (read):** the subscriber reads the zaak's `identificatie` back through the ACL (§8.1 — only the ACL talks to ZGW) via a new `POST /zaken/reference`, and stores it on the projection row **and** the `processed_notifications` replay log. - **BFF + openbaar:** the public view exposes `id/status/reference` (never bsn/naam) and searches by id or reference; the register's "Referentie" column shows the reference. Storing the reference in the replay log keeps ADR-0008's **rebuild-is-log-only** invariant intact — `/admin/rebuild` reproduces the reference without re-reading the ACL. Decision recorded in **ADR-0012**. ## Definition of Done - [x] Linked issue: #78 - [x] Tests written first; red → green per layer - [x] Unit + acceptance green (`make unit`): domain 49, acl 27, bff 20, event-subscriber 19, acceptance 7 - [x] Frontend lint + test green (`nx run-many -t lint test`) - [x] Mutation ≥ break(90): acl 100%, event-subscriber 100%, bff 100%, domain 98.41% (pre-existing FlowableWorkflowClient baseline, untouched) - [x] e2e extended: confirmation reference == register reference - [x] openapi.json + api-client regenerated (drift guard green) - [x] ADR-0012 added; demo-script note appended - [x] `Acl__BaseUrl` wired for the subscriber in compose closes #78 Reviewed-on: #79 |
||
|
|
1c185e6686 |
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 |
||
|
|
bc9831c113 |
S-09: Openbaar Register portal — public lookup (#76)
Anonymous openbaar portal completing the walking skeleton (submit → projection → public visibility). closes #10 |
||
|
|
7e8c5d7b51 |
Merge pull request 'ci: speed up pipeline — NuGet cache + prebuilt Playwright image' (#74) from chore/73-ci-speedups into main
Reviewed-on: #74 |
||
|
|
2b9eb5eb41 |
ci(e2e): run Playwright from the prebuilt image instead of downloading browsers (refs #73)
CI / build (pull_request) Successful in 56s
CI / mutation (pull_request) Successful in 4m6s
CI / verify-stack (pull_request) Successful in 7m3s
CI / unit (pull_request) Successful in 1m0s
CI / frontend (pull_request) Successful in 1m50s
CI / lint (pull_request) Successful in 5m43s
The verify-e2e lane downloaded ~150 MB of Chromium (npx playwright install) on every verify-stack run. Use the official mcr.microsoft.com/playwright image with browsers pre-baked; npm install still pins @playwright/test from tests/e2e, and the image tag is kept in lockstep with that version. Verified the exact create + docker cp + start flow launches the baked browser with no download. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |