feat(openzaak): bounded retry + flagged write divergence (WP-60)

Local aanvraag/document writes and their paired ZGW writes aren't
transactional; a ZGW failure after the local write succeeds used to
diverge silently. ZgwHttpClient now retries transport-shaped failures
(not 500, which can follow a partial commit on the non-idempotent
statussen/rollen POSTs), and a ZGW failure that survives retry sets
Aanvraag.ZgwError plus a zgw:divergence audit row instead of failing
or diverging quietly. No outbox/reconcile job: three request-triggered
write paths don't justify a persisted queue that would also need to
carry citizen PII for the JWT audit claims.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-30 18:11:55 +02:00
co-authored by Claude Sonnet 5
parent 67abc58052
commit 3ff80c124f
18 changed files with 855 additions and 82 deletions
+9 -10
View File
@@ -110,7 +110,7 @@ for its existing violations, so every WP ends green.
| [WP-57](WP-57-openzaak-least-privilege-scopes.md) | Least-privilege client scopes | 10 · OpenZaak hardening | done |
| [WP-58](WP-58-openzaak-notifications.md) | Real notifications (celery + scripted abonnement) | 10 · OpenZaak hardening | done |
| [WP-59](WP-59-document-confidentialiteit-config.md) | Per-document-type confidentialiteit config | 10 · OpenZaak hardening | done |
| [WP-60](WP-60-write-divergence-resilience.md) | Write-divergence resilience (local + ZGW writes) | 10 · OpenZaak hardening | todo |
| [WP-60](WP-60-write-divergence-resilience.md) | Write-divergence resilience (local + ZGW writes) | 10 · OpenZaak hardening | done |
| [WP-61](WP-61-behandelportal-bootstrap.md) | Bootstrap the behandelportal app | 11 · Behandelportal | todo |
| [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | todo |
| [WP-63](WP-63-aanvraag-status-lifecycle.md) | Backend: aanvraag status lifecycle as a published DTO | 11 · Behandelportal | todo |
@@ -149,17 +149,16 @@ deployment of 4952) and **54** (a docker OpenZaak harness + opt-in integratio
CRUD arc and can land any time; 54 depends on 49 (something to read) and unlocks realistic
testing for the rest. Both are self-contained (each WP file carries its own current-state
handoff) and sized for a fresh Sonnet session.
Phase 10 (OpenZaak production hardening, WP-55..60) and Phase 11 (Behandelportal,
WP-61..66) are two independent tracks that can be worked concurrently — neither blocks
the other. Within phase 10: 55/59/60 are fully independent; 57 and 58 both build on 56's
provisioning mechanism, otherwise independent of each other. Within phase 11: 61
Phase 10 (OpenZaak production hardening, WP-55..60 — now **done**) and Phase 11
(Behandelportal, WP-61..66) are two independent tracks that can be worked concurrently —
neither blocks the other. Within phase 10: 55/59/60 were fully independent; 57 and 58 both
built on 56's provisioning mechanism, otherwise independent of each other. Within phase 11: 61
(bootstrap), 62 (backend medewerker identity), and 63 (backend status DTO) are
independent of each other and can land in any order; 64 needs all three (61 for the app
to exist, 62 for identity, 63 for the status it reads); 65 needs 64; 66 needs 65 and
benefits from — but doesn't strictly require — phase 10's WP-60 landing first (WP-66 is
a second, currently-unprotected write pair otherwise). WP-60 is the one slice in phase 10
sized for a `planner`-agent kickoff rather than direct implementation — its Decisions
block is deliberately left open (outbox vs. retry+reconcile).
to exist, 62 for identity, 63 for the status it reads); 65 needs 64; 66 needs 65 and — now
that WP-60 has landed (bounded retry + flagged divergence in `ZgwHttpClient`/`Program.cs`) —
inherits that retry for free, but must call `RecordZgwDivergence` on its own besluit write path
to get the flagging half too.
## WP template