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:
@@ -23,6 +23,8 @@ condensed, cross-linked curriculum.
|
||||
| [architecture/0002-user-groups-and-bounded-contexts.md](reference/architecture/0002-user-groups-and-bounded-contexts.md) | ADR — user groups as actors; identity vs authorization. |
|
||||
| [architecture/0003-cibg-huisstijl.md](reference/architecture/0003-cibg-huisstijl.md) | ADR — adopt CIBG Huisstijl (vendored Bootstrap 5.2) + the token bridge. |
|
||||
| [architecture/0004-stamdata-as-code.md](reference/architecture/0004-stamdata-as-code.md) | ADR — business-tunable reference data as typed, compile-time-validated config (not a production DB). |
|
||||
| [architecture/0005-openzaak-behind-bff.md](reference/architecture/0005-openzaak-behind-bff.md) | ADR — connect to OpenZaak (ZGW APIs) behind the BFF via a config-gated data-source seam; the FE never changes. |
|
||||
| [openzaak-integration.md](reference/openzaak-integration.md) | How the BFF sources cases from OpenZaak (the IZaakSource seam + ZGW client), and how to add the next slice. |
|
||||
| [stamdata.md](reference/stamdata.md) | How stamdata (config-as-code reference data) is laid out, how to add a table with zero UI code, and why coupling stays low. |
|
||||
| [audit-log.md](reference/audit-log.md) | How the data-minimised authz/PII-reveal audit trail is built, how to audit a new action, and the one-producer-hub coupling. |
|
||||
| [feature-flags.md](reference/feature-flags.md) | How runtime feature flags work (catalog-as-code + runtime state), how to add one, and the hand-wired gating coupling to watch. |
|
||||
|
||||
@@ -93,6 +93,10 @@ for its existing violations, so every WP ends green.
|
||||
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
|
||||
| [WP-47](WP-47-feature-flags.md) | Runtime feature flags (catalog-in-code, admin toggle, FE+backend) | 8 · platform/DX/showcase | done |
|
||||
| [WP-48](WP-48-stamdata-deletion-protection.md) | Stamdata deletion protection (CI referential gate + editor expire/warn) | 8 · platform/DX/showcase | done |
|
||||
| [WP-49](WP-49-openzaak-zaken-read-seam.md) | OpenZaak zaken read seam (IZaakSource + ZGW client, config-gated, offline default) | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-50](WP-50-openzaak-create-zaak.md) | OpenZaak create-zaak (first write slice) | 9 · OpenZaak/ZGW | todo |
|
||||
| [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | todo |
|
||||
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | 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
|
||||
@@ -114,6 +118,10 @@ WP-40 (P2) → WP-43 (P3) → WP-41 → WP-42 → WP-44 → WP-45 (P4). Ordering
|
||||
make the generator simple) + 43; 45 (`create-ssp`) needs 43+44. 37/38/39/40/43/46 are otherwise
|
||||
independent. Two open tool forks, decided as step 1 of their WP: 38 dependency-cruiser vs Sheriff;
|
||||
43 plop vs Angular schematics.
|
||||
Phase 9 (OpenZaak/ZGW integration, WP-49..52) is strictly ordered 49 → 50 → 51 → 52: 49 lands
|
||||
the source seam + ZGW client the rest reuse; 50 (create) needs a zaak to exist; 51 (documents)
|
||||
links to 50's zaak; 52 (notificaties) reacts to changes on the zaken 49–51 manage. All slices
|
||||
stay behind `Zgw:Enabled` (default off) so the POC keeps running offline.
|
||||
|
||||
## WP template
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# WP-49 — OpenZaak zaken read seam (slice 1)
|
||||
|
||||
Status: done
|
||||
Phase: 9 — OpenZaak / ZGW integration
|
||||
|
||||
## Why
|
||||
|
||||
The POC serves cases from local SQLite. To grow toward production it must be able to source
|
||||
them from a real ZGW backend (OpenZaak) **without changing the frontend** (BFF-lite, ADR-0001).
|
||||
The backend had no data-access abstraction to swap behind, no outbound HTTP, and no JWT. This is
|
||||
the first thin vertical slice: read-only zaken.
|
||||
|
||||
## Read first
|
||||
|
||||
- [ADR-0005 — OpenZaak behind the BFF](../reference/architecture/0005-openzaak-behind-bff.md)
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- OpenZaak's anti-corruption layer lives in the **BFF**, never the browser.
|
||||
- Source selected by config (`Zgw:Enabled`, default false) → POC still runs offline.
|
||||
- Each source maps into the **existing** `ApplicationSummaryDto` → no api-client drift, no FE change.
|
||||
- Fresh HS256 JWT **per call** (no refresh flow). Hand-rolled (no new NuGet).
|
||||
- `IZaakSource` is sync (matches the endpoint + local store); OpenZaak source does sync-over-async.
|
||||
|
||||
## Files
|
||||
|
||||
- `Data/IZaakSource.cs`, `Data/LocalZaakSource.cs`
|
||||
- `Zgw/{ZgwOptions,ZgwTokenProvider,ZgwZaakMapper,OpenZaakZaakSource}.cs`
|
||||
- `Program.cs` (DI + resolve `IZaakSource` in `/admin/cases`), `appsettings.json` (`Zgw` section)
|
||||
- tests: `ZgwTokenProviderTests`, `ZgwZaakMapperTests`, `OpenZaakZaakSourceTests`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Extract the cases read into `IZaakSource`; `LocalZaakSource` delegates to `ApplicationStore`.
|
||||
2. Build the `Zgw/` client (options, JWT minter, ZGW→DTO mapper, paginating HTTP source).
|
||||
3. Wire DI by config; refactor `/admin/cases` to resolve `IZaakSource`.
|
||||
4. Unit-test the minter, mapper, and source (fixtures + stub `HttpMessageHandler`).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `/admin/cases` serves identical DTOs via `LocalZaakSource` (default, offline).
|
||||
- [x] `OpenZaakZaakSource` follows pagination, resolves+caches zaaktype labels, sends a Bearer token.
|
||||
- [x] JWT verifies (HS256) with the required claims.
|
||||
- [x] `dotnet test` green (142); `npm run ci` green with **no api-client drift** (FE untouched).
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test`; `npm run ci`; manual: `/beheer/zaken` still lists cases with `Zgw:Enabled=false`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Create-zaak (WP-50), Documenten/DRC (WP-51), Notificaties (WP-52), real inbound OIDC/JWT,
|
||||
OpenZaak in docker-compose.
|
||||
|
||||
## Risks
|
||||
|
||||
Sync-over-async blocks a thread under load if OpenZaak becomes the default → make the read path
|
||||
async then (noted at the call site).
|
||||
@@ -0,0 +1,52 @@
|
||||
# WP-50 — OpenZaak create-zaak (first write slice)
|
||||
|
||||
Status: todo
|
||||
Phase: 9 — OpenZaak / ZGW integration
|
||||
|
||||
## Why
|
||||
|
||||
WP-49 made the cases **read** path source-swappable. The next slice is the first **write**:
|
||||
create a Zaak in OpenZaak when an aanvraag is submitted, still behind the config gate, still
|
||||
without changing the FE contract.
|
||||
|
||||
## Read first
|
||||
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md) — "How to add the next slice"
|
||||
- [ADR-0005](../reference/architecture/0005-openzaak-behind-bff.md), [ADR-0001](../reference/architecture/0001-bff-lite-decision-dtos.md)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Route the create through the existing submit/mutation seam; keep the FE response DTO identical.
|
||||
- A create needs a `zaaktype` **URL** from Catalogi (OpenZaak validates it by fetching) — map
|
||||
the aanvraag `type` → a configured zaaktype URL.
|
||||
- Follow the create with a `status` + a `rol` (initiator/betrokkene by BSN) as ZGW expects.
|
||||
|
||||
## Files
|
||||
|
||||
- Extend `IZaakSource` (or add a write method) + `OpenZaakZaakSource`; `LocalZaakSource` keeps
|
||||
the current local submit.
|
||||
- `ZgwOptions`: a `type → zaaktype URL` map + `bronorganisatie`/`verantwoordelijkeOrganisatie` (RSIN).
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add `CreateZaak` to the source seam; local impl = current submit, OpenZaak impl = POST to ZRC.
|
||||
2. Map aanvraag `type` → zaaktype URL; POST zaak, then status + rol.
|
||||
3. Map the created Zaak back into the existing submit response DTO.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Submitting with `Zgw:Enabled=true` creates a Zaak (+ status + rol) in OpenZaak.
|
||||
- [ ] FE submit response DTO unchanged; no api-client drift.
|
||||
- [ ] Covered by tests (stub handler asserts the POST bodies + type→zaaktype mapping).
|
||||
|
||||
## Verification
|
||||
|
||||
`dotnet test`; against a docker OpenZaak if available.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Documenten (WP-51), Notificaties (WP-52).
|
||||
|
||||
## Risks
|
||||
|
||||
Create needs read scope on Catalogi (type-URL validation) — provision AC scopes accordingly.
|
||||
@@ -0,0 +1,52 @@
|
||||
# WP-51 — OpenZaak Documenten (DRC) upload + link
|
||||
|
||||
Status: todo
|
||||
Phase: 9 — OpenZaak / ZGW integration
|
||||
|
||||
## Why
|
||||
|
||||
Uploaded documents currently persist as bytes in local SQLite (`DocumentStore`). To be
|
||||
production-ready they must live in OpenZaak's **Documenten API (DRC)** as
|
||||
`enkelvoudiginformatieobjecten`, linked to a Zaak via `zaakinformatieobject` — behind the same
|
||||
config gate, still without a FE contract change.
|
||||
|
||||
## Read first
|
||||
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md)
|
||||
- WP-49 (the seam pattern), WP-50 (create-zaak, the zaak to link to)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Introduce an `IDocumentSource` sibling of `IZaakSource`; local impl = `DocumentStore`,
|
||||
OpenZaak impl = DRC. FE upload DTOs unchanged.
|
||||
- A document needs an `informatieobjecttype` **URL** from Catalogi (like zaaktype for a zaak).
|
||||
- Upload → returns document URL → `zaakinformatieobject` links it to the zaak URL.
|
||||
|
||||
## Files
|
||||
|
||||
- `Data/IDocumentSource.cs`, `Data/LocalDocumentSource.cs`, `Zgw/OpenZaakDocumentSource.cs`
|
||||
- `ZgwOptions`: `informatieobjecttype` URL(s) + `DrcBaseUrl`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Abstract the upload/read/link paths behind `IDocumentSource`.
|
||||
2. OpenZaak impl: POST `enkelvoudiginformatieobjecten` (content), then POST `zaakinformatieobjecten`.
|
||||
3. Map DRC document metadata back into the existing document DTOs.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] With `Zgw:Enabled=true`, an upload lands in DRC and is linked to its zaak.
|
||||
- [ ] FE upload/list DTOs unchanged; no api-client drift.
|
||||
- [ ] Tests cover the DRC POST bodies + the zaak-link step (stub handler).
|
||||
|
||||
## Verification
|
||||
|
||||
`dotnet test`; against a docker OpenZaak if available.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Notificaties (WP-52), content virus-scanning / blob-storage tuning.
|
||||
|
||||
## Risks
|
||||
|
||||
Large file content over base64/multipart — mind memory; stream if needed.
|
||||
@@ -0,0 +1,51 @@
|
||||
# WP-52 — OpenZaak Notificaties (NRC) live status
|
||||
|
||||
Status: todo
|
||||
Phase: 9 — OpenZaak / ZGW integration
|
||||
|
||||
## Why
|
||||
|
||||
With cases in OpenZaak, case status changes in the backoffice, not in this app. Production
|
||||
"live" status needs the **Notificaties API (NRC)**: subscribe to zaak events and update on
|
||||
webhook, rather than polling. Last slice of the ZGW integration arc.
|
||||
|
||||
## Read first
|
||||
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md)
|
||||
- WP-49/50/51 (the read/write/document slices this builds on)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- The BFF exposes a webhook endpoint that NRC calls; it validates an Authorization header the
|
||||
BFF issued, then invalidates any cached case data / notifies the FE.
|
||||
- Subscription (`abonnement` on the `zaken` kanaal) is provisioning/config, not runtime code.
|
||||
- FE update mechanism reuses the existing RemoteData reload — no new FE contract.
|
||||
|
||||
## Files
|
||||
|
||||
- A new BFF webhook endpoint (`POST /zgw/notificaties`) + Authorization validation.
|
||||
- `ZgwOptions`: `NrcBaseUrl` + the webhook shared secret.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add the webhook endpoint (auth-checked, no PII logged — reuse the audit seam).
|
||||
2. On a zaak event, invalidate cache / push an update to the FE.
|
||||
3. Document the `abonnement` provisioning (out-of-band, one-time).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A posted NRC event (correct auth) triggers a case refresh; a bad-auth post is rejected.
|
||||
- [ ] No PII in the webhook logs.
|
||||
- [ ] Tests cover auth accept/reject + the refresh trigger.
|
||||
|
||||
## Verification
|
||||
|
||||
`dotnet test`; against a docker OpenZaak + NRC if available.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Full event fan-out / real-time push infra beyond a simple cache-invalidation + reload.
|
||||
|
||||
## Risks
|
||||
|
||||
Webhook must be reachable from NRC in prod (network/ingress) — a deployment concern, not code.
|
||||
@@ -0,0 +1,69 @@
|
||||
# ADR-0005 — OpenZaak (ZGW APIs) behind the BFF
|
||||
|
||||
Status: Accepted · Date: 2026-07-24
|
||||
|
||||
## Context
|
||||
|
||||
The POC serves cases (aanvragen) from a local SQLite store. To grow toward production it must
|
||||
be able to source cases from a real Dutch **Zaakgericht Werken (ZGW)** backend — **OpenZaak**,
|
||||
the VNG reference implementation. ZGW is not one API but five separate services (Zaken/ZRC,
|
||||
Documenten/DRC, Catalogi/ZTC, Besluiten/BRC, Notificaties/NRC), each on its own base URL, with
|
||||
traits that make raw responses unfit to hand to a browser:
|
||||
|
||||
- resources are identified by **full URLs**, not bare ids;
|
||||
- references between resources are **URLs into other services** (a zaak's `zaaktype` lives in
|
||||
Catalogi), so a single screen means joining across services;
|
||||
- lists use a uniform `{count,next,previous,results}` pagination envelope;
|
||||
- auth is a short-lived **HS256 JWT** signed with a client secret (no OAuth refresh), which
|
||||
OpenZaak rejects an hour past `iat`.
|
||||
|
||||
Two constraints shaped the decision: the **frontend must not change** (BFF-lite, ADR-0001 —
|
||||
the FE renders decision DTOs and never recomputes rules), and the POC must **still run fully
|
||||
offline** (no OpenZaak needed for local dev/CI).
|
||||
|
||||
The backend, however, had **no data-access abstraction** — endpoints called concrete static
|
||||
stores directly — and no outbound HTTP or JWT machinery. So there was no injection point to
|
||||
swap a data source behind.
|
||||
|
||||
## Options
|
||||
|
||||
1. **FE talks to OpenZaak directly.** Rejected: leaks ZGW shapes + the client secret to the
|
||||
browser, and contradicts BFF-lite.
|
||||
2. **Rewrite the static stores in place to call OpenZaak.** Rejected: no seam, no offline mode,
|
||||
all-or-nothing, untestable without a live server.
|
||||
3. **Introduce a data-source interface behind the existing DTO contract, select the
|
||||
implementation by config.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Put the OpenZaak anti-corruption layer **in the .NET BFF**, never in the browser. Introduce a
|
||||
per-domain source interface (starting with `IZaakSource` for the cases read path) whose default
|
||||
implementation reads the local SQLite store and whose alternate implementation calls OpenZaak —
|
||||
selected by a config flag (`Zgw:Enabled`, default false). Each implementation maps into the
|
||||
**existing** wire DTO (`ApplicationSummaryDto`), so the `/api/v1` contract and the FE are
|
||||
untouched. The BFF holds the client secret and **mints a fresh JWT per outbound call**.
|
||||
|
||||
This is deliberately a **thin vertical slice** (read-only zaken, WP-49); create/documents/
|
||||
notifications follow the same seam in later slices (WP-50/51/52) rather than being scaffolded
|
||||
up front — the migration stance ADR-0001 already prescribes.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **+** The FE is production-ready as-is: swapping to OpenZaak is backend-only, behind one
|
||||
config flag, with zero DTO/api-client drift. The POC still runs offline (default = local).
|
||||
- **+** The seam is unit-testable without a live server: the JWT minter, the ZGW→DTO mapper,
|
||||
and the paginating source are all covered with fixtures + a stub `HttpMessageHandler`.
|
||||
- **+** URL-as-identity and cross-service joins are contained in one mapper; nothing downstream
|
||||
sees a ZGW shape.
|
||||
- **−** Only the cases **read** path has a source interface today; other endpoints still call
|
||||
static stores directly. Each future slice introduces its own seam as needed (not a big-bang
|
||||
repository refactor).
|
||||
- **−** `IZaakSource` is synchronous (matching the existing sync endpoint + local store), so
|
||||
`OpenZaakZaakSource` does sync-over-async; fine under ASP.NET Core (no sync-context), to be
|
||||
made async if OpenZaak becomes the default. Marked with a `ponytail:` note at the call site.
|
||||
- **Shipped with this ADR (WP-49):** `IZaakSource` + `LocalZaakSource` (default) +
|
||||
`OpenZaakZaakSource` (config-gated), the `Zgw/` client (`ZgwOptions`, `ZgwTokenProvider`,
|
||||
`ZgwZaakMapper`), and the reference guide [openzaak-integration.md](../openzaak-integration.md).
|
||||
- **Deferred:** real inbound OIDC/JWT auth (still header-stubbed), create-zaak (WP-50),
|
||||
Documenten/DRC upload + link (WP-51), Notificaties/NRC webhooks (WP-52), adding OpenZaak to
|
||||
docker-compose.
|
||||
@@ -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).
|
||||
@@ -6,6 +6,11 @@ build**, not a runtime-editable database. For the _why_, see
|
||||
[ADR-0004 — Stamdata as code](architecture/0004-stamdata-as-code.md); this page is _how the
|
||||
code is laid out and how to add a table without coupling_. Built in WP-29, hardened in WP-48.
|
||||
|
||||
Tables today: `professions` (opleiding-program → beroep), `beroepen` (the BIG professions master
|
||||
list), `opleidingen` (temporal; `beroep` → `beroepen.code`) and `specialismen` (`beroep` →
|
||||
`beroepen.code`). The last two are **stamdata → stamdata** references — one table keyed on by two
|
||||
others — enforced by the CI gate below.
|
||||
|
||||
## The one rule that shapes everything: no runtime write path
|
||||
|
||||
The catalog is the source of truth and lives in code. The admin editor **downloads** an
|
||||
@@ -50,8 +55,11 @@ component**. This is the payoff of the schema-driven design.
|
||||
|
||||
`backend/tests/BigRegister.Tests/StamdataValidationTests.cs`. `Every_catalog_table_is_valid`
|
||||
covers every registered table generically; the `StamdataRef` list catches dangling
|
||||
references (today: `Diploma.Opleiding → professions.program`). A bad edit, an orphaning
|
||||
delete, or a premature expire **fails the PR build** — never prod.
|
||||
references — both seed → stamdata (`Diploma.Opleiding → professions.program`) and
|
||||
stamdata → stamdata (`Opleiding.beroep → beroepen.code`, `Specialisme.beroep →
|
||||
beroepen.code`). A bad edit, an orphaning delete, or a premature expire **fails the PR
|
||||
build** — never prod. Adding a cross-table FK is one `StamdataRef` entry: the referencing
|
||||
keys + a resolver against the target table's (valid-today) keys.
|
||||
|
||||
## Coupling
|
||||
|
||||
|
||||
Reference in New Issue
Block a user