feat(backend): expand stamdata + OpenZaak-ready cases seam (WP-49)
CI / frontend (push) Successful in 2m59s
CI / backend (push) Successful in 1m27s
CI / semgrep (push) Successful in 58s
CI / e2e (push) Successful in 2m30s
CI / api-client-drift (push) Canceled after 1m14s
CI / storybook-a11y (push) Canceled after 29m8s
CI / frontend (push) Successful in 2m59s
CI / backend (push) Successful in 1m27s
CI / semgrep (push) Successful in 58s
CI / e2e (push) Successful in 2m30s
CI / api-client-drift (push) Canceled after 1m14s
CI / storybook-a11y (push) Canceled after 29m8s
Stamdata: add beroepen, opleidingen (temporal), and specialismen tables to the schema-driven catalog (zero UI code). opleidingen.beroep and specialismen.beroep both reference beroepen.code — the first stamdata->stamdata references, enforced by two new StamdataRef entries in the CI gate. OpenZaak/ZGW (WP-49, slice 1 — read-only zaken): introduce IZaakSource as the cases read seam. Default LocalZaakSource reads the local SQLite store (offline); an OpenZaakZaakSource (Zgw/ client: HS256 per-call JWT, ZGW->existing-DTO mapper, paginating HTTP source) is selected behind Zgw:Enabled (default false). The FE never changes — same ApplicationSummaryDto, no api-client drift. Unit-tested with fixtures + a stub HttpMessageHandler; no live OpenZaak needed. Docs: ADR-0005, reference/openzaak-integration.md, WP-49..52 roadmap, stamdata.md update, README index rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# OpenZaak / ZGW integration — how the BFF connects (& how to extend)
|
||||
|
||||
How the BFF sources cases from a real **OpenZaak** (ZGW APIs) while the frontend stays
|
||||
unchanged. For the _why_, see [ADR-0005](architecture/0005-openzaak-behind-bff.md); this page
|
||||
is _how the seam is built and how to add the next slice_. Built in
|
||||
[WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (read-only zaken).
|
||||
|
||||
## The one rule: OpenZaak sits behind the BFF, never in the browser
|
||||
|
||||
The Angular app only ever sees the BFF's decision DTOs (BFF-lite, ADR-0001). All ZGW
|
||||
awkwardness — URL-as-identity, cross-service joins, JWT auth, pagination — is absorbed by the
|
||||
.NET BFF. Flipping the data source from local SQLite to OpenZaak is a **backend config change**
|
||||
with **zero frontend change and no api-client drift**.
|
||||
|
||||
## The seam (data source by config)
|
||||
|
||||
- `Data/IZaakSource.cs` — the cases READ interface. Returns the existing
|
||||
`ApplicationSummaryDto`, so each implementation owns its own mapping.
|
||||
- `Data/LocalZaakSource.cs` — **default**; reads the local SQLite `ApplicationStore`
|
||||
(offline, unchanged behaviour).
|
||||
- `Zgw/OpenZaakZaakSource.cs` — the OpenZaak client; selected only when `Zgw:Enabled=true`.
|
||||
- Wiring (`Program.cs`): `if (Zgw:Enabled) AddHttpClient<IZaakSource, OpenZaakZaakSource>()
|
||||
else AddSingleton<IZaakSource, LocalZaakSource>()`. The `/admin/cases` endpoint resolves
|
||||
`IZaakSource` from DI — routes + DTOs untouched.
|
||||
|
||||
## The ZGW client (`backend/src/BigRegister.Api/Zgw/`)
|
||||
|
||||
- `ZgwOptions.cs` — bound from the `Zgw` appsettings section: `Enabled`, per-service base URLs
|
||||
(`ZrcBaseUrl`, `ZtcBaseUrl`), `ClientId`, `Secret`, `UserId`, `UserRepresentation`. The five
|
||||
ZGW APIs are separate base URLs; slice 1 needs only Zaken (ZRC) + Catalogi (ZTC).
|
||||
- `ZgwTokenProvider.cs` — mints an **HS256 JWT per call** (`iss`/`client_id`/`iat`/`user_id`/
|
||||
`user_representation`). No refresh flow — OpenZaak expires tokens 1h past `iat`, so per-call
|
||||
minting is the recommended pattern. Hand-rolled (no `Microsoft.IdentityModel.*` dependency).
|
||||
- `ZgwZaakMapper.cs` — the anti-corruption map: ZGW Zaak → `ApplicationSummaryDto`. This is
|
||||
where **URL identity** becomes the trailing uuid and the **zaaktype URL** is resolved to a
|
||||
human label (the cross-service join).
|
||||
- `OpenZaakZaakSource.cs` — follows `{count,next,previous,results}` pagination, resolves +
|
||||
caches zaaktype labels, attaches `Authorization: Bearer <jwt>`.
|
||||
|
||||
## The five ZGW APIs (context for later slices)
|
||||
|
||||
| API | Component | Used by |
|
||||
| ------------ | --------- | --------------------------------------------------- |
|
||||
| Zaken | ZRC | slice 1 (read), WP-50 (create) |
|
||||
| Catalogi | ZTC | slice 1 (zaaktype label; also type URLs for create) |
|
||||
| Documenten | DRC | WP-51 (upload + zaak↔document link) |
|
||||
| Besluiten | BRC | later (formal decisions) |
|
||||
| Notificaties | NRC | WP-52 (live status via webhooks, not polling) |
|
||||
|
||||
## How to add the next slice
|
||||
|
||||
1. **Read** — extend `IZaakSource` (or add a sibling interface, e.g. `IDocumentSource`) with
|
||||
the new operation; implement it on both `LocalZaakSource` and the OpenZaak source. Keep the
|
||||
return type the existing DTO so the FE never changes.
|
||||
2. **Write** (create-zaak, WP-50) — a create needs a `zaaktype` URL from Catalogi (OpenZaak
|
||||
validates it by fetching), then usually a follow-up `status` + `rol`. Route it through the
|
||||
existing submit/mutation seam.
|
||||
3. **Enforce server-side** for anything the FE gates — a config value the FE echoes is never
|
||||
the authority (ADR-0001).
|
||||
|
||||
## Coupling
|
||||
|
||||
Low and one-directional. Consumer coupling is near zero — `IZaakSource` is injected at one
|
||||
endpoint, and the FE is fully decoupled by the DTO. The producer side is contained in `Zgw/`:
|
||||
add a slice by adding a source method + a mapper case, not by touching the FE or the contract.
|
||||
Watch the **sync-over-async** `ponytail:` note in `OpenZaakZaakSource` — make the cases read
|
||||
path async if OpenZaak becomes the default.
|
||||
|
||||
## Config
|
||||
|
||||
```jsonc
|
||||
// appsettings.json — off by default (POC runs offline on the local store)
|
||||
"Zgw": {
|
||||
"Enabled": true,
|
||||
"ZrcBaseUrl": "https://open-zaak.example/zaken/api/v1",
|
||||
"ZtcBaseUrl": "https://open-zaak.example/catalogi/api/v1",
|
||||
"ClientId": "big-register", "Secret": "<from a secret store>",
|
||||
"UserId": "<session user>", "UserRepresentation": "<session name>"
|
||||
}
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [ADR-0005 — OpenZaak behind the BFF](architecture/0005-openzaak-behind-bff.md) — the decision.
|
||||
- [ADR-0001 — BFF-lite + decision DTOs](architecture/0001-bff-lite-decision-dtos.md) — why the FE doesn't change.
|
||||
- [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (this), WP-50/51/52 (later slices).
|
||||
- `backend/src/BigRegister.Api/Zgw/` — the client; `Data/IZaakSource.cs` — the seam.
|
||||
- [ZGW standard (VNG)](https://vng-realisatie.github.io/gemma-zaken/) · [OpenZaak auth docs](https://open-zaak.readthedocs.io/en/stable/client-development/authentication.html).
|
||||
Reference in New Issue
Block a user