Compare commits

..
Author SHA1 Message Date
notandClaude Opus 4.8 af83194e79 fix(acl): use camelCase indicatieGebruiksrecht so OpenZaak honours it (refs #103)
CI / lint (pull_request) Successful in 1m22s
CI / build (pull_request) Successful in 1m4s
CI / unit (pull_request) Successful in 1m31s
CI / frontend (pull_request) Successful in 2m43s
CI / mutation (pull_request) Successful in 5m43s
CI / verify-stack (pull_request) Successful in 8m24s
The ZGW Documenten API property is indicatieGebruiksrecht (camelCase); the
all-lowercase key was silently dropped on write (DRF camel-case mapping),
leaving the field null so the zaak still could not close. Correct the JSON
name on the DTO and both assertions to match the ZGW spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:52:34 +02:00
notandClaude Opus 4.8 756e718ee2 fix(acl): set indicatiegebruiksrecht=false on the diploma document (refs #103)
CI / lint (pull_request) Successful in 1m22s
CI / build (pull_request) Successful in 1m3s
CI / unit (pull_request) Successful in 1m18s
CI / frontend (pull_request) Successful in 2m45s
CI / mutation (pull_request) Successful in 5m42s
CI / verify-stack (pull_request) Failing after 5m44s
Records 'no usage-rights restrictions' explicitly so OpenZaak allows the
related zaak to be closed on behandelaar approval, fixing the verify-stack
e2e regression introduced by S-10b diploma upload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:31:44 +02:00
notandClaude Opus 4.8 53e9564f94 test(acl): diploma document sets indicatiegebruiksrecht so the zaak can close (refs #103)
The behandelaar approval closes the zaak via OpenZaak. A related
informatieobject with indicatieGebruiksrecht left null makes OpenZaak
reject the close (400 indicatiegebruiksrecht-unset), surfacing as a 500
and failing the registration e2e flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:31:44 +02:00
notandClaude Opus 4.8 5325a99755 fix(acceptance): CapturingDomainClient matches the new ProvideDocumentsAsync signature (refs #103)
CI / lint (pull_request) Successful in 1m19s
CI / build (pull_request) Successful in 1m0s
CI / unit (pull_request) Successful in 1m11s
CI / frontend (pull_request) Successful in 2m29s
CI / mutation (pull_request) Successful in 5m26s
CI / verify-stack (pull_request) Failing after 12m23s
The BFF IDomainClient.ProvideDocumentsAsync grew (base64 file + name/type); the
acceptance host's fake wasn't updated, breaking the Release build (per-project Debug
builds missed it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:23:48 +02:00
notandClaude Opus 4.8 036005e486 docs(acl): ADR-0018 + demo/backlog for diploma upload via the ACL Documenten API (refs #103)
CI / lint (pull_request) Successful in 1m19s
CI / build (pull_request) Failing after 56s
CI / unit (pull_request) Failing after 1m6s
CI / frontend (pull_request) Successful in 2m37s
CI / mutation (pull_request) Successful in 5m44s
CI / verify-stack (pull_request) Has been cancelled
Records the ACL-fronted Documenten create+relate, the base64-JSON transport chosen
over proposal #107's multipart, the informatieobjecttype seeding, and the S-10c
(zaak-close) carve-off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:18:46 +02:00
notandClaude Opus 4.8 ae7fc1b8f0 test(acl,e2e): live diploma-upload coverage (refs #103)
- ACL integration test: stores a real document against a published Diploma
  informatieobjecttype and asserts it is persisted (bestandsnaam, informatieobjecttype,
  bestandsomvang) and related to the zaak.
- e2e: the registration journey now selects a PDF and uploads it (setInputFiles)
  before the behandelaar step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:17:28 +02:00
notandClaude Opus 4.8 1abd4b6472 feat(portal): real diploma file upload on the self-service page (refs #103)
Replaces the S-10a stub button with a labelled file input (accept application/pdf);
the component base64-encodes the chosen file client-side and posts it (with its name
and type) keyed by the reference, confirming on success and surfacing a retryable
failure. The upload button stays disabled until a file is chosen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:15:49 +02:00
notandClaude Opus 4.8 d354fe507a test(portal): self-service uploads a chosen diploma file (refs #103)
RED: after submitting, the citizen picks a PDF and uploads it; the component base64-
encodes it client-side and posts { contentBase64, fileName, contentType } keyed by the
reference, then confirms. Replaces the S-10a stub button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:14:36 +02:00
notandClaude Opus 4.8 4c516cdad3 feat(bff): documents endpoint accepts the base64 file and forwards it to the domain (refs #103)
The self-service documents endpoint takes { contentBase64, fileName, contentType }
as JSON (bsn from the token) and forwards it via IDomainClient.ProvideDocumentsAsync.
Regenerates openapi.json + the Angular client (postSelfServiceRegistrationsIdDocuments
now takes a ProvideDocumentsRequest body).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:13:10 +02:00
notandClaude Opus 4.8 9d327bbd81 test(bff): documents endpoint forwards the base64 file to the domain (refs #103)
RED: the self-service documents endpoint takes the base64 file (+ fileName/contentType)
as JSON — the portal encodes client-side — with the bsn from the token, and forwards
all of it to the domain. IDomainClient.ProvideDocumentsAsync grows accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:07:42 +02:00
notandClaude Opus 4.8 3c344caa29 feat(domain): ProvideDocuments stores the diploma via the ACL then completes the wait (refs #103)
IAclClient.StoreDiplomaAsync + AclHttpClient (base64 JSON to the ACL /documenten
endpoint). ProvideDocuments stores the uploaded bytes against the zaak (when opened)
before completing WachtOpDocumenten; the domain endpoint accepts the file base64-encoded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:05:40 +02:00
notandClaude Opus 4.8 ecad42873c test(domain): providing documents stores the diploma via the ACL then completes the wait (refs #103)
RED: ProvideDocuments now carries the file bytes, stores them against the zaak via
IAclClient before completing the WachtOpDocumenten wait; owner-scoped; best-effort
when no zaak/process exists yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:02:32 +02:00
notandClaude Opus 4.8 4b4b58b486 feat(infra): seed the Diploma informatieobjecttype + wire the ACL default (refs #103)
seed_catalogus.py (OZ_PUBLISH) creates a "Diploma" informatieobjecttype, relates it
to the zaaktype (zaaktype-informatieobjecttypen, while both concept), publishes both,
and prints INFORMATIEOBJECTTYPE_URL. verify-domain captures it and recreates the ACL
with Acl__Defaults__InformatieobjecttypeUrl (placeholder default in compose otherwise).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 12:00:48 +02:00
notandClaude Opus 4.8 dca9455bb5 feat(acl): store a diploma in the Documenten API and relate it to the zaak (refs #103)
IZaakGateway.StoreDocumentAsync creates an enkelvoudiginformatieobject (base64
inhoud, buffered non-chunked body, no CRS) and relates it via zaakinformatieobjecten;
AclService.StoreDiplomaAsync default-fills the ZGW document fields (informatieobjecttype,
bronorganisatie, taal nld, creatiedatum); POST /documenten exposes it. Adds the
InformatieobjecttypeUrl default (wired in a following infra commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:57:39 +02:00
notandClaude Opus 4.8 aaa7135fb1 test(acl): gateway stores a diploma as an enkelvoudiginformatieobject + relates it (refs #103)
RED: StoreDocumentAsync creates the informatieobject in the Documenten API (base64
inhoud, bestandsomvang, definitief) with a Bearer token and a buffered (non-chunked)
body and no CRS headers, then relates it to the zaak via zaakinformatieobjecten, and
surfaces an OpenZaak rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:53:59 +02:00
23 changed files with 46 additions and 597 deletions
+1 -9
View File
@@ -215,9 +215,7 @@ Split (issue #11 closed) into two independently-demoable slices per §13 — the
#### S-10c · Close the ZGW zaak on document-timeout expiry — #106 #### S-10c · Close the ZGW zaak on document-timeout expiry — #106
**Outcome:** when the 30-day term lapses (S-10a `RegistratieVerlopen`), the ZGW zaak is set to a distinct non-terminal `Geannuleerd` status + `Vervallen` resultaat (not just the domain aggregate → `Verlopen`), resolved by name in the ACL. Adds the cancellation statustype/resultaattype to the seed + an ACL `CancelZaakAsync`/`POST /annuleringen` + expiry-worker wiring. Carved from S-10b (ADR-0017/0018/0019). Depends on #103. **Outcome:** when the 30-day term lapses (S-10a `RegistratieVerlopen`), the ZGW zaak is set to a cancellation status (not just the domain aggregate → `Verlopen`). Adds a cancellation statustype/resultaattype to the seed + an ACL method + expiry-worker wiring. Carved from S-10b (ADR-0017/0018). Depends on #103.
**Acceptance:** ACL↔OpenZaak integration test (cancellation records `Geannuleerd` + a resultaat, live); the domain verify script fires the P30D timer and asserts the zaak reaches `Geannuleerd` end-to-end; BDD asserts the zaak is cancelled on timeout but untouched when documents arrive in time.
### S-11 · Withdrawal (Flow 3) ### S-11 · Withdrawal (Flow 3)
@@ -239,12 +237,6 @@ Split (issue #11 closed) into two independently-demoable slices per §13 — the
**Outcome:** Boundary timer on beoordeling user task — 14 days. On timeout, reassigns to a teamlead role. **Outcome:** Boundary timer on beoordeling user task — 14 days. On timeout, reassigns to a teamlead role.
### S-26 · Self-service — resume an existing registration after refresh — #111
**Outcome:** a signed-in zorgprofessional who reloads the self-service portal (or returns later) gets back to their in-flight registration and its actions (Documenten aanleveren, Trek aanvraag in), instead of a blank submit form with the reference lost. Today all post-submit state lives in in-memory signals, the reference is not in the URL, and there is no self-service read endpoint — so a reload strands the registration. Adds an owner-scoped (DigiD bsn) `GET /self-service/registrations` on the BFF/domain and a load-on-init/route restore in the portal.
**Acceptance:** BDD — resume after refresh shows the existing registration; lookup is owner-scoped (never another citizen's); a user with no in-flight registration still sees the submit form. Playwright e2e reloads mid-flow and asserts the actions remain reachable.
--- ---
## Iteration 3 — Maintenance portal and observability *(milestone: `Iteration 3 — Beheer & Observability`)* ## Iteration 3 — Maintenance portal and observability *(milestone: `Iteration 3 — Beheer & Observability`)*
@@ -1,81 +0,0 @@
# ADR-0019: A timed-out zaak is cancelled with a distinct status + resultaat, resolved by name
- **Status:** Accepted
- **Date:** 2026-07-21
- **Deciders:** Respellion engineering
- **Relates to:** S-10c (#106). Completes the S-10a/S-10b boundary noted in ADR-0017 (§Consequences) and
reuses the ACL close-zaak machinery from S-09b (approval) and the Documenten work in ADR-0018.
## Context
ADR-0017 (S-10a) cancels the *process* and marks the domain aggregate `Verlopen` when the 30-day
document term lapses, but explicitly deferred setting the ZGW **zaak** to a cancellation status. Left
open, a timed-out zaak stays open in OpenZaak while the register shows the registration as lapsed — the
two diverge. S-10c closes that gap: on expiry the domain must also cancel the zaak through the ACL
(§8.1, the only code that talks to ZGW).
The non-obvious part is *how to represent "cancelled" in ZGW* alongside the existing "approved" close.
The approval path (S-09b) sets the zaak's **eindstatus** (the terminal statustype) plus a resultaat. In
ZGW a zaaktype has exactly one eindstatus — the highest-`volgnummer` statustype — and setting it is what
closes the zaak (`einddatum`). A second *terminal* status would collide with that single-eindstatus rule.
## Decision
**Model cancellation as a distinct, non-terminal `Geannuleerd` statustype plus a distinct `Vervallen`
resultaat, and resolve both the approval and cancellation statustype/resultaat by their omschrijving
(name) rather than by position or the eindstatus flag alone.**
- **Seed.** `Geannuleerd` is seeded at `volgnummer` 2 — between `Ontvangen` (1) and the `Afgehandeld`
eindstatus (3) — so it is a *non-terminal* status and never displaces the eindstatus the approval path
resolves. A second resultaattype `Vervallen` (archiefnominatie `vernietigen`) is seeded beside the
approval `Geregistreerd` (`blijvend_bewaren`); both draw their `selectielijstklasse` from the
zaaktype's single `selectielijstProcestype` so they validate on publish.
- **The ACL owns the mapping.** `OpenZaakGateway.SetZaakToCancellationStatusAsync` resolves `Geannuleerd`
+ `Vervallen` by omschrijving and POSTs the resultaat then the status (OpenZaak requires a resultaat
before a closing/terminal status), mirroring `SetZaakToEindstatusAsync`. Exposed as
`AclService.CancelZaakAsync` behind the ACL endpoint `POST /annuleringen`. The omschrijvingen live as
constants in the gateway — the ACL, not the domain, knows which ZGW status means what (§8.1).
- **Approval now resolves its resultaat by name too.** With two resultaattypen present, taking the first
is ambiguous (the Zaken API does not guarantee order), so the approval path resolves `Geregistreerd`
by omschrijving. Its statustype resolution is unchanged (still the eindstatus).
- **Domain wiring.** The `ExpireRegistrationWorker` calls `IAclClient.CancelZaakAsync(zaakUrl)` **before**
advancing the aggregate to `Verlopen` (ACL-first, mirroring approval): if the ACL call fails the job is
redelivered (§8.6) rather than leaving the aggregate `Verlopen` with an open zaak. The existing
open-state guard stops a redelivered job from cancelling twice (a second resultaat would be a 400); a
registration that lapsed before its zaak was opened has nothing to cancel.
## Consequences
**Positive**
- The domain aggregate and the ZGW zaak no longer diverge on timeout — both reflect the cancellation.
- Reuses the approval close machinery (resultaat-then-status, ACL endpoint shape, ACL-first ordering), so
the change is additive and §8 stays clean (only the ACL talks to ZGW).
- Verified at two levels: an ACL↔OpenZaak integration test asserts the live zaak reaches `Geannuleerd`
with a resultaat, and the domain verify script fires the real P30D timer and confirms the zaak is
cancelled end-to-end.
**Negative / costs**
- `Geannuleerd` is non-terminal, so the cancelled zaak's `einddatum` is not set — it carries a
cancellation status + resultaat but is not formally "closed" in ZGW. Accepted: the register reads the
domain aggregate's status, and a single eindstatus per zaaktype is a ZGW constraint we chose not to
fight. Formally closing a cancelled zaak (a second eindstatus, or reusing `Afgehandeld` with a
`Vervallen` resultaat) is a possible follow-up.
- The ACL couples to the seeded omschrijvingen (`Geregistreerd`/`Geannuleerd`/`Vervallen`) by string
constants. This mirrors the existing implicit coupling to the catalogus (zaaktype URL, eindstatus) and
is documented in the gateway.
- Renumbering `Afgehandeld` from `volgnummer` 2 to 3 means a *stale* local catalogus must have its
OpenZaak volumes reset for the change to take effect; CI reseeds a fresh catalogus each run.
## Alternatives considered
- **Shared eindstatus, distinct resultaat only** (reuse `Afgehandeld`, distinguish approval vs
cancellation purely by the resultaat). ZGW-idiomatic and would set `einddatum` on cancellation too, but
the register would show no visibly distinct cancellation *status*. Rejected in favour of the issue's
explicit "distinct statustype + resultaattype" outcome, which makes the cancellation legible in ZGW.
- **A second terminal (eindstatus) `Geannuleerd`.** Rejected: ZGW allows only one eindstatus per
zaaktype (highest volgnummer); a second terminal status would either not close the zaak or collide with
the approval eindstatus resolution.
- **Passing the target omschrijvingen from the domain.** Rejected: which ZGW status means "cancelled" is
ZGW vocabulary the ACL owns (§8.1); the domain says only "cancel this zaak".
+1 -21
View File
@@ -461,24 +461,4 @@ make verify-acl # → "Storing a diploma creates a real informatieobject
`ProvideDocuments` → ACL `POST /documenten` → ZGW `enkelvoudiginformatieobjecten` + `ProvideDocuments` → ACL `POST /documenten` → ZGW `enkelvoudiginformatieobjecten` +
`zaakinformatieobjecten`; the wait is then completed and the case advances to Beoordelen (§8.1, ADR-0018). `zaakinformatieobjecten`; the wait is then completed and the case advances to Beoordelen (§8.1, ADR-0018).
## S-10c — the ZGW zaak is cancelled when the document term lapses (#106) > Setting the ZGW zaak to a cancellation status on 30-day expiry is a follow-up (S-10c, #106).
When the 30-day document term lapses (S-10a), the domain no longer only marks the aggregate `Verlopen` —
it now also cancels the **ZGW zaak** through the ACL, so OpenZaak and the register agree. The zaak is set
to a distinct, non-terminal **`Geannuleerd`** status with a **`Vervallen`** resultaat (as opposed to the
approval `Afgehandeld` + `Geregistreerd`), resolved by name in the ACL (§8.1, ADR-0019).
```bash
# 1. The ACL integration test proves cancellation records the Geannuleerd status + a resultaat
# against a live OpenZaak:
make verify-acl # → "Cancelling a zaak records the geannuleerd status and a resultaat"
#
# 2. End-to-end: the domain check submits a registration, fires its 30-day timer early, and asserts
# the timeout worker both expires the registration (VERLOPEN) and cancels its zaak (Geannuleerd):
make verify-domain # → "the timed-out registration's zaak was cancelled to Geannuleerd in OpenZaak"
```
**The path:** Flowable P30D timer → `RegistratieVerlopen` job → domain `ExpireRegistrationWorker` → ACL
`POST /annuleringen` → ZGW `resultaten` + `statussen` (Geannuleerd); the aggregate then moves to
`Verlopen`. The ACL cancels the zaak **before** the aggregate is expired, so a failed ZGW call leaves the
job for redelivery rather than diverging the two (ADR-0019).
+13 -34
View File
@@ -10,7 +10,7 @@ Creates (if absent):
Auth uses the JWT client provisioned by setup_configuration (see ADR-0002). Auth uses the JWT client provisioned by setup_configuration (see ADR-0002).
Stdlib only — no pip deps. Re-running is safe (matches existing by identifier). Stdlib only — no pip deps. Re-running is safe (matches existing by identifier).
""" """
import base64, hashlib, hmac, json, os, sys, time, urllib.error, urllib.parse, urllib.request import base64, hashlib, hmac, json, os, sys, time, urllib.error, urllib.request
BASE = os.environ.get("OZ_BASE", "http://localhost:8000") BASE = os.environ.get("OZ_BASE", "http://localhost:8000")
CLIENT_ID = os.environ.get("OZ_CLIENT_ID", "big-reference-seed") CLIENT_ID = os.environ.get("OZ_CLIENT_ID", "big-reference-seed")
@@ -77,12 +77,8 @@ def publish_zaaktype(zt):
Selectielijst `selectielijstklasse` whose procestype matches the zaaktype's Selectielijst `selectielijstklasse` whose procestype matches the zaaktype's
`selectielijstProcestype`, plus a `resultaattypeomschrijving`. `selectielijstProcestype`, plus a `resultaattypeomschrijving`.
""" """
# Ontvangen (begin) → Afgehandeld (eind, highest volgnummer). "Geannuleerd" (S-10c) sits between
# them: a non-terminal status the document-timeout branch sets, so it never displaces the Afgehandeld
# eindstatus the approval path resolves. Keyed by volgnummer on a fresh catalogus (CI reseeds); a
# stale local stack must reset its OpenZaak volumes for the renumbering to take effect.
have_st = {s.get("volgnummer") for s in find(f"/statustypen?zaaktype={zt['url']}&status=alles")} have_st = {s.get("volgnummer") for s in find(f"/statustypen?zaaktype={zt['url']}&status=alles")}
for volgnummer, omschrijving in [(1, "Ontvangen"), (2, "Geannuleerd"), (3, "Afgehandeld")]: for volgnummer, omschrijving in [(1, "Ontvangen"), (2, "Afgehandeld")]:
if volgnummer not in have_st: if volgnummer not in have_st:
st, body = api("POST", "/statustypen", { st, body = api("POST", "/statustypen", {
"omschrijving": omschrijving, "zaaktype": zt["url"], "volgnummer": volgnummer}) "omschrijving": omschrijving, "zaaktype": zt["url"], "volgnummer": volgnummer})
@@ -99,42 +95,25 @@ def publish_zaaktype(zt):
sys.exit(f"create roltype -> {st}: {json.dumps(body, indent=2)}") sys.exit(f"create roltype -> {st}: {json.dumps(body, indent=2)}")
print("create roltype Aanvrager") print("create roltype Aanvrager")
# Two resultaattypen, keyed by omschrijving so each is created independently (idempotent): if find(f"/resultaattypen?zaaktype={zt['url']}&status=alles"):
# "Geregistreerd" — the approval outcome (S-09b) print("skip resultaattype Geregistreerd")
# "Vervallen" — the document-timeout cancellation outcome (S-10c)
# Both selectielijstklassen must share the zaaktype's selectielijstProcestype, so pick two
# Selectielijst resultaten from a single procestype and set that procestype on the zaaktype.
have_rt = {r.get("omschrijving") for r in find(f"/resultaattypen?zaaktype={zt['url']}&status=alles")}
wanted = [("Geregistreerd", "blijvend_bewaren"), ("Vervallen", "vernietigen")]
if all(naam in have_rt for naam, _ in wanted):
print("skip resultaattypen Geregistreerd + Vervallen")
else: else:
# Anchor on the procestype of an arbitrary resultaat, then fetch that procestype's resultaten so resultaat = selectielijst("/resultaten?pageSize=1")["results"][0]
# both klassen validate against the zaaktype's selectielijstProcestype.
procestype = selectielijst("/resultaten?pageSize=1")["results"][0]["procesType"]
resultaten = selectielijst(f"/resultaten?procesType={urllib.parse.quote(procestype, safe='')}")["results"]
if len(resultaten) < len(wanted):
sys.exit(f"selectielijst procestype has too few resultaten ({len(resultaten)}) for {len(wanted)} resultaattypen")
omschrijvingen = selectielijst("/resultaattypeomschrijvingen") omschrijvingen = selectielijst("/resultaattypeomschrijvingen")
oms_list = omschrijvingen if isinstance(omschrijvingen, list) else omschrijvingen["results"] oms = (omschrijvingen if isinstance(omschrijvingen, list) else omschrijvingen["results"])[0]["url"]
# The selectielijstklasse and the zaaktype must share a procestype.
st, body = api("PATCH", zt["url"], {"selectielijstProcestype": procestype}) st, body = api("PATCH", zt["url"], {"selectielijstProcestype": resultaat["procesType"]})
if st != 200: if st != 200:
sys.exit(f"set procestype -> {st}: {json.dumps(body, indent=2)}") sys.exit(f"set procestype -> {st}: {json.dumps(body, indent=2)}")
for i, (naam, archiefnominatie) in enumerate(wanted):
if naam in have_rt:
print(f"skip resultaattype {naam}")
continue
st, body = api("POST", "/resultaattypen", { st, body = api("POST", "/resultaattypen", {
"zaaktype": zt["url"], "omschrijving": naam, "zaaktype": zt["url"], "omschrijving": "Geregistreerd",
"resultaattypeomschrijving": oms_list[i]["url"], "selectielijstklasse": resultaten[i]["url"], "resultaattypeomschrijving": oms, "selectielijstklasse": resultaat["url"],
"archiefnominatie": archiefnominatie, "archiefnominatie": "blijvend_bewaren",
"brondatumArchiefprocedure": {"afleidingswijze": "afgehandeld"}, "brondatumArchiefprocedure": {"afleidingswijze": "afgehandeld"},
}) })
if st != 201: if st != 201:
sys.exit(f"create resultaattype {naam} -> {st}: {json.dumps(body, indent=2)}") sys.exit(f"create resultaattype -> {st}: {json.dumps(body, indent=2)}")
print(f"create resultaattype {naam}") print("create resultaattype Geregistreerd")
if zt.get("concept", True): if zt.get("concept", True):
st, body = api("POST", f"{zt['url']}/publish") st, body = api("POST", f"{zt['url']}/publish")
+4 -48
View File
@@ -309,11 +309,10 @@ done
[ -n "$escalated" ] || { echo "FAIL — Beoordelen task not reassigned to teamlead (candidate groups: '$groups')" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; } [ -n "$escalated" ] || { echo "FAIL — Beoordelen task not reassigned to teamlead (candidate groups: '$groups')" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; }
echo "OK — the 14-day timer escalated the still-open Beoordelen task to the teamlead" echo "OK — the 14-day timer escalated the still-open Beoordelen task to the teamlead"
# ── S-10a/S-10c: document timeout. A registration parks at WachtOpDocumenten and — unlike every block # ── S-10a: document timeout. A registration parks at WachtOpDocumenten and — unlike every block above —
# above — its documents never arrive. We fire its 30-day boundary timer early via the management API; # its documents never arrive. We fire its 30-day boundary timer early via the management API; the
# the INTERRUPTING timer cancels the wait and routes a token to the RegistratieVerlopen external task. # INTERRUPTING timer cancels the wait and routes a token to the RegistratieVerlopen external task. The
# The domain's timeout worker acquires it, cancels the ZGW zaak via the ACL (S-10c), and expires the # domain's timeout worker acquires it and expires the registration to VERLOPEN (ADR-0017). ────────────
# registration to VERLOPEN (ADR-0017). ─────────────────────────────────────────────────────────────
echo ">> submitting a registration to let its document term lapse" echo ">> submitting a registration to let its document term lapse"
locv="$(docker run --rm --network "$net" curlimages/curl:latest \ locv="$(docker run --rm --network "$net" curlimages/curl:latest \
-fsS -D - -o /dev/null -X POST "http://$dom_ip:8080/registrations" \ -fsS -D - -o /dev/null -X POST "http://$dom_ip:8080/registrations" \
@@ -355,47 +354,4 @@ for _ in $(seq 1 30); do
done done
[ -n "$verlopen" ] || { echo "FAIL — registration $reg_idv not VERLOPEN after the document timer fired (body: $body)" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; } [ -n "$verlopen" ] || { echo "FAIL — registration $reg_idv not VERLOPEN after the document timer fired (body: $body)" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; }
echo "OK — the 30-day document timer expired the registration to VERLOPEN" echo "OK — the 30-day document timer expired the registration to VERLOPEN"
# S-10c: the worker cancels the ZGW zaak (ACL-first, before it expires the aggregate), so a VERLOPEN
# registration must carry a zaak whose current status is "Geannuleerd". Read it back from OpenZaak with
# a ZGW token minted like the seed's client (the same client OpenZaak trusts for this stack).
zaak_url_v="$(printf '%s' "$body" | grep -oiE 'http://[^"]*/zaken/api/v1/zaken/[a-f0-9-]+' | head -1)"
[ -n "$zaak_url_v" ] || { echo "FAIL — VERLOPEN registration $reg_idv exposes no zaak URL (body: $body)" >&2; exit 1; }
echo ">> confirming the zaak $zaak_url_v reached the Geannuleerd status in OpenZaak"
read_zaak_status() {
# -i so the heredoc reaches `python -` on the container's stdin (without it the script is empty).
docker run --rm -i --network "$net" \
-e OZ_CLIENT_ID="${OZ_CLIENT_ID:-big-reference-seed}" \
-e OZ_SECRET="${OZ_SECRET:-insecure-dev-secret-change-me}" \
python:3-slim python - "$1" <<'PY'
import base64, hashlib, hmac, json, os, sys, time, urllib.request
cid, sec = os.environ["OZ_CLIENT_ID"], os.environ["OZ_SECRET"]
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
def token():
hdr = {"alg": "HS256", "typ": "JWT"}
pl = {"iss": cid, "iat": int(time.time()), "client_id": cid, "user_id": "verify", "user_representation": "verify"}
seg = b64(json.dumps(hdr, separators=(",", ":")).encode()) + b"." + b64(json.dumps(pl, separators=(",", ":")).encode())
return (seg + b"." + b64(hmac.new(sec.encode(), seg, hashlib.sha256).digest())).decode()
def get(url):
req = urllib.request.Request(url, headers={
"Authorization": "Bearer " + token(), "Accept": "application/json", "Accept-Crs": "EPSG:4326"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
zaak = get(sys.argv[1])
status_url = zaak.get("status")
if not status_url:
print(""); sys.exit(0)
print(get(get(status_url)["statustype"]).get("omschrijving", ""))
PY
}
geannuleerd=""
for _ in $(seq 1 15); do
oms="$(read_zaak_status "$zaak_url_v" 2>/dev/null | tr -d '\r' || true)"
[ "$oms" = "Geannuleerd" ] && { geannuleerd=1; break; }
sleep 2
done
[ -n "$geannuleerd" ] || { echo "FAIL — zaak $zaak_url_v not Geannuleerd after timeout (current status omschrijving: '$oms')" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; }
echo "OK — the timed-out registration's zaak was cancelled to Geannuleerd in OpenZaak"
exit 0 exit 0
-10
View File
@@ -32,14 +32,6 @@ app.MapPost("/statussen", async (SetStatusRequest body, AclService acl, Cancella
return Results.NoContent(); return Results.NoContent();
}); });
// Cancel a zaak on document-timeout expiry (S-10c): set it to its zaaktype's cancellation statustype
// + resultaat. The domain hands over only the zaak URL; the ACL owns the ZGW resolution (§8.1).
app.MapPost("/annuleringen", async (CancelZaakRequest body, AclService acl, CancellationToken ct) =>
{
await acl.CancelZaakAsync(new Uri(body.ZaakUrl), ct);
return Results.NoContent();
});
// Read a zaak's public-safe reference (its identificatie). The Event Subscriber calls this to enrich // Read a zaak's public-safe reference (its identificatie). The Event Subscriber calls this to enrich
// the read projection without reading ZGW itself (§8.1, #78). // the read projection without reading ZGW itself (§8.1, #78).
app.MapPost("/zaken/reference", async (ZaakReferenceRequest body, AclService acl, CancellationToken ct) => app.MapPost("/zaken/reference", async (ZaakReferenceRequest body, AclService acl, CancellationToken ct) =>
@@ -63,8 +55,6 @@ public sealed record OpenZaakRequest(string Bsn, string Reference);
public sealed record SetStatusRequest(string ZaakUrl); public sealed record SetStatusRequest(string ZaakUrl);
public sealed record CancelZaakRequest(string ZaakUrl);
public sealed record ZaakReferenceRequest(string ZaakUrl); public sealed record ZaakReferenceRequest(string ZaakUrl);
public sealed record StoreDocumentRequest(string ZaakUrl, string ContentBase64, string FileName, string ContentType); public sealed record StoreDocumentRequest(string ZaakUrl, string ContentBase64, string FileName, string ContentType);
@@ -30,18 +30,6 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, ICloc
return gateway.SetZaakToEindstatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct); return gateway.SetZaakToEindstatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct);
} }
/// <summary>
/// Cancel a zaak on document-timeout expiry (S-10c): set it to the configured BIG zaaktype's
/// cancellation statustype + resultaat. The domain hands over only the zaak URL; the ACL owns which
/// statustype/resultaat means "cancelled" (§8.1).
/// </summary>
public Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(zaakUrl);
return gateway.SetZaakToCancellationStatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct);
}
/// <summary>The zaak's reference (its ZGW identificatie), for the read projection (#78).</summary> /// <summary>The zaak's reference (its ZGW identificatie), for the read projection (#78).</summary>
public Task<string> GetZaakReferenceAsync(Uri zaakUrl, CancellationToken ct = default) public Task<string> GetZaakReferenceAsync(Uri zaakUrl, CancellationToken ct = default)
{ {
@@ -13,15 +13,6 @@ public interface IZaakGateway
/// </summary> /// </summary>
Task SetZaakToEindstatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default); Task SetZaakToEindstatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default);
/// <summary>
/// Set the given zaak to the <em>cancellation</em> statustype ("Geannuleerd") and record the
/// matching cancellation resultaat ("Vervallen") — the ZGW translation of "the 30-day document term
/// lapsed" (S-10c). Distinct from <see cref="SetZaakToEindstatusAsync"/> (approval): the gateway
/// resolves both the cancellation statustype and resultaattype from the catalogus by their
/// omschrijving, POSTs the resultaat then the status, dated <paramref name="datumStatusGezet"/>.
/// </summary>
Task SetZaakToCancellationStatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default);
/// <summary>Read the zaak's <c>identificatie</c> — the public-safe reference the register shows. /// <summary>Read the zaak's <c>identificatie</c> — the public-safe reference the register shows.
/// The Event Subscriber calls this through the ACL rather than reading ZGW itself (§8.1, #78).</summary> /// The Event Subscriber calls this through the ACL rather than reading ZGW itself (§8.1, #78).</summary>
Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default); Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default);
@@ -8,12 +8,6 @@ namespace Acl.Infrastructure;
/// <summary>The only code that talks to OpenZaak's Zaken API (ADR-0001).</summary> /// <summary>The only code that talks to OpenZaak's Zaken API (ADR-0001).</summary>
public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : IZaakGateway public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : IZaakGateway
{ {
// The ACL owns which ZGW statustype/resultaat carries each domain outcome (§8.1). These
// omschrijvingen match the seeded BIG catalogus (infra/openzaak/seed_catalogus.py).
private const string GeregistreerdResultaat = "Geregistreerd"; // approval outcome
private const string GeannuleerdStatus = "Geannuleerd"; // document-timeout cancellation status (S-10c)
private const string VervallenResultaat = "Vervallen"; // document-timeout cancellation outcome (S-10c)
public async Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default) public async Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
{ {
ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request);
@@ -54,9 +48,7 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
ArgumentNullException.ThrowIfNull(zaaktypeUrl); ArgumentNullException.ThrowIfNull(zaaktypeUrl);
var eindstatus = await ResolveEindstatusAsync(zaaktypeUrl, ct); var eindstatus = await ResolveEindstatusAsync(zaaktypeUrl, ct);
// Resolve the approval resultaat by name: once S-10c adds the Vervallen resultaattype, taking var resultaattype = await ResolveResultaattypeAsync(zaaktypeUrl, ct);
// the first would be ambiguous (the Zaken API does not guarantee order).
var resultaattype = await ResolveResultaattypeByOmschrijvingAsync(zaaktypeUrl, GeregistreerdResultaat, ct);
// OpenZaak refuses to set a zaak's eindstatus unless the zaak has a resultaat // OpenZaak refuses to set a zaak's eindstatus unless the zaak has a resultaat
// ("resultaat-does-not-exist"), so record the resultaat first, then the status. // ("resultaat-does-not-exist"), so record the resultaat first, then the status.
@@ -70,27 +62,6 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
"Setting the zaak status", ct); "Setting the zaak status", ct);
} }
public async Task SetZaakToCancellationStatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(zaakUrl);
ArgumentNullException.ThrowIfNull(zaaktypeUrl);
// Distinct from approval: resolve the cancellation statustype + resultaat by name (Geannuleerd
// is a non-terminal statustype, so it is never the eindstatus the approval path resolves).
var cancellationStatus = await ResolveStatustypeByOmschrijvingAsync(zaaktypeUrl, GeannuleerdStatus, ct);
var cancellationResultaat = await ResolveResultaattypeByOmschrijvingAsync(zaaktypeUrl, VervallenResultaat, ct);
// As with approval, OpenZaak wants the resultaat recorded before the status.
await PostAsync("/zaken/api/v1/resultaten",
new ResultaatDto(zaakUrl.ToString(), cancellationResultaat.ToString()),
"Setting the zaak cancellation resultaat", ct);
await PostAsync("/zaken/api/v1/statussen",
new StatusDto(zaakUrl.ToString(), cancellationStatus.ToString(),
datumStatusGezet.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc).ToString("yyyy-MM-ddTHH:mm:ssZ")),
"Setting the zaak cancellation status", ct);
}
public async Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default) public async Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default)
{ {
ArgumentNullException.ThrowIfNull(zaakUrl); ArgumentNullException.ThrowIfNull(zaakUrl);
@@ -203,23 +174,13 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
return new Uri(eindstatus.Url); return new Uri(eindstatus.Url);
} }
/// <summary>Resolve a specific statustype from the catalogus by its omschrijving (e.g. "Geannuleerd").</summary> /// <summary>Resolve the zaaktype's resultaattype from the catalogus (the seed defines one).</summary>
private async Task<Uri> ResolveStatustypeByOmschrijvingAsync(Uri zaaktypeUrl, string omschrijving, CancellationToken ct) private async Task<Uri> ResolveResultaattypeAsync(Uri zaaktypeUrl, CancellationToken ct)
{
var page = await GetCatalogusAsync<StatustypePage>("statustypen", zaaktypeUrl, "statustypen", ct);
var match = (page.Results ?? []).FirstOrDefault(s => s.Omschrijving == omschrijving)
?? throw new InvalidOperationException($"No '{omschrijving}' statustype found for zaaktype {zaaktypeUrl}");
return new Uri(match.Url);
}
/// <summary>Resolve a specific resultaattype from the catalogus by its omschrijving (the seed defines
/// "Geregistreerd" for approval and "Vervallen" for a document-timeout cancellation).</summary>
private async Task<Uri> ResolveResultaattypeByOmschrijvingAsync(Uri zaaktypeUrl, string omschrijving, CancellationToken ct)
{ {
var page = await GetCatalogusAsync<ResultaattypePage>("resultaattypen", zaaktypeUrl, "resultaattypen", ct); var page = await GetCatalogusAsync<ResultaattypePage>("resultaattypen", zaaktypeUrl, "resultaattypen", ct);
var match = (page.Results ?? []).FirstOrDefault(r => r.Omschrijving == omschrijving) var resultaattype = (page.Results ?? []).FirstOrDefault()
?? throw new InvalidOperationException($"No '{omschrijving}' resultaattype found for zaaktype {zaaktypeUrl}"); ?? throw new InvalidOperationException($"No resultaattypen found for zaaktype {zaaktypeUrl}");
return new Uri(match.Url); return new Uri(resultaattype.Url);
} }
// GETs a catalogus collection filtered by zaaktype (status=alles includes concept + published). // GETs a catalogus collection filtered by zaaktype (status=alles includes concept + published).
@@ -263,8 +224,7 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
private sealed record StatustypeDto( private sealed record StatustypeDto(
[property: JsonPropertyName("url")] string Url, [property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("volgnummer")] int Volgnummer, [property: JsonPropertyName("volgnummer")] int Volgnummer,
[property: JsonPropertyName("isEindstatus")] bool IsEindstatus, [property: JsonPropertyName("isEindstatus")] bool IsEindstatus);
[property: JsonPropertyName("omschrijving")] string? Omschrijving);
private sealed record ResultaatDto( private sealed record ResultaatDto(
[property: JsonPropertyName("zaak")] string Zaak, [property: JsonPropertyName("zaak")] string Zaak,
@@ -274,8 +234,7 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
[property: JsonPropertyName("results")] IReadOnlyList<ResultaattypeDto>? Results); [property: JsonPropertyName("results")] IReadOnlyList<ResultaattypeDto>? Results);
private sealed record ResultaattypeDto( private sealed record ResultaattypeDto(
[property: JsonPropertyName("url")] string Url, [property: JsonPropertyName("url")] string Url);
[property: JsonPropertyName("omschrijving")] string? Omschrijving);
private sealed record CreatedDto( private sealed record CreatedDto(
[property: JsonPropertyName("url")] string Url); [property: JsonPropertyName("url")] string Url);
@@ -114,19 +114,6 @@ public sealed class OpenZaakFixture : IDisposable
return fallback ?? throw new InvalidOperationException($"No statustypen for zaaktype {zaaktypeUrl}"); return fallback ?? throw new InvalidOperationException($"No statustypen for zaaktype {zaaktypeUrl}");
} }
/// <summary>Resolve a statustype by its omschrijving (e.g. the S-10c "Geannuleerd" cancellation status).</summary>
public async Task<Uri> FindStatustypeByOmschrijvingAsync(Uri zaaktypeUrl, string omschrijving, CancellationToken ct = default)
{
var query = new Uri(BaseUrl,
"/catalogi/api/v1/statustypen?status=alles&zaaktype=" + Uri.EscapeDataString(zaaktypeUrl.ToString()));
var page = await GetJsonAsync(query, ct);
foreach (var st in page.GetProperty("results").EnumerateArray())
if (st.TryGetProperty("omschrijving", out var o) && o.GetString() == omschrijving)
return new Uri(st.GetProperty("url").GetString()!);
throw new InvalidOperationException($"No '{omschrijving}' statustype for zaaktype {zaaktypeUrl}");
}
// A ZGW (vng-api-common) HS256 JWT, mirroring the seed's client. Minted here // A ZGW (vng-api-common) HS256 JWT, mirroring the seed's client. Minted here
// rather than reusing Acl.Infrastructure's internal minter to keep that internal. // rather than reusing Acl.Infrastructure's internal minter to keep that internal.
private string MintToken() private string MintToken()
@@ -75,39 +75,6 @@ public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack)
Assert.Equal(eindstatustype.ToString(), status.GetProperty("statustype").GetString()); Assert.Equal(eindstatustype.ToString(), status.GetProperty("statustype").GetString());
} }
[Fact]
public async Task Cancelling_a_zaak_records_the_geannuleerd_status_and_a_resultaat()
{
var zaaktype = await stack.FindPublishedBigZaaktypeAsync();
Assert.True(zaaktype is not null,
"No published BIG-REGISTRATIE zaaktype found in OpenZaak — bring the stack up and " +
"seed it with OZ_PUBLISH=1 (`make integration` does this).");
var gateway = new OpenZaakGateway(stack.Http, stack.Options);
var zaakUrl = await gateway.OpenZaakAsync(new ZaakRequest(
Bronorganisatie: "517439943",
VerantwoordelijkeOrganisatie: "517439943",
Vertrouwelijkheidaanduiding: "openbaar",
Zaaktype: zaaktype!,
Startdatum: DateOnly.FromDateTime(DateTime.UtcNow),
Identificatie: Guid.NewGuid().ToString()));
await gateway.SetZaakToCancellationStatusAsync(zaakUrl, zaaktype!, DateOnly.FromDateTime(DateTime.UtcNow));
// The zaak's current status is the Geannuleerd statustype — distinct from the approval eindstatus.
var zaak = await stack.GetZaakAsync(zaakUrl);
var statusUrl = zaak.GetProperty("status").GetString();
Assert.False(string.IsNullOrEmpty(statusUrl), "the cancelled zaak has no current status");
var status = await stack.GetJsonAsync(new Uri(statusUrl!));
var geannuleerd = await stack.FindStatustypeByOmschrijvingAsync(zaaktype!, "Geannuleerd");
Assert.Equal(geannuleerd.ToString(), status.GetProperty("statustype").GetString());
// ...and a resultaat is recorded (OpenZaak requires it before a closing/terminal status).
Assert.False(string.IsNullOrEmpty(zaak.GetProperty("resultaat").GetString()),
"the cancelled zaak has no resultaat");
}
[Fact] [Fact]
public async Task Storing_a_diploma_creates_a_real_informatieobject_related_to_the_zaak() public async Task Storing_a_diploma_creates_a_real_informatieobject_related_to_the_zaak()
{ {
-36
View File
@@ -23,14 +23,6 @@ public class AclServiceTests
return Task.CompletedTask; return Task.CompletedTask;
} }
public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Cancelled;
public Task SetZaakToCancellationStatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default)
{
Cancelled = (zaakUrl, zaaktypeUrl, datumStatusGezet);
return Task.CompletedTask;
}
public Uri? ReadReferenceFor; public Uri? ReadReferenceFor;
public Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default) public Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default)
@@ -134,34 +126,6 @@ public class AclServiceTests
Assert.Null(gateway.Approved); Assert.Null(gateway.Approved);
} }
[Fact]
public async Task Cancelling_a_zaak_sets_it_to_the_cancellation_status_dated_today()
{
var gateway = new FakeGateway();
var defaults = Defaults();
var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
await service.CancelZaakAsync(zaak);
Assert.NotNull(gateway.Cancelled);
Assert.Equal(zaak, gateway.Cancelled!.Value.Zaak);
Assert.Equal(defaults.ZaaktypeUrl, gateway.Cancelled.Value.Zaaktype);
Assert.Equal(new DateOnly(2026, 6, 4), gateway.Cancelled.Value.Datum);
// Cancellation must not touch the approval path.
Assert.Null(gateway.Approved);
}
[Fact]
public async Task Cancelling_a_null_zaak_is_rejected_without_touching_the_gateway()
{
var gateway = new FakeGateway();
var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4)));
await Assert.ThrowsAsync<ArgumentNullException>(() => service.CancelZaakAsync(null!));
Assert.Null(gateway.Cancelled);
}
[Fact] [Fact]
public async Task Storing_a_diploma_default_fills_the_document_fields_and_returns_its_url() public async Task Storing_a_diploma_default_fills_the_document_fields_and_returns_its_url()
{ {
+2 -135
View File
@@ -173,8 +173,7 @@ public class OpenZaakGatewayTests
private sealed class OzRoutes private sealed class OzRoutes
{ {
public string StatustypenJson { get; init; } = StatustypenPage(withEindstatusFlag: true); public string StatustypenJson { get; init; } = StatustypenPage(withEindstatusFlag: true);
public string ResultaattypenJson { get; init; } = public string ResultaattypenJson { get; init; } = """{"results":[{"url":"http://openzaak/catalogi/api/v1/resultaattypen/1"}]}""";
"""{"results":[{"url":"http://openzaak/catalogi/api/v1/resultaattypen/1","omschrijving":"Geregistreerd"}]}""";
public HttpStatusCode StatustypenStatus { get; init; } = HttpStatusCode.OK; public HttpStatusCode StatustypenStatus { get; init; } = HttpStatusCode.OK;
public HttpStatusCode ResultaattypenStatus { get; init; } = HttpStatusCode.OK; public HttpStatusCode ResultaattypenStatus { get; init; } = HttpStatusCode.OK;
public HttpStatusCode ResultaatPostStatus { get; init; } = HttpStatusCode.Created; public HttpStatusCode ResultaatPostStatus { get; init; } = HttpStatusCode.Created;
@@ -252,138 +251,6 @@ public class OpenZaakGatewayTests
Assert.True(status.Length > 0); Assert.True(status.Length > 0);
} }
[Fact]
public async Task Approving_selects_the_geregistreerd_resultaat_by_name_when_several_exist()
{
// Once S-10c adds a second resultaattype (Vervallen), picking the first is ambiguous — the
// Zaken API does not guarantee order. Approval must resolve its resultaat by omschrijving.
var rec = new Recorder();
var twoResultaattypen = """
{"results":[
{"url":"http://openzaak/catalogi/api/v1/resultaattypen/vervallen","omschrijving":"Vervallen"},
{"url":"http://openzaak/catalogi/api/v1/resultaattypen/geregistreerd","omschrijving":"Geregistreerd"}
]}
""";
await Gateway(ApprovalStub(rec, new OzRoutes { ResultaattypenJson = twoResultaattypen }))
.SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4));
Assert.Contains("\"resultaattype\":\"http://openzaak/catalogi/api/v1/resultaattypen/geregistreerd\"",
rec.Sent("/resultaten").Body);
}
// --- SetZaakToCancellationStatusAsync (document-timeout cancellation / S-10c) ---
// A catalogus with the three statustypen S-10c seeds (Geannuleerd is non-terminal, below the
// Afgehandeld eindstatus) and both resultaattypen. Cancellation must resolve "Geannuleerd" and
// "Vervallen" by omschrijving, never the approval pair.
private const string CancellationStatustypenJson = """
{"results":[
{"url":"http://openzaak/catalogi/api/v1/statustypen/ontvangen","volgnummer":1,"omschrijving":"Ontvangen","isEindstatus":false},
{"url":"http://openzaak/catalogi/api/v1/statustypen/geannuleerd","volgnummer":2,"omschrijving":"Geannuleerd","isEindstatus":false},
{"url":"http://openzaak/catalogi/api/v1/statustypen/afgehandeld","volgnummer":3,"omschrijving":"Afgehandeld","isEindstatus":true}
]}
""";
private const string CancellationResultaattypenJson = """
{"results":[
{"url":"http://openzaak/catalogi/api/v1/resultaattypen/geregistreerd","omschrijving":"Geregistreerd"},
{"url":"http://openzaak/catalogi/api/v1/resultaattypen/vervallen","omschrijving":"Vervallen"}
]}
""";
[Fact]
public async Task Cancelling_records_the_vervallen_resultaat_then_the_geannuleerd_status_against_the_zaak()
{
var rec = new Recorder();
await Gateway(ApprovalStub(rec, new OzRoutes
{
StatustypenJson = CancellationStatustypenJson,
ResultaattypenJson = CancellationResultaattypenJson,
})).SetZaakToCancellationStatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4));
// Resultaat precedes status (OpenZaak requires a resultaat before a closing/terminal status).
Assert.True(rec.IndexOf("/resultaten") < rec.IndexOf("/statussen"));
var resultaat = rec.Sent("/resultaten");
Assert.Contains("\"zaak\":\"" + ZaakUrl + "\"", resultaat.Body);
// The cancellation resultaat (Vervallen) is chosen by name — not the approval one (Geregistreerd).
Assert.Contains("\"resultaattype\":\"http://openzaak/catalogi/api/v1/resultaattypen/vervallen\"", resultaat.Body);
var status = rec.Sent("/statussen");
Assert.Contains("\"zaak\":\"" + ZaakUrl + "\"", status.Body);
// The Geannuleerd statustype is chosen by name — not the Afgehandeld eindstatus (approval).
Assert.Contains("\"statustype\":\"http://openzaak/catalogi/api/v1/statustypen/geannuleerd\"", status.Body);
Assert.Contains("\"datumStatusGezet\":\"2026-06-04T00:00:00Z\"", status.Body);
}
[Fact]
public async Task Cancelling_throws_when_the_zaaktype_has_no_geannuleerd_statustype()
{
var rec = new Recorder();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
Gateway(ApprovalStub(rec, new OzRoutes
{
// Only the approval statustypen — no "Geannuleerd".
StatustypenJson = StatustypenPage(withEindstatusFlag: true),
ResultaattypenJson = CancellationResultaattypenJson,
})).SetZaakToCancellationStatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4)));
Assert.Contains("Geannuleerd", ex.Message);
}
[Fact]
public async Task Cancelling_rejects_a_null_zaak_without_calling_openzaak()
{
var handler = new StubHandler(_ => throw new InvalidOperationException("should not be sent"));
await Assert.ThrowsAsync<ArgumentNullException>(() =>
Gateway(handler).SetZaakToCancellationStatusAsync(null!, Zaaktype, new DateOnly(2026, 6, 4)));
}
[Fact]
public async Task Cancelling_rejects_a_null_zaaktype_without_calling_openzaak()
{
var handler = new StubHandler(_ => throw new InvalidOperationException("should not be sent"));
await Assert.ThrowsAsync<ArgumentNullException>(() =>
Gateway(handler).SetZaakToCancellationStatusAsync(new Uri(ZaakUrl), null!, new DateOnly(2026, 6, 4)));
}
[Fact]
public async Task Cancelling_surfaces_the_failure_when_recording_the_resultaat_is_rejected()
{
var rec = new Recorder();
var ex = await Assert.ThrowsAsync<HttpRequestException>(() =>
Gateway(ApprovalStub(rec, new OzRoutes
{
StatustypenJson = CancellationStatustypenJson,
ResultaattypenJson = CancellationResultaattypenJson,
ResultaatPostStatus = HttpStatusCode.BadRequest,
})).SetZaakToCancellationStatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4)));
Assert.Contains("cancellation resultaat", ex.Message);
// It fails on the resultaat, before it ever posts the status.
Assert.Equal(-1, rec.IndexOf("/statussen"));
}
[Fact]
public async Task Cancelling_surfaces_the_failure_when_recording_the_status_is_rejected()
{
var rec = new Recorder();
var ex = await Assert.ThrowsAsync<HttpRequestException>(() =>
Gateway(ApprovalStub(rec, new OzRoutes
{
StatustypenJson = CancellationStatustypenJson,
ResultaattypenJson = CancellationResultaattypenJson,
StatusPostStatus = HttpStatusCode.BadRequest,
})).SetZaakToCancellationStatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4)));
Assert.Contains("cancellation status", ex.Message);
}
[Fact] [Fact]
public async Task Approving_falls_back_to_the_highest_volgnummer_when_no_eindstatus_is_flagged() public async Task Approving_falls_back_to_the_highest_volgnummer_when_no_eindstatus_is_flagged()
{ {
@@ -458,7 +325,7 @@ public class OpenZaakGatewayTests
Gateway(ApprovalStub(rec, new OzRoutes { ResultaattypenJson = "{}" })) Gateway(ApprovalStub(rec, new OzRoutes { ResultaattypenJson = "{}" }))
.SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4))); .SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4)));
Assert.Contains("'Geregistreerd' resultaattype", ex.Message); Assert.Contains("No resultaattypen found", ex.Message);
// Resolved the eindstatus + queried resultaattypen, but posted nothing. // Resolved the eindstatus + queried resultaattypen, but posted nothing.
Assert.Equal(-1, rec.IndexOf("/resultaten")); Assert.Equal(-1, rec.IndexOf("/resultaten"));
Assert.Equal(-1, rec.IndexOf("/statussen")); Assert.Equal(-1, rec.IndexOf("/statussen"));
@@ -9,7 +9,7 @@ namespace Big.Application;
/// nothing of Flowable. The polling loop that feeds it jobs lives in Infrastructure. Mirrors /// nothing of Flowable. The polling loop that feeds it jobs lives in Infrastructure. Mirrors
/// <see cref="OpenZaakWorker"/>. /// <see cref="OpenZaakWorker"/>.
/// </summary> /// </summary>
public sealed class ExpireRegistrationWorker(IRegistrationStore store, IAclClient acl) public sealed class ExpireRegistrationWorker(IRegistrationStore store)
{ {
/// <summary> /// <summary>
/// Process the job. Idempotent and tolerant of races (§8.6, at-least-once delivery): a job whose /// Process the job. Idempotent and tolerant of races (§8.6, at-least-once delivery): a job whose
@@ -31,14 +31,6 @@ public sealed class ExpireRegistrationWorker(IRegistrationStore store, IAclClien
if (registration.Status is not (RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling)) if (registration.Status is not (RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling))
return; return;
// Cancel the ZGW zaak before advancing the aggregate (mirrors the approval path): if the ACL
// call fails it throws, the aggregate stays open, and the job is redelivered (§8.6) — rather
// than leaving the aggregate VERLOPEN while the zaak stays open. The status guard above stops a
// redelivered job from cancelling the zaak twice (a second resultaat would be a 400). A
// registration expired before its zaak was opened has nothing to cancel.
if (registration.ZaakUrl is not null)
await acl.CancelZaakAsync(registration.ZaakUrl, ct);
registration.Expire(); registration.Expire();
await store.SaveAsync(registration, ct); await store.SaveAsync(registration, ct);
} }
-6
View File
@@ -59,12 +59,6 @@ public interface IAclClient
/// zaak (§8.1). Returns the stored document's URL. /// zaak (§8.1). Returns the stored document's URL.
/// </summary> /// </summary>
Task<Uri> StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default); Task<Uri> StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default);
/// <summary>
/// Cancel the zaak on document-timeout expiry (S-10c): the 30-day document term lapsed, so the ACL
/// translates this to the ZGW cancellation status/resultaat. The domain never names statustypen.
/// </summary>
Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default);
} }
/// <summary> /// <summary>
@@ -31,15 +31,6 @@ public sealed class AclHttpClient(HttpClient http, AclOptions options) : IAclCli
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
} }
public async Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(zaakUrl);
using var response = await http.PostAsJsonAsync(
new Uri(options.BaseUrl, "annuleringen"), new CancelZaakRequest(zaakUrl.ToString()), ct);
response.EnsureSuccessStatusCode();
}
public async Task<Uri> StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default) public async Task<Uri> StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default)
{ {
ArgumentNullException.ThrowIfNull(zaakUrl); ArgumentNullException.ThrowIfNull(zaakUrl);
@@ -65,8 +56,6 @@ public sealed class AclHttpClient(HttpClient http, AclOptions options) : IAclCli
private sealed record SetStatusRequest([property: JsonPropertyName("zaakUrl")] string ZaakUrl); private sealed record SetStatusRequest([property: JsonPropertyName("zaakUrl")] string ZaakUrl);
private sealed record CancelZaakRequest([property: JsonPropertyName("zaakUrl")] string ZaakUrl);
private sealed record StoreDocumentRequest( private sealed record StoreDocumentRequest(
[property: JsonPropertyName("zaakUrl")] string ZaakUrl, [property: JsonPropertyName("zaakUrl")] string ZaakUrl,
[property: JsonPropertyName("contentBase64")] string ContentBase64, [property: JsonPropertyName("contentBase64")] string ContentBase64,
@@ -10,13 +10,10 @@ public class ExpireRegistrationWorkerTests
{ {
private const string Bsn = "123456782"; private const string Bsn = "123456782";
// By the time the 30-day document timer fires, the zaak was opened long ago (OpenZaakAanmaken runs private static Registration Submitted(string processInstanceId = "proc-1")
// early in the flow), so a timed-out registration carries a zaak the worker can cancel.
private static Registration Submitted(string processInstanceId = "proc-1", Uri? zaakUrl = null)
{ {
var registration = Registration.Submit(Bsn); var registration = Registration.Submit(Bsn);
registration.RecordProcessStarted(processInstanceId); registration.RecordProcessStarted(processInstanceId);
registration.AttachZaak(zaakUrl ?? FakeAclClient.DefaultZaakUrl);
return registration; return registration;
} }
@@ -27,7 +24,7 @@ public class ExpireRegistrationWorkerTests
var registration = Submitted(); var registration = Submitted();
store.Seed(registration); store.Seed(registration);
await new ExpireRegistrationWorker(store, new FakeAclClient()).HandleAsync( await new ExpireRegistrationWorker(store).HandleAsync(
new RegistratieVerlopenJob("job-7", registration.Id)); new RegistratieVerlopenJob("job-7", registration.Id));
var saved = await store.GetAsync(registration.Id); var saved = await store.GetAsync(registration.Id);
@@ -36,60 +33,37 @@ public class ExpireRegistrationWorkerTests
} }
[Fact] [Fact]
public async Task Cancels_the_zaak_via_the_acl_when_expiring_a_still_open_registration() public async Task An_already_verlopen_registration_is_not_persisted_again()
{ {
// S-10c: expiring the aggregate is not enough — the ZGW zaak must also be set to its // A redelivered job (§8.6) finds the aggregate already VERLOPEN: a no-op, not saved again.
// cancellation status, which the ACL owns (§8.1). The worker hands the ACL the zaak URL.
var store = new FakeRegistrationStore();
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/timed-out");
var registration = Submitted(zaakUrl: zaak);
store.Seed(registration);
var acl = new FakeAclClient();
await new ExpireRegistrationWorker(store, acl).HandleAsync(
new RegistratieVerlopenJob("job-7", registration.Id));
Assert.Equal(1, acl.CancelCallCount);
Assert.Equal(zaak, acl.CancelledZaakUrl);
}
[Fact]
public async Task An_already_verlopen_registration_is_not_persisted_again_and_the_zaak_is_not_recancelled()
{
// A redelivered job (§8.6) finds the aggregate already VERLOPEN: a no-op, not saved again — and
// the ACL is not asked to cancel the zaak a second time (posting a second resultaat would 400).
var store = new FakeRegistrationStore(); var store = new FakeRegistrationStore();
var registration = Submitted(); var registration = Submitted();
registration.Expire(); registration.Expire();
store.Seed(registration); store.Seed(registration);
var acl = new FakeAclClient();
await new ExpireRegistrationWorker(store, acl).HandleAsync( await new ExpireRegistrationWorker(store).HandleAsync(
new RegistratieVerlopenJob("job-7", registration.Id)); new RegistratieVerlopenJob("job-7", registration.Id));
Assert.Equal(0, store.SaveCount); Assert.Equal(0, store.SaveCount);
Assert.Equal(0, acl.CancelCallCount);
Assert.Equal(RegistrationStatus.Verlopen, (await store.GetAsync(registration.Id))!.Status); Assert.Equal(RegistrationStatus.Verlopen, (await store.GetAsync(registration.Id))!.Status);
} }
[Fact] [Fact]
public async Task An_already_resolved_registration_is_left_alone_and_the_zaak_is_not_cancelled() public async Task An_already_resolved_registration_is_left_alone_and_the_job_completes()
{ {
// Race with S-11: the citizen withdrew while parked at WachtOpDocumenten, so the aggregate is // Race with S-11: the citizen withdrew while parked at WachtOpDocumenten, so the aggregate is
// already terminal (INGETROKKEN) when the timer's job arrives. Expiring it would violate the // already terminal (INGETROKKEN) when the timer's job arrives. Expiring it would violate the
// aggregate's invariant; the worker must instead no-op (and let the job complete), not throw // aggregate's invariant; the worker must instead no-op (and let the job complete), not throw
// into a redelivery loop — and it must not cancel the zaak of a registration it didn't expire. // into a redelivery loop.
var store = new FakeRegistrationStore(); var store = new FakeRegistrationStore();
var registration = Submitted(); var registration = Submitted();
registration.Withdraw(); registration.Withdraw();
store.Seed(registration); store.Seed(registration);
var acl = new FakeAclClient();
await new ExpireRegistrationWorker(store, acl).HandleAsync( await new ExpireRegistrationWorker(store).HandleAsync(
new RegistratieVerlopenJob("job-7", registration.Id)); new RegistratieVerlopenJob("job-7", registration.Id));
Assert.Equal(0, store.SaveCount); Assert.Equal(0, store.SaveCount);
Assert.Equal(0, acl.CancelCallCount);
Assert.Equal(RegistrationStatus.Ingetrokken, (await store.GetAsync(registration.Id))!.Status); Assert.Equal(RegistrationStatus.Ingetrokken, (await store.GetAsync(registration.Id))!.Status);
} }
@@ -99,12 +73,12 @@ public class ExpireRegistrationWorkerTests
var store = new FakeRegistrationStore(); var store = new FakeRegistrationStore();
await Assert.ThrowsAsync<InvalidOperationException>(() => await Assert.ThrowsAsync<InvalidOperationException>(() =>
new ExpireRegistrationWorker(store, new FakeAclClient()).HandleAsync( new ExpireRegistrationWorker(store).HandleAsync(
new RegistratieVerlopenJob("job-7", RegistrationId.New()))); new RegistratieVerlopenJob("job-7", RegistrationId.New())));
} }
[Fact] [Fact]
public async Task Rejects_a_null_job() public async Task Rejects_a_null_job()
=> await Assert.ThrowsAsync<ArgumentNullException>(() => => await Assert.ThrowsAsync<ArgumentNullException>(() =>
new ExpireRegistrationWorker(new FakeRegistrationStore(), new FakeAclClient()).HandleAsync(null!)); new ExpireRegistrationWorker(new FakeRegistrationStore()).HandleAsync(null!));
} }
-10
View File
@@ -119,14 +119,4 @@ internal sealed class FakeAclClient(Uri? zaakUrl = null) : IAclClient
StoredDiploma = (zaakUrl, content, fileName, contentType); StoredDiploma = (zaakUrl, content, fileName, contentType);
return Task.FromResult(DefaultDocumentUrl); return Task.FromResult(DefaultDocumentUrl);
} }
public Uri? CancelledZaakUrl { get; private set; }
public int CancelCallCount { get; private set; }
public Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default)
{
CancelCallCount++;
CancelledZaakUrl = zaakUrl;
return Task.CompletedTask;
}
} }
@@ -30,7 +30,7 @@ public class RegistratieVerlopenProcessorTests
} }
} }
private static ExpireRegistrationWorker Worker(FakeRegistrationStore store) => new(store, new FakeAclClient()); private static ExpireRegistrationWorker Worker(FakeRegistrationStore store) => new(store);
[Fact] [Fact]
public async Task Acquires_a_job_expires_the_registration_and_completes_the_job() public async Task Acquires_a_job_expires_the_registration_and_completes_the_job()
@@ -14,7 +14,6 @@ Feature: Een documenttermijn laten verlopen
When the 30-day document timer fires When the 30-day document timer fires
And the document-timeout worker runs And the document-timeout worker runs
Then the registration is verlopen Then the registration is verlopen
And the zaak is cancelled in ZGW
Scenario: Tijdig aangeleverde documenten laten de registratie niet vervallen Scenario: Tijdig aangeleverde documenten laten de registratie niet vervallen
Given a registration parked at the WachtOpDocumenten task Given a registration parked at the WachtOpDocumenten task
@@ -22,4 +21,3 @@ Feature: Een documenttermijn laten verlopen
And the 30-day document timer fires And the 30-day document timer fires
And the document-timeout worker runs And the document-timeout worker runs
Then the registration is not verlopen Then the registration is not verlopen
And the zaak is not cancelled in ZGW
@@ -17,11 +17,8 @@ namespace Acceptance.Steps;
[Scope(Feature = "Een documenttermijn laten verlopen")] [Scope(Feature = "Een documenttermijn laten verlopen")]
public sealed class EenDocumentTermijnVerlopenSteps public sealed class EenDocumentTermijnVerlopenSteps
{ {
private static readonly Uri ZaakUrl = new("http://openzaak/zaken/api/v1/zaken/acc-timeout");
private readonly InMemoryDocumentTimeoutClient _flowable = new(); private readonly InMemoryDocumentTimeoutClient _flowable = new();
private readonly Support.InMemoryRegistrationStore _store = new(); private readonly Support.InMemoryRegistrationStore _store = new();
private readonly InMemoryAclClient _acl = new();
private Registration _registration = null!; private Registration _registration = null!;
private string _processInstanceId = ""; private string _processInstanceId = "";
@@ -29,9 +26,6 @@ public sealed class EenDocumentTermijnVerlopenSteps
public async Task GivenARegistrationParkedAtWachtOpDocumenten() public async Task GivenARegistrationParkedAtWachtOpDocumenten()
{ {
_registration = Registration.Submit("123456782"); _registration = Registration.Submit("123456782");
// By the time it parks at WachtOpDocumenten its zaak has been opened (OpenZaakAanmaken runs
// earlier), so a timeout has a zaak to cancel.
_registration.AttachZaak(ZaakUrl);
await _store.SaveAsync(_registration); await _store.SaveAsync(_registration);
_processInstanceId = _flowable.ParkWaitingForDocuments(_registration.Id); _processInstanceId = _flowable.ParkWaitingForDocuments(_registration.Id);
} }
@@ -45,7 +39,7 @@ public sealed class EenDocumentTermijnVerlopenSteps
[When("the document-timeout worker runs")] [When("the document-timeout worker runs")]
public async Task WhenTheTimeoutWorkerRuns() public async Task WhenTheTimeoutWorkerRuns()
=> await new RegistratieVerlopenProcessor( => await new RegistratieVerlopenProcessor(
_flowable, new ExpireRegistrationWorker(_store, _acl), _flowable, new ExpireRegistrationWorker(_store),
NullLogger<RegistratieVerlopenProcessor>.Instance).PumpOnceAsync(5); NullLogger<RegistratieVerlopenProcessor>.Instance).PumpOnceAsync(5);
[Then("the registration is verlopen")] [Then("the registration is verlopen")]
@@ -55,10 +49,4 @@ public sealed class EenDocumentTermijnVerlopenSteps
[Then("the registration is not verlopen")] [Then("the registration is not verlopen")]
public async Task ThenTheRegistrationIsNotVerlopen() public async Task ThenTheRegistrationIsNotVerlopen()
=> Assert.Equal(RegistrationStatus.Ingediend, (await _store.GetAsync(_registration.Id))!.Status); => Assert.Equal(RegistrationStatus.Ingediend, (await _store.GetAsync(_registration.Id))!.Status);
[Then("the zaak is cancelled in ZGW")]
public void ThenTheZaakIsCancelled() => Assert.Equal(ZaakUrl, _acl.CancelledZaakUrl);
[Then("the zaak is not cancelled in ZGW")]
public void ThenTheZaakIsNotCancelled() => Assert.Null(_acl.CancelledZaakUrl);
} }
@@ -60,14 +60,6 @@ public sealed class InMemoryAclClient : IAclClient
return Task.CompletedTask; return Task.CompletedTask;
} }
public Uri? CancelledZaakUrl { get; private set; }
public Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default)
{
CancelledZaakUrl = zaakUrl;
return Task.CompletedTask;
}
public (Uri ZaakUrl, string FileName)? StoredDiploma { get; private set; } public (Uri ZaakUrl, string FileName)? StoredDiploma { get; private set; }
public Task<Uri> StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default) public Task<Uri> StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default)
@@ -12,7 +12,6 @@ public sealed class InMemoryZaakGateway : IZaakGateway
public ZaakRequest? Captured { get; private set; } public ZaakRequest? Captured { get; private set; }
public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Approved { get; private set; } public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Approved { get; private set; }
public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Cancelled { get; private set; }
public Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default) public Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
{ {
@@ -26,12 +25,6 @@ public sealed class InMemoryZaakGateway : IZaakGateway
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SetZaakToCancellationStatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default)
{
Cancelled = (zaakUrl, zaaktypeUrl, datumStatusGezet);
return Task.CompletedTask;
}
public Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default) public Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default)
=> Task.FromResult("ACC-REF-1"); => Task.FromResult("ACC-REF-1");