diff --git a/docs/project/backlog/README.md b/docs/project/backlog/README.md index 2c2218a..9865d4c 100644 --- a/docs/project/backlog/README.md +++ b/docs/project/backlog/README.md @@ -105,6 +105,18 @@ for its existing violations, so every WP ends green. | [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done | | [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done | | [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | done | +| [WP-55](WP-55-openzaak-secrets-tls.md) | Real secrets + TLS for the OpenZaak harness | 10 · OpenZaak hardening | todo | +| [WP-56](WP-56-openzaak-catalogus-provisioning.md) | Idempotent catalogus provisioning | 10 · OpenZaak hardening | todo | +| [WP-57](WP-57-openzaak-least-privilege-scopes.md) | Least-privilege client scopes | 10 · OpenZaak hardening | todo | +| [WP-58](WP-58-openzaak-notifications.md) | Real notifications (celery + scripted abonnement) | 10 · OpenZaak hardening | todo | +| [WP-59](WP-59-document-confidentialiteit-config.md) | Per-document-type confidentialiteit config | 10 · OpenZaak hardening | todo | +| [WP-60](WP-60-write-divergence-resilience.md) | Write-divergence resilience (local + ZGW writes) | 10 · OpenZaak hardening | todo | +| [WP-61](WP-61-behandelportal-bootstrap.md) | Bootstrap the behandelportal app | 11 · Behandelportal | todo | +| [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | todo | +| [WP-63](WP-63-aanvraag-status-lifecycle.md) | Backend: aanvraag status lifecycle as a published DTO | 11 · Behandelportal | todo | +| [WP-64](WP-64-behandelportal-werkvoorraad.md) | Behandelportal: werkvoorraad (queue) screen | 11 · Behandelportal | todo | +| [WP-65](WP-65-behandelportal-beoordeling.md) | Behandelportal: zaak detail + beoordeling (decision) screen | 11 · Behandelportal | todo | +| [WP-66](WP-66-behandelportal-openzaak-write.md) | Wire the decision into OpenZaak | 11 · Behandelportal | todo | Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn); 03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed @@ -137,6 +149,17 @@ deployment of 49–52) and **54** (a docker OpenZaak harness + opt-in integratio CRUD arc and can land any time; 54 depends on 49 (something to read) and unlocks realistic testing for the rest. Both are self-contained (each WP file carries its own current-state handoff) and sized for a fresh Sonnet session. +Phase 10 (OpenZaak production hardening, WP-55..60) and Phase 11 (Behandelportal, +WP-61..66) are two independent tracks that can be worked concurrently — neither blocks +the other. Within phase 10: 55/59/60 are fully independent; 57 and 58 both build on 56's +provisioning mechanism, otherwise independent of each other. Within phase 11: 61 +(bootstrap), 62 (backend medewerker identity), and 63 (backend status DTO) are +independent of each other and can land in any order; 64 needs all three (61 for the app +to exist, 62 for identity, 63 for the status it reads); 65 needs 64; 66 needs 65 and +benefits from — but doesn't strictly require — phase 10's WP-60 landing first (WP-66 is +a second, currently-unprotected write pair otherwise). WP-60 is the one slice in phase 10 +sized for a `planner`-agent kickoff rather than direct implementation — its Decisions +block is deliberately left open (outbox vs. retry+reconcile). ## WP template diff --git a/docs/project/backlog/WP-55-openzaak-secrets-tls.md b/docs/project/backlog/WP-55-openzaak-secrets-tls.md new file mode 100644 index 0000000..0ab3f07 --- /dev/null +++ b/docs/project/backlog/WP-55-openzaak-secrets-tls.md @@ -0,0 +1,66 @@ +# WP-55 — Real secrets + TLS for the OpenZaak harness + +Status: todo +Phase: 10 — OpenZaak production hardening + +## Why + +`backend/openzaak/docker-compose.openzaak.yml` is explicitly a throwaway dev/test harness: +`SECRET_KEY: wp-54-local-harness-not-for-prod`, `POSTGRES_HOST_AUTH_METHOD=trust` (no DB +password), `IS_HTTPS: 'no'`, `DISABLE_2FA: 'true'`. Before anything else in this phase can +be called "production," the instance needs real secrets, real DB auth, and TLS. The BFF +side is already fine — `ZgwOptions.cs` binds from `IConfiguration`, so this is a deploy-config +change, not application code. + +## Read first + +- [openzaak-integration.md](../reference/openzaak-integration.md) +- `backend/openzaak/README.md` +- [ADR-0005 — OpenZaak behind the BFF](../reference/architecture/0005-openzaak-behind-bff.md) + +## Decisions (pre-made, don't relitigate) + +- Secrets come from the deployment environment (env vars / secrets manager), never + checked into compose or appsettings. +- TLS termination happens at a reverse proxy/ingress in front of OpenZaak — OpenZaak + itself doesn't need built-in cert handling. +- The existing dev harness stays as-is for local iteration (WP-54's trimmed rig is + intentional and still valuable); this WP adds a production compose/override or an + env-driven parameterization of the same file, not a replacement of the dev rig. + +## Files + +- `backend/openzaak/docker-compose.openzaak.yml` (or a new `docker-compose.openzaak.prod.yml` override) +- `backend/openzaak/README.md` +- `backend/src/BigRegister.Api/appsettings*.json` / `Zgw/ZgwOptions.cs` (confirm only, likely no change) + +## Steps + +1. Parameterize `SECRET_KEY`, DB user/password, and the ZGW JWT secret via env vars; + remove hardcoded values from the committed file. +2. Switch `POSTGRES_HOST_AUTH_METHOD` from `trust` to password auth, password from env. +3. Set `IS_HTTPS: 'yes'`; document the required reverse-proxy/ingress TLS termination. +4. Update `backend/openzaak/README.md` with the required env vars and the TLS note. +5. Confirm the BFF's JWT secret already comes from config — no code change expected. + +## Acceptance criteria + +- [ ] No secret value is hardcoded in any committed compose/config file. +- [ ] The production compose fails fast (or docs state clearly) when secrets aren't + supplied — no silent fallback to a real-looking default. +- [ ] README documents exactly which env vars must be set and how TLS is terminated. + +## Verification + +`docker compose -f backend/openzaak/docker-compose.openzaak.yml config` with required env +vars set; `cd backend && dotnet test` (WP-54 harness tests unaffected); manual: the local +dev harness still works with its dev-only values documented as dev-only. + +## Out of scope + +Catalogus provisioning (WP-56), client scope narrowing (WP-57), notifications (WP-58). + +## Risks + +If TLS/secrets docs lag an actual deploy, someone could ship with dev defaults — mitigate +by making the prod compose fail without required env vars rather than silently defaulting. diff --git a/docs/project/backlog/WP-56-openzaak-catalogus-provisioning.md b/docs/project/backlog/WP-56-openzaak-catalogus-provisioning.md new file mode 100644 index 0000000..0da1f28 --- /dev/null +++ b/docs/project/backlog/WP-56-openzaak-catalogus-provisioning.md @@ -0,0 +1,73 @@ +# WP-56 — Idempotent catalogus provisioning + +Status: todo +Phase: 10 — OpenZaak production hardening + +## Why + +`backend/openzaak/bootstrap-catalogus.sh` seeds catalogus/zaaktype/statustype/roltype/zaak +via hand-rolled curl+JWT and is explicitly **not idempotent** (fails on `domein`+`rsin` +uniqueness on rerun) — fine for a one-shot WP-54 harness, wrong for an environment that +needs to be rebuildable. OpenZaak already ships a documented, scripted alternative — the +`setup_configuration` mechanism (already used in the harness for the JWTSecret/Applicatie, +see `setup_configuration/data.yaml`) — this WP extends that same mechanism to the catalogus +content too. + +## Read first + +- `backend/openzaak/bootstrap-catalogus.sh` +- `backend/openzaak/setup_configuration/data.yaml` +- `backend/openzaak/docker-compose.openzaak.yml` (`web-init` service) +- OpenZaak's own `setup_configuration` / `openzaak_config_cli` docs (upstream) + +## Decisions (pre-made, don't relitigate) + +- Move catalogus/zaaktype/statustype/roltype provisioning into `setup_configuration`, + run by the existing `web-init` one-shot service, instead of the separate curl script. +- Keep `bootstrap-catalogus.sh` only for whatever content `setup_configuration` genuinely + can't express (e.g. a demo zaak instance) — confirm what's left at kickoff. +- Provisioning must be safe to run against an already-provisioned instance — either + genuinely idempotent, or the compose is structured to only run it once per fresh + volume (document which, don't leave it ambiguous). + +## Files + +- `backend/openzaak/setup_configuration/data.yaml` +- `backend/openzaak/bootstrap-catalogus.sh` (trim to whatever remains) +- `backend/openzaak/docker-compose.openzaak.yml` +- `backend/openzaak/README.md` + +## Steps + +1. Express the catalogus/zaaktype/statustype/roltype definitions currently created by + curl as `setup_configuration` YAML. +2. Wire it into the `web-init` command alongside the existing JWTSecret/Applicatie config. +3. Trim `bootstrap-catalogus.sh` to only what setup_configuration can't cover, if anything. +4. Test: tear down + `docker compose up` twice in a row (fresh volume, then existing + volume); confirm no failure on rerun. +5. Update the README describing the provisioning flow. + +## Acceptance criteria + +- [ ] Catalogus/zaaktype/statustype/roltype provisioning is declarative + (`setup_configuration`), not imperative curl. +- [ ] Running the compose stack up twice in a row doesn't error. +- [ ] WP-54's `OpenZaakIntegrationTests` still pass unchanged (same content, different + provisioning mechanism). + +## Verification + +`docker compose -f backend/openzaak/docker-compose.openzaak.yml up` twice in a row (fresh +volume, then existing volume); `cd backend && dotnet test --filter Category=Integration` +against the harness. + +## Out of scope + +Secrets/TLS (WP-55, unrelated but sequenced first in the table only by number), client +scopes (WP-57), notifications (WP-58). + +## Risks + +OpenZaak's `setup_configuration` coverage for zaaktype/besluittype content may be +incomplete upstream — if a piece genuinely can't be expressed declaratively, keep it in a +clearly-labeled idempotent script rather than forcing a bad fit. diff --git a/docs/project/backlog/WP-57-openzaak-least-privilege-scopes.md b/docs/project/backlog/WP-57-openzaak-least-privilege-scopes.md new file mode 100644 index 0000000..e716e08 --- /dev/null +++ b/docs/project/backlog/WP-57-openzaak-least-privilege-scopes.md @@ -0,0 +1,61 @@ +# WP-57 — Least-privilege client scopes + +Status: todo +Phase: 10 — OpenZaak production hardening + +## Why + +The harness's OpenZaak client is granted `heeft_alle_autorisaties: true` in +`setup_configuration/data.yaml` — acceptable for a disposable test rig, wrong for anything +closer to production, where the BFF's client should hold only the Autorisaties it actually +exercises. + +## Read first + +- `backend/openzaak/setup_configuration/data.yaml` +- `backend/src/BigRegister.Api/Zgw/OpenZaakZaakSource.cs` and `OpenZaakDocumentSource.cs` + (the actual ZGW endpoints/verbs called) + +## Decisions (pre-made, don't relitigate) + +- Scope precisely to what the BFF calls today: zaken (aanmaken, bijwerken, lezen), + statussen (aanmaken), rollen (aanmaken), documenten/zaakinformatieobjecten (aanmaken, + lezen) — enumerate exactly at kickoff from the client code, don't guess broader. +- No wildcard/all-scope grant in any environment beyond the pre-WP-56 disposable dev rig. + +## Files + +- `backend/openzaak/setup_configuration/data.yaml` (Autorisaties block) +- `backend/openzaak/README.md` + +## Steps + +1. Grep `OpenZaakZaakSource.cs` and `OpenZaakDocumentSource.cs` for every ZGW + endpoint/verb called. +2. Replace `heeft_alle_autorisaties: true` with an explicit `autorisaties` list matching + exactly that set. +3. Re-run the full integration suite against the narrowed client; add any scope a 403 + surfaces. + +## Acceptance criteria + +- [ ] Client config has no wildcard/all-scopes grant. +- [ ] `OpenZaakIntegrationTests` (WP-54) pass unchanged against the narrowed client. + +## Verification + +`cd backend && dotnet test --filter Category=Integration` against the harness with the +narrowed client. + +## Out of scope + +Rotating/expiring the client credential itself — defer until multi-tenant/production ops +actually need it. + +## Risks + +An overlooked scope only surfaces as a runtime 403 against a real instance — mitigated by +running the full integration suite, which already exercises every current call path +(WP-54). + +Depends on: WP-56 (provisioning mechanism this scopes down). diff --git a/docs/project/backlog/WP-58-openzaak-notifications.md b/docs/project/backlog/WP-58-openzaak-notifications.md new file mode 100644 index 0000000..2fa8d2e --- /dev/null +++ b/docs/project/backlog/WP-58-openzaak-notifications.md @@ -0,0 +1,68 @@ +# WP-58 — Real notifications (celery + scripted abonnement) + +Status: todo +Phase: 10 — OpenZaak production hardening + +## Why + +The WP-54 harness deliberately trims celery/celery-beat/celery-flower and nginx, and sets +`NOTIFICATIONS_DISABLED: 'true'` — without a Celery worker, OpenZaak 500s and rolls back on +every write to a notified resource. Fine for a fixture-driven integration harness; a real +deployment that wants live Notificaties (WP-52's webhook) needs the workers running and the +`abonnement` (subscription) actually registered against the BFF's public callback URL — +today that registration step is manual. + +## Read first + +- `backend/openzaak/docker-compose.openzaak.yml` (top-of-file ponytail note) +- [openzaak-integration.md](../reference/openzaak-integration.md) (Notificaties section) +- [WP-52](WP-52-openzaak-notificaties.md) + +## Decisions (pre-made, don't relitigate) + +- Add celery + celery-beat as additional compose services (same `openzaak/open-zaak` + image, different command), pointed at the same redis broker already in the harness. +- Registering the `abonnement` becomes a scripted, idempotent step — not a manual + admin-UI action — parameterized by the BFF's real public URL. +- Keep the existing WP-54 harness variant (`NOTIFICATIONS_DISABLED: 'true'`) available for + fast local iteration where a live webhook round-trip isn't needed; this WP is additive + (a "with notifications" profile/override), not a replacement. + +## Files + +- `backend/openzaak/docker-compose.openzaak.yml` (or an override file) +- New script/config for `abonnement` registration +- `docs/reference/openzaak-integration.md` + +## Steps + +1. Add celery/celery-beat services to a notifications-enabled compose profile. +2. Flip `NOTIFICATIONS_DISABLED` off for that profile. +3. Script the `abonnement` registration (POST to the NRC, pointed at the BFF's + `/zgw/notificaties` endpoint from WP-52), idempotent on rerun. +4. Verify a real write (e.g. a status change) triggers a live webhook delivery to the BFF. + +## Acceptance criteria + +- [ ] A notifications-enabled harness profile runs celery/celery-beat and delivers a real + notification end-to-end to the BFF's webhook. +- [ ] The `abonnement` registration step is a script, re-runnable without erroring on an + already-registered subscription. + +## Verification + +Bring up the notifications-enabled profile; create a zaak/status change; confirm the BFF's +`/zgw/notificaties` endpoint receives and logs it. + +## Out of scope + +Cache invalidation on notification receipt (flagged separately in +`openzaak-integration.md` as a `ponytail:` marker, not part of this slice); +celery-flower/monitoring UI. + +## Risks + +Celery/celery-beat add real operational surface (another process to keep alive) — scope +this WP to "works, documented," not a fully monitored deployment. + +Depends on: WP-56 (provisioning mechanism this extends). diff --git a/docs/project/backlog/WP-59-document-confidentialiteit-config.md b/docs/project/backlog/WP-59-document-confidentialiteit-config.md new file mode 100644 index 0000000..6023c21 --- /dev/null +++ b/docs/project/backlog/WP-59-document-confidentialiteit-config.md @@ -0,0 +1,65 @@ +# WP-59 — Per-document-type confidentialiteit config + +Status: todo +Phase: 10 — OpenZaak production hardening + +## Why + +`OpenZaakDocumentSource` hardcodes `vertrouwelijkheidaanduiding` to `"openbaar"` for every +uploaded document, regardless of document type. Real BIG-register documents (diploma's, ID +scans) plausibly need different confidentiality levels. This repo already has a house +pattern for exactly this kind of business-tunable value — stamdata-as-code (ADR-0004) — so +this slice is "apply the existing pattern," not invent a new one. + +## Read first + +- [ADR-0004 — Stamdata as code](../reference/architecture/0004-stamdata-as-code.md) +- `backend/src/BigRegister.Api/Stamdata/` (an existing table for the shape to imitate) +- `backend/src/BigRegister.Api/Zgw/OpenZaakDocumentSource.cs` + +## Decisions (pre-made, don't relitigate) + +- Confidentiality level is keyed by document type (whatever type already distinguishes + uploads, e.g. diploma vs. id-bewijs) via a new Stamdata table, using the existing + `StamdataTable.Of` mechanism — not a new ad hoc config format. +- Default/fallback value stays `"openbaar"` if a document type isn't in the table, to + avoid a silent upload failure. + +## Files + +- `Stamdata/` (new table + validation) +- `Zgw/OpenZaakDocumentSource.cs` +- `StamdataCatalog.cs` (register the new table) + +## Steps + +1. Add a `DocumentConfidentialiteit` stamdata table (document type → + vertrouwelijkheidaanduiding), validated at build like every other stamdata table + (`StamdataValidationTests`). +2. Register it in `StamdataCatalog` so it's editable via the existing `/beheer/stamdata` + grid. +3. `OpenZaakDocumentSource` looks up the level by document type instead of hardcoding + `"openbaar"`. + +## Acceptance criteria + +- [ ] Confidentiality level for a real upload varies by document type per the new + stamdata table. +- [ ] `StamdataValidationTests` cover the new table (a bad edit fails CI, per ADR-0004). +- [ ] `/beheer/stamdata` can edit the new table without a code change (existing generic + editor). + +## Verification + +`cd backend && dotnet test`; manual: `/beheer/stamdata` shows and edits the new table; an +upload for a mapped document type carries the mapped confidentiality level (test +asserted). + +## Out of scope + +Any UI-facing confidentiality display/change on the citizen side (FE keeps rendering +decisions, not recomputing them, per ADR-0001). + +## Risks + +None significant — this is a config/data-shape change reusing an established mechanism. diff --git a/docs/project/backlog/WP-60-write-divergence-resilience.md b/docs/project/backlog/WP-60-write-divergence-resilience.md new file mode 100644 index 0000000..60d5745 --- /dev/null +++ b/docs/project/backlog/WP-60-write-divergence-resilience.md @@ -0,0 +1,74 @@ +# WP-60 — Write-divergence resilience (local + ZGW writes) + +Status: todo +Phase: 10 — OpenZaak production hardening + +## Why + +A citizen action today does a local `Aanvraag`/`Document` write and a paired ZGW write +(create zaak/status/document); these aren't transactional. If the ZGW call fails after the +local write succeeds (or vice versa), the two diverge silently — +`openzaak-integration.md` flags this explicitly as "acceptable for a demo backend; a +production arc needs retry/reconciliation or an outbox." This is the one genuine +correctness gap standing between the current integration and something safe to call +production. + +## Read first + +- [openzaak-integration.md](../reference/openzaak-integration.md) (the section discussing + this gap) +- `backend/src/BigRegister.Api/Data/ApplicationStore.cs`, + `Zgw/OpenZaakZaakSource.cs` (the two write sides) +- [ADR-0005 — OpenZaak behind the BFF](../reference/architecture/0005-openzaak-behind-bff.md) + +## Decisions + +Intentionally left open for kickoff — this is exactly the kind of ambiguous-root-cause, +multi-file design call the `planner` agent should make, not something pre-decided here. +Options to weigh at kickoff: + +- (a) an outbox table — write local + an outbox row in one local transaction, a background + worker drains the outbox to ZGW with retry. +- (b) a simpler synchronous retry-with-backoff at the call site, plus a reconciliation job + that periodically diffs local vs. ZGW state and flags/repairs divergence. + +Pick the smaller one that closes the gap — don't build a generic outbox framework if a +bounded retry+reconcile suffices for this POC's actual write volume. + +## Files + +Likely `Data/ApplicationStore.cs`, a new reconciliation/outbox mechanism, +`Zgw/OpenZaakZaakSource.cs`, `Program.cs` (background job registration if needed). + +## Steps + +1. Design review with the `planner` agent — pick outbox vs. retry+reconcile. +2. Implement the chosen mechanism for the create-zaak and status-transition write paths. +3. Add a test that simulates a ZGW failure mid-write and asserts the system recovers + (retries successfully, or is left in a detectably-inconsistent-but-flagged state) + rather than silently diverging. + +## Acceptance criteria + +- [ ] A simulated ZGW failure after a successful local write no longer leaves permanent + silent divergence — either it retries to consistency or the divergence is + detectable/flagged. +- [ ] No new synchronous latency added to the happy path beyond what the chosen mechanism + requires. + +## Verification + +A new integration test that fails a stubbed ZGW call mid-write and asserts +recovery/flagging behavior; `cd backend && dotnet test`. + +## Out of scope + +A general-purpose outbox framework reusable beyond this one write pair (YAGNI unless a +second write pair appears — note WP-66 is exactly that second pair, so revisit scope if +WP-66 lands first); UI surfacing of reconciliation state (backend-only fix for now). + +## Risks + +Over-building this (a generic outbox/saga framework) for a POC's actual write volume — +ladder check at kickoff: does a bounded retry + periodic reconcile job cover it before +reaching for an outbox table? diff --git a/docs/project/backlog/WP-61-behandelportal-bootstrap.md b/docs/project/backlog/WP-61-behandelportal-bootstrap.md new file mode 100644 index 0000000..b85aeba --- /dev/null +++ b/docs/project/backlog/WP-61-behandelportal-bootstrap.md @@ -0,0 +1,62 @@ +# WP-61 — Bootstrap the behandelportal app + +Status: todo +Phase: 11 — Behandelportal + +## Why + +ADR-0002 already designed the Behandelaar/backoffice as a separate sibling frontend app, +not a folder in this repo. Nothing exists yet — `/beheer/zaken` is confirmed to be only a +cross-owner list+delete, no treatment workflow. The `new-ssp` skill exists precisely to +bootstrap a new portal from this template; this slice is running that recipe for real, +with no business context yet — an empty, correctly-scaffolded shell. + +## Read first + +- `.claude/skills/new-ssp/SKILL.md` +- [ADR-0002 — user groups & bounded contexts](../reference/architecture/0002-user-groups-and-bounded-contexts.md) +- [ADR-0001 — BFF-lite decision DTOs](../reference/architecture/0001-bff-lite-decision-dtos.md) + +## Decisions (pre-made, don't relitigate) + +- Follow `new-ssp`'s own "keep vs. strip" checklist as-is: keep the `shared/ui` kernel, + tooling/CI gates, ADRs 0001-0003; strip the four citizen contexts and citizen branding. +- The new app talks to the same `BigRegister.Api` backend — no new backend service + (confirmed by ADR-0002: contexts integrate through the backend). +- Repo layout for the new app (separate repo vs. a second app in this monorepo) — decide + at kickoff based on how `new-ssp` is meant to be invoked. + +## Files + +Whatever `new-ssp`'s recipe touches (new app root, `package.json`, shared/ui copy or +workspace reference, CI config) — enumerate at kickoff by following the skill. + +## Steps + +1. Run the `new-ssp` bootstrap per its own checklist. +2. Confirm the known un-genericizable rough edges it flags (`shared/ui/debug-state/`, the + `/dashboard` route) are handled per the skill's own guidance (delete / TODO stopgap) + rather than re-solved from scratch. +3. Land an empty landing/login page only — no `behandeling` context yet (that's WP-64+). +4. Get the new app's own CI green. + +## Acceptance criteria + +- [ ] New app boots and its own `npm run ci` is green. +- [ ] No citizen-facing business context (`registratie`, `herregistratie`, `brief`, + `showcase`) present. +- [ ] Points at the same backend (`BigRegister.Api`) as this repo, no new backend stood + up. + +## Verification + +`npm run ci` in the new app; manual smoke — app loads to an empty shell page. + +## Out of scope + +Any `behandeling` screens (WP-64/65), identity (WP-62), status lifecycle (WP-63). + +## Risks + +`new-ssp`'s own docs already name its rough edges (`debug-state`, `/dashboard`) — budget +time for those rather than being surprised by them. diff --git a/docs/project/backlog/WP-62-medewerker-identity-authz.md b/docs/project/backlog/WP-62-medewerker-identity-authz.md new file mode 100644 index 0000000..ac52ef4 --- /dev/null +++ b/docs/project/backlog/WP-62-medewerker-identity-authz.md @@ -0,0 +1,73 @@ +# WP-62 — Backend: medewerker caller identity + authz seam + +Status: todo +Phase: 11 — Behandelportal + +## Why + +The backend's only identity today is `CallerIdentity` (BSN + display name, from WP-53) +modeling a single zorgverlener actor. ADR-0002 requires a second actor kind +(medewerker/employee) that authenticates differently (no BSN, has `rollen`) and needs its +own capability checks for backoffice calls. This slice adds that identity + authz surface +on the backend only — unused by any frontend until WP-64 calls it, matching the same +"seam, not provider" discipline WP-53 used for citizen identity (stub, no real employee +SSO — out of scope per CLAUDE.md, same as DigiD). + +## Read first + +- [ADR-0002 §3 — Principal union](../reference/architecture/0002-user-groups-and-bounded-contexts.md) +- `backend/src/BigRegister.Api/Domain/Authorization/CallerIdentity.cs`, + `IIdentityProvider.cs`, `StubIdentityProvider.cs` (WP-53's pattern to extend/mirror) +- [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) + +## Decisions (pre-made, don't relitigate) + +- Model the two actor kinds as a discriminated union (mirroring ADR-0002 §3: + `{ kind: 'zorgverlener'; bsn }` | `{ kind: 'medewerker'; medewerkerId; rollen }`), + backend-side, extending `CallerIdentity` rather than introducing a parallel type. +- Stub the medewerker identity the same way WP-53 stubbed citizen identity (a + header-driven `StubIdentityProvider` variant) — no real employee SSO/eHerkenning. +- New capability checks (e.g. `canBeoordelen`) are computed backend-side and exposed only + as decision flags, never a permission matrix shipped to a frontend (ADR-0001 discipline, + reaffirmed by ADR-0002 §3). + +## Files + +- `Domain/Authorization/CallerIdentity.cs` (extend to the union) +- `Domain/Authorization/StubIdentityProvider.cs` (medewerker variant) +- `Domain/Authorization/Authz.cs` (medewerker capability checks) +- Tests + +## Steps + +1. Extend `CallerIdentity` to the two-actor-kind union. +2. Extend the stub identity provider to produce a `medewerker` identity from a + header/config, alongside the existing zorgverlener stub. +3. Add capability checks a backoffice caller needs (start with `canBeoordelen`; extend as + WP-65 needs more). +4. Unit tests for both identity kinds and the new capability checks — no consumer exists + yet (WP-64+ will call this). + +## Acceptance criteria + +- [ ] `CallerIdentity` represents both actor kinds without breaking any existing + zorgverlener call site (WP-53's tests still green). +- [ ] A stub medewerker identity resolves from a request header, mirroring the existing + citizen stub. +- [ ] At least one capability flag (`canBeoordelen`) computable for a medewerker + identity, unit-tested. + +## Verification + +`cd backend && dotnet test` (existing WP-53 tests unaffected + new medewerker tests +green). + +## Out of scope + +Any actual backoffice endpoint using this (WP-64+); real employee SSO/eHerkenning. + +## Risks + +If the union is modeled as a bolt-on rather than replacing the flat type, existing +zorgverlener call sites could break — mitigated by keeping WP-53's existing tests as a +regression gate. diff --git a/docs/project/backlog/WP-63-aanvraag-status-lifecycle.md b/docs/project/backlog/WP-63-aanvraag-status-lifecycle.md new file mode 100644 index 0000000..0123f29 --- /dev/null +++ b/docs/project/backlog/WP-63-aanvraag-status-lifecycle.md @@ -0,0 +1,69 @@ +# WP-63 — Backend: aanvraag status lifecycle as a published DTO + +Status: todo +Phase: 11 — Behandelportal + +## Why + +The FE currently infers "in behandeling" from a single boolean, `pendingHerregistratie` +(`big-profile.store.ts:53`) — explicitly called out in ADR-0002 as "a temporary stand-in +for a real, backend-owned status." The full lifecycle (`Ingediend → In behandeling → +(Meer info gevraagd ⇄) → Goedgekeurd/Afgewezen`) needs to become a real backend-published +value before either frontend can render it meaningfully — the SSP needs it as a richer +read (this WP), the behandelportal needs it as the thing it advances (WP-65). + +## Read first + +- [ADR-0002](../reference/architecture/0002-user-groups-and-bounded-contexts.md) (status + lifecycle diagram) +- `src/app/registratie/application/big-profile.store.ts` (the current boolean) +- [ADR-0001 — BFF-lite decision DTOs](../reference/architecture/0001-bff-lite-decision-dtos.md) + +## Decisions (pre-made, don't relitigate) + +- Status lives on the existing `Aanvraag`/`ApplicationSummaryDto` aggregate (extend, + don't invent a parallel status resource). +- The DTO change is additive: the SSP's `pendingHerregistratie` boolean can be derived + from the new status field (or kept as a computed convenience) so this ships with zero + required FE behavior change — a pure backend + contract widening. +- Only the status *value* is published here; any transition (advancing it) is a separate + write endpoint, not part of this slice (that's WP-65's mutation). + +## Files + +- `Data/ApplicationStore.cs` (status field/enum) +- `Contracts/Dtos.cs` (extend `ApplicationSummaryDto`/status DTO) +- The FE `infrastructure/*.adapter.ts` + `parse*` boundary consuming it +- `big-profile.store.ts` (derive the existing boolean from the new field) + +## Steps + +1. Model the full status enum backend-side (`Ingediend`, `InBehandeling`, + `MeerInfoGevraagd`, `Goedgekeurd`, `Afgewezen`) on `Aanvraag`. +2. Publish it on the existing DTO the SSP already consumes. +3. Regenerate the typed client (`npm run gen:api`); update the FE `parse*` boundary to + read the new field. +4. Point `pendingHerregistratie` (or its replacement) at the new field so the SSP's + existing behavior is unchanged, just backed by a real value. + +## Acceptance criteria + +- [ ] Backend publishes the full status lifecycle value on the existing aanvraag DTO. +- [ ] `npm run gen:api` leaves no drift; SSP's existing "pending" display is unchanged in + behavior, now backed by the real status. +- [ ] `dotnet test` + `npm run ci` green. + +## Verification + +`cd backend && dotnet test`; `npm run gen:api` (no drift); `npm run ci`; manual: SSP +dashboard still shows the same pending/approved states it does today. + +## Out of scope + +Any endpoint that *advances* the status (WP-65); the behandelportal consuming it (WP-64). + +## Risks + +If the enum doesn't anticipate a state WP-65 needs (e.g. distinguishing who can transition +from what), it gets revised there — acceptable, this slice only needs to cover the states +already named in ADR-0002's diagram. diff --git a/docs/project/backlog/WP-64-behandelportal-werkvoorraad.md b/docs/project/backlog/WP-64-behandelportal-werkvoorraad.md new file mode 100644 index 0000000..638b9b3 --- /dev/null +++ b/docs/project/backlog/WP-64-behandelportal-werkvoorraad.md @@ -0,0 +1,65 @@ +# WP-64 — Behandelportal: werkvoorraad (queue) screen + +Status: todo +Phase: 11 — Behandelportal + +## Why + +First real screen in the new app — a read-only list of aanvragen needing treatment (the +"werkvoorraad"), gated by the medewerker identity from WP-62 and backed by the real status +DTO from WP-63. This is the smallest useful vertical slice of actual case-treatment +functionality — usable and demoable on its own, even before any decision can be recorded +(WP-65). + +## Read first + +- WP-61/62/63 outcomes +- `bff-endpoint` skill (screen-shaped decision DTO recipe) +- `src/app/registratie/ui/admin-cases.page.ts` (the existing cross-owner list, for what + to avoid repeating — that page is audit/delete, this one is a queue) + +## Decisions (pre-made, don't relitigate) + +- New BFF-lite endpoint (decision-enriched DTO) shaped for a werkvoorraad screen — not a + reuse of the existing `/admin/cases` endpoint, which is audit-shaped, not queue-shaped + (per CLAUDE.md's per-screen endpoint discipline). +- Gated by the `canBeoordelen`-style capability from WP-62, not a new ad hoc role check. +- Domain first, then infrastructure, application, UI — per the house `new-feature` + recipe. + +## Files + +New backend endpoint + DTO in `BigRegister.Api`; new `behandeling` context in the +behandelportal app (domain/infrastructure/application/ui per the house layering). + +## Steps + +1. Backend: new screen-shaped endpoint returning aanvragen needing treatment + (status = `InBehandeling`/`Ingediend`), gated by WP-62's capability. +2. FE: scaffold the `behandeling` context (domain → infrastructure → application → ui), + following `new-feature`. +3. UI: a list page (queue), composed from the shared `shared/ui` kernel — no new atoms + unless nothing existing fits. +4. Storybook story for the new list component/page, a11y-checked. + +## Acceptance criteria + +- [ ] Werkvoorraad screen lists aanvragen needing treatment for an authenticated + medewerker. +- [ ] `npm run ci` green in the behandelportal app; Storybook story present. +- [ ] Endpoint follows BFF-lite discipline (decision-enriched, not raw passthrough). + +## Verification + +`npm run ci` in the behandelportal app; `cd backend && dotnet test`; manual: log in as a +stub medewerker, see the queue populated from seeded aanvragen. + +## Out of scope + +Viewing a single zaak's detail (WP-65); recording any decision (WP-65). + +## Risks + +None major — this is a read-only composition slice once WP-61-63 exist. + +Depends on: WP-61, WP-62, WP-63. diff --git a/docs/project/backlog/WP-65-behandelportal-beoordeling.md b/docs/project/backlog/WP-65-behandelportal-beoordeling.md new file mode 100644 index 0000000..f6ca38d --- /dev/null +++ b/docs/project/backlog/WP-65-behandelportal-beoordeling.md @@ -0,0 +1,72 @@ +# WP-65 — Behandelportal: zaak detail + beoordeling (decision) screen + +Status: todo +Phase: 11 — Behandelportal + +## Why + +The core case-treatment write path — a medewerker opens one aanvraag's detail (including +its documents) and records a decision (goedkeuren/afwijzen/meer info opvragen), advancing +the status lifecycle WP-63 published. This is the first genuinely new *write* capability +in the system beyond what the citizen SSP already does to itself. + +## Read first + +- `mutation-command` skill +- `form-machine` skill (the decision action is a state-changing form, same idiom as + everywhere else in this house) +- [WP-63](WP-63-aanvraag-status-lifecycle.md) (the status field being advanced) + +## Decisions (pre-made, don't relitigate) + +- The decision action is modeled as a `*.machine.ts` (Model/Msg/reduce) + a `submit-*` + command returning `Result`, per this house's one-idiom-for-forms rule — not a + hand-rolled mutable field. +- The mutation endpoint is a new BFF-lite write (per `mutation-command` recipe) that + transitions the status field from WP-63; it validates the transition is legal + server-side (e.g. can't approve an already-approved case) — the backend remains the + authority. +- Runs against `LocalZaakSource` for this slice; wiring the decision into real OpenZaak is + explicitly WP-66, not bundled here — keeps this slice's surface to app-level behavior + only. + +## Files + +New mutation endpoint + command in `BigRegister.Api`; `behandeling/ui` detail page + +`behandeling/application` decision machine in the behandelportal app. + +## Steps + +1. Backend: mutation endpoint advancing aanvraag status (goedkeuren/afwijzen/meer-info- + opvragen), validating the transition. +2. FE: zaak-detail page (documents + current status) + a decision form machine + submit + command. +3. Wire the werkvoorraad list (WP-64) to link into this detail page. +4. Storybook stories + a11y for the new detail/decision UI. + +## Acceptance criteria + +- [ ] A medewerker can view one aanvraag's detail and record a decision that advances its + status. +- [ ] Illegal transitions are rejected server-side (tested). +- [ ] End-to-end smoke: werkvoorraad → detail → decision → status change reflected back + in the queue. +- [ ] `npm run ci` (behandelportal app) + `dotnet test` green. + +## Verification + +Manual/automated smoke test of the full werkvoorraad → beoordeling → besluit flow against +`LocalZaakSource`; `npm run ci`; `cd backend && dotnet test`. + +## Out of scope + +Writing the decision to real OpenZaak (WP-66). + +## Risks + +This is the largest FE slice in Phase 11 — if it feels too big at kickoff, split +detail-view (read) from decision-recording (write) into two sessions; the WP as scoped +already keeps them in one slice because a detail view with no decision action isn't +independently useful for a caseworker. + +Depends on: WP-64. diff --git a/docs/project/backlog/WP-66-behandelportal-openzaak-write.md b/docs/project/backlog/WP-66-behandelportal-openzaak-write.md new file mode 100644 index 0000000..26f6ee7 --- /dev/null +++ b/docs/project/backlog/WP-66-behandelportal-openzaak-write.md @@ -0,0 +1,69 @@ +# WP-66 — Wire the decision into OpenZaak + +Status: todo +Phase: 11 — Behandelportal + +## Why + +WP-65's decision currently only updates local state (`LocalZaakSource`). For the +behandelportal to actually function against a real register, the recorded decision needs +to also write a besluit/status transition to ZGW — extending the write capability that +already partially exists (`CreateStatusRequest`, `CreateRolRequest` in +`OpenZaakZaakSource.cs`) rather than building a new ZGW client from scratch. + +## Read first + +- `backend/src/BigRegister.Api/Zgw/OpenZaakZaakSource.cs` (existing write records) +- ZGW's Besluiten API (referenced in `openzaak-integration.md` if covered, or the ZGW + standard docs) for besluit creation +- [WP-50](WP-50-openzaak-create-zaak.md) (the first ZGW write slice, for the pattern to + follow) + +## Decisions (pre-made, don't relitigate) + +- Extend `IZaakSource`/`OpenZaakZaakSource` with a besluit/status-transition write, + following the same pattern WP-50 established for create-zaak (a records + mapper + addition, not a new abstraction). +- Gated by `Zgw:Enabled` like every other ZGW write — the behandelportal keeps working + against `LocalZaakSource` when it's off. +- Best done after Phase 10's WP-60 (write-divergence resilience) lands, since this is + exactly the second write pair that resilience work should already cover — but not + strictly blocked on it if Phase 10 is still in progress (call out the residual risk + explicitly if shipped first). + +## Files + +`Zgw/OpenZaakZaakSource.cs` (besluit/status write), `Data/IZaakSource.cs` (new write +method), tests. + +## Steps + +1. Add the besluit/status-transition write to `OpenZaakZaakSource`, mirroring WP-50's + create-zaak pattern. +2. Wire WP-65's decision command to call it when `Zgw:Enabled=true`. +3. Integration test against the WP-54 harness (extend `OpenZaakIntegrationTests`). + +## Acceptance criteria + +- [ ] A recorded decision writes a real besluit/status transition to OpenZaak when + `Zgw:Enabled=true`. +- [ ] Behandelportal still works unchanged against `LocalZaakSource` when + `Zgw:Enabled=false`. +- [ ] `OpenZaakIntegrationTests` covers the new write. + +## Verification + +`cd backend && dotnet test --filter Category=Integration` against the (ideally +Phase-10-hardened) OpenZaak harness; manual smoke with `Zgw:Enabled=true`. + +## Out of scope + +Any further behandelportal screens beyond beoordeling. + +## Risks + +If Phase 10's WP-60 (write-divergence resilience) hasn't landed yet, this introduces a +second unprotected write pair — call this out explicitly if the two phases aren't +sequenced together in practice. + +Depends on: WP-65. Benefits from (but doesn't strictly require) WP-60.