feat(zgw): OpenZaak create-zaak, first write slice (WP-50)
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>
This commit is contained in:
@@ -64,6 +64,8 @@ up front — the migration stance ADR-0001 already prescribes.
|
||||
- **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.
|
||||
- **Also shipped (WP-50):** `IZaakSource.CreateZaak` — the first write. Submitting an aanvraag
|
||||
now also creates a Zaak + Status + Rol in OpenZaak when `Zgw:Enabled=true`, routed through the
|
||||
existing submit endpoint with zero DTO change (same seam, same anti-corruption boundary).
|
||||
- **Deferred:** real inbound OIDC/JWT auth (still header-stubbed), Documenten/DRC upload + link
|
||||
(WP-51), Notificaties/NRC webhooks (WP-52), adding OpenZaak to docker-compose.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# 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).
|
||||
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
|
||||
|
||||
@@ -14,14 +15,48 @@ 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/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).
|
||||
(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` endpoint resolves
|
||||
`IZaakSource` from DI — routes + DTOs untouched.
|
||||
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/`)
|
||||
|
||||
@@ -75,7 +110,14 @@ path async if OpenZaak becomes the default.
|
||||
"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>"
|
||||
"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>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -111,9 +153,9 @@ Principles this demonstrates:
|
||||
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: today only the cases **read** path has a source interface (`IZaakSource`). Other BFF
|
||||
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-50/51/52 roadmap, plus the two cross-cutting WPs the arc needs for
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user