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:
eho
2026-07-29 09:03:13 +02:00
co-authored by Claude Sonnet 5
parent abc4728c97
commit de3bff0d7f
10 changed files with 303 additions and 25 deletions
@@ -9,8 +9,8 @@ namespace BigRegister.Api.Data;
/// contract</em> — so the frontend never changes (BFF-lite anti-corruption, ADR-0001).
///
/// Default binding is <see cref="LocalZaakSource"/> (offline). Setting <c>Zgw:Enabled=true</c>
/// swaps in <c>OpenZaakZaakSource</c>. Slice 1 is read-only; create/update stay on the
/// local write path until WP-50. The interface returns the wire DTO (not the domain
/// swaps in <c>OpenZaakZaakSource</c>. Slice 1 (WP-49) was read-only; <see cref="CreateZaak"/>
/// (WP-50) is the first write. The interface returns the wire DTO (not the domain
/// <see cref="Aanvraag"/>) precisely so each source owns its own mapping — the OpenZaak
/// source maps a ZGW Zaak into this shape, the local source maps the stored aanvraag.
/// </summary>
@@ -18,4 +18,15 @@ public interface IZaakSource
{
/// <summary>Every case, newest-first (the admin cross-owner list, WP-36).</summary>
IReadOnlyList<ApplicationSummaryDto> ListCases(DateTimeOffset now);
/// <summary>
/// Register a just-submitted <paramref name="aanvraag"/> as a zaak (WP-50). The aanvraag is
/// already persisted locally (<c>ApplicationStore.Submit</c> already ran) — this is the
/// integration side-effect, and its return value is what the submit endpoint hands back to
/// the FE (ADR-0001: route the create through the existing submit response DTO, don't add a
/// second one). The local source is a pure passthrough of the already-computed local
/// reference/status; the OpenZaak source creates a Zaak (+ status + rol) and maps the result
/// back into the same shape.
/// </summary>
(string Referentie, AanvraagStatusDto Status) CreateZaak(Aanvraag aanvraag, DateTimeOffset now);
}
@@ -12,4 +12,9 @@ public sealed class LocalZaakSource : IZaakSource
{
public IReadOnlyList<ApplicationSummaryDto> ListCases(DateTimeOffset now) =>
ApplicationStore.ListAll().Select(a => a.ToAdminSummaryDto(now)).ToList();
/// <summary>No external zaak to create — the aanvraag's local submit already IS the record
/// of truth, exactly as before this seam existed (WP-50). Zero behaviour change.</summary>
public (string Referentie, AanvraagStatusDto Status) CreateZaak(Aanvraag aanvraag, DateTimeOffset now) =>
(aanvraag.Referentie!, aanvraag.ToStatusDto(now));
}