Extends the IZaakSource seam (WP-49, read-only) with CreateZaak: submitting
an aanvraag now also registers a Zaak + Status + Rol in OpenZaak when
Zgw:Enabled=true, routed through the existing /applications/{id}/submit
endpoint with the FE response DTO unchanged (ADR-0001/ADR-0005 — the
endpoint never branches on the config flag itself, DI already picked the
implementation).
- ZgwOptions gains a Type→zaaktype-URL map + the two RSINs a Zaak needs.
- LocalZaakSource.CreateZaak is a pure passthrough of what the endpoint
already computes locally (zero behaviour change for the offline default).
- OpenZaakZaakSource.CreateZaak POSTs the zaak (identificatie = the same
local reference, so both stay in sync), resolves + POSTs the initial
status and the initiator rol (BSN) via Catalogi lookups, and maps the
result back into the submit response.
- Marked ponytail shortcuts: first-statustype/roltype-Catalogi-returns
(no per-type config) and no compensating transaction on partial failure
— both fine for a first slice against a demo backend.
Verified: full `npm run ci` green, zero api-client drift, 144/144 backend
tests (142 existing + 2 new stub-handler tests asserting the POST bodies
+ type→zaaktype mapping per the acceptance criteria).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
170 lines
11 KiB
Markdown
170 lines
11 KiB
Markdown
# OpenZaak / ZGW integration — how the BFF connects (& how to extend)
|
|
|
|
How the BFF sources (and now creates) cases against 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) and
|
|
[WP-50](../project/backlog/WP-50-openzaak-create-zaak.md) (the first write: create-zaak).
|
|
|
|
## 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 + (WP-50) WRITE interface: `ListCases` and
|
|
`CreateZaak`. Both return the existing DTOs, so each implementation owns its own mapping.
|
|
- `Data/LocalZaakSource.cs` — **default**; reads the local SQLite `ApplicationStore`
|
|
(offline, unchanged behaviour). `CreateZaak` is a pure passthrough of what the submit
|
|
endpoint already computed locally — no external call.
|
|
- `Zgw/OpenZaakZaakSource.cs` — the OpenZaak client; selected only when `Zgw:Enabled=true`.
|
|
`CreateZaak` posts a Zaak, then a Status, then a Rol (see below).
|
|
- Wiring (`Program.cs`): `if (Zgw:Enabled) AddHttpClient<IZaakSource, OpenZaakZaakSource>()
|
|
else AddSingleton<IZaakSource, LocalZaakSource>()`. The `/admin/cases` GET and the
|
|
`/applications/{id}/submit` POST both resolve `IZaakSource` from DI — routes + DTOs
|
|
untouched either way.
|
|
|
|
## Create-zaak (WP-50) — the first write
|
|
|
|
`POST /applications/{id}/submit` already persists the aanvraag locally (`ApplicationStore.Submit`
|
|
— unconditionally, regardless of `Zgw:Enabled`, since draft/step/document bookkeeping stays
|
|
local either way) and only THEN calls `zaken.CreateZaak(submitted, now)`. The submit endpoint
|
|
never branches on `Zgw:Enabled` itself — DI already picked the implementation, so the endpoint
|
|
just asks the seam for `(Referentie, Status)` and returns exactly that in the unchanged
|
|
`SubmitApplicationResponse`. Under the default (local) source this returns precisely what was
|
|
just computed; under OpenZaak, three calls happen in order:
|
|
|
|
1. **POST zaak** (`{ZrcBaseUrl}/zaken`) — `zaaktype` resolved from `Zgw:ZaaktypeUrls[aanvraag.Type]`
|
|
(OpenZaak validates the URL by fetching it), `bronorganisatie`/`verantwoordelijkeOrganisatie`
|
|
(RSIN) from config, `identificatie` set to the **same** reference `ApplicationStore.Submit`
|
|
already generated — so the human-readable reference matches in both places, not two
|
|
independently-generated ones.
|
|
2. **POST status** (`{ZrcBaseUrl}/statussen`) — `statustype` resolved via a Catalogi GET
|
|
(`statustypen?zaaktype=...`, lowest `volgnummer`); marks the zaak as freshly opened.
|
|
3. **POST rol** (`{ZrcBaseUrl}/rollen`) — `roltype` resolved via a Catalogi GET
|
|
(`roltypen?zaaktype=...&omschrijvingGeneriek=initiator`); `betrokkeneIdentificatie.inpBsn`
|
|
set to the aanvraag's owner (BSN) — the current stand-in for real identity (WP-53).
|
|
|
|
The created zaak's `identificatie` becomes the returned `Referentie`; its status maps to the
|
|
same coarse `InBehandeling` shape `ZgwZaakMapper` already uses for a freshly-opened zaak
|
|
(`ZgwZaakMapper.ToCreatedStatusDto`).
|
|
|
|
ponytail shortcuts, marked at the call sites: (a) "first statustype/roltype Catalogi returns"
|
|
rather than a fully-configured per-type map — fine while a zaaktype has exactly one initial
|
|
status and initiator role; (b) no compensating transaction — if any ZGW call throws, the
|
|
aanvraag is already `Submitted` locally with no matching zaak (acceptable for a demo backend;
|
|
a production arc needs retry/reconciliation or an outbox before trusting this dual-write).
|
|
|
|
## 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>",
|
|
// WP-50 (create-zaak): RSINs + the aanvraag-type → zaaktype URL map.
|
|
"Bronorganisatie": "<RSIN>", "VerantwoordelijkeOrganisatie": "<RSIN>",
|
|
"ZaaktypeUrls": {
|
|
"registratie": "https://open-zaak.example/catalogi/api/v1/zaaktypen/<uuid>",
|
|
"herregistratie": "https://open-zaak.example/catalogi/api/v1/zaaktypen/<uuid>",
|
|
"intake": "https://open-zaak.example/catalogi/api/v1/zaaktypen/<uuid>"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Anti-corruption layer — two nested boundaries (what to learn)
|
|
|
|
This setup is an anti-corruption layer (ACL) **twice over**, and seeing them as a pair is the
|
|
lesson worth taking away:
|
|
|
|
1. **The BFF guards everything against upstream systems.** OpenZaak's foreign model —
|
|
URL-as-identity, a `zaaktype` that is a URL _into another service_, `{count,next,previous,
|
|
results}` pagination, HS256 JWT auth — never leaves the BFF. `ZgwZaakMapper` translates it
|
|
into the BFF's own `ApplicationSummaryDto`; `IZaakSource` makes the boundary swappable
|
|
(`LocalZaakSource` vs `OpenZaakZaakSource` return the _same_ DTO).
|
|
2. **The Angular app guards itself against the BFF.** `infrastructure/` is the only layer that
|
|
touches the network (lint-enforced); every response crosses a `parse*` (`Result`) trust
|
|
boundary + a `toDomain` mapper before any domain/UI code sees it (ADR-0001, ARCHITECTURE §6).
|
|
|
|
The DTO at `/api/v1` is the membrane between them — which is why wiring OpenZaak touched **zero
|
|
frontend code and produced zero api-client drift**. That was the proof the ACL held.
|
|
|
|
Principles this demonstrates:
|
|
|
|
- **An ACL is a _mapping_, not a passthrough.** A DTO that is the upstream shape renamed is
|
|
corruption with extra steps; the valuable ACLs here (`ZgwZaakMapper`, the `parse*`/`toDomain`
|
|
pairs) actively translate a foreign model into a local one.
|
|
- **Put the ACL where trust changes, and make it the _only_ place.** One choke point per
|
|
boundary — the `Zgw/` folder + `IZaakSource` server-side, `infrastructure/` client-side.
|
|
- **Decision DTOs and the ACL are complementary.** BFF-lite (server decides, FE renders) is an
|
|
ACL against _business-rule_ drift, layered on the ACL against _data-shape_ drift.
|
|
- **A real seam is swap-testable offline.** Because the ACL returns a stable DTO, the ZGW
|
|
client is unit-testable with fixtures + a stub handler — no live OpenZaak.
|
|
- **Mark the honest edges.** The `ponytail:` sync-over-async note and the "coarse status map"
|
|
comment in `ZgwZaakMapper` show where the ACL is deliberately thin — an ACL need not be
|
|
complete on day one, but its shortcuts should be visible.
|
|
|
|
Caveat: `IZaakSource` now covers the cases **read + create** path (WP-49/50). Other BFF
|
|
endpoints still read `SeedData`/static stores directly — ACL-ready (the DTO seam exists) but not
|
|
yet swappable. That is the WP-51/52 roadmap, plus the two cross-cutting WPs the arc needs for
|
|
production: **WP-53** (a real per-request identity seam + citizen-scoping — today the owner/BSN
|
|
is stubbed) and **WP-54** (a docker OpenZaak harness + opt-in integration test — today everything
|
|
is fixture/mock-tested against no live instance).
|
|
|
|
## 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 (CRUD arc), WP-53/54 (identity seam + integration harness).
|
|
- `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).
|