diff --git a/services/acl/Acl.Infrastructure/OpenZaakGateway.cs b/services/acl/Acl.Infrastructure/OpenZaakGateway.cs index 2dcffd4..5f33ba8 100644 --- a/services/acl/Acl.Infrastructure/OpenZaakGateway.cs +++ b/services/acl/Acl.Infrastructure/OpenZaakGateway.cs @@ -8,6 +8,12 @@ namespace Acl.Infrastructure; /// The only code that talks to OpenZaak's Zaken API (ADR-0001). 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 OpenZaakAsync(ZaakRequest request, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(request); @@ -48,7 +54,9 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : ArgumentNullException.ThrowIfNull(zaaktypeUrl); var eindstatus = await ResolveEindstatusAsync(zaaktypeUrl, ct); - var resultaattype = await ResolveResultaattypeAsync(zaaktypeUrl, ct); + // Resolve the approval resultaat by name: once S-10c adds the Vervallen resultaattype, taking + // 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 // ("resultaat-does-not-exist"), so record the resultaat first, then the status. @@ -62,8 +70,26 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : "Setting the zaak status", ct); } - public Task SetZaakToCancellationStatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default) - => throw new NotImplementedException(); + 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 GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default) { @@ -177,13 +203,23 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : return new Uri(eindstatus.Url); } - /// Resolve the zaaktype's resultaattype from the catalogus (the seed defines one). - private async Task ResolveResultaattypeAsync(Uri zaaktypeUrl, CancellationToken ct) + /// Resolve a specific statustype from the catalogus by its omschrijving (e.g. "Geannuleerd"). + private async Task ResolveStatustypeByOmschrijvingAsync(Uri zaaktypeUrl, string omschrijving, CancellationToken ct) + { + var page = await GetCatalogusAsync("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); + } + + /// Resolve a specific resultaattype from the catalogus by its omschrijving (the seed defines + /// "Geregistreerd" for approval and "Vervallen" for a document-timeout cancellation). + private async Task ResolveResultaattypeByOmschrijvingAsync(Uri zaaktypeUrl, string omschrijving, CancellationToken ct) { var page = await GetCatalogusAsync("resultaattypen", zaaktypeUrl, "resultaattypen", ct); - var resultaattype = (page.Results ?? []).FirstOrDefault() - ?? throw new InvalidOperationException($"No resultaattypen found for zaaktype {zaaktypeUrl}"); - return new Uri(resultaattype.Url); + var match = (page.Results ?? []).FirstOrDefault(r => r.Omschrijving == omschrijving) + ?? throw new InvalidOperationException($"No '{omschrijving}' resultaattype found for zaaktype {zaaktypeUrl}"); + return new Uri(match.Url); } // GETs a catalogus collection filtered by zaaktype (status=alles includes concept + published). @@ -227,7 +263,8 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : private sealed record StatustypeDto( [property: JsonPropertyName("url")] string Url, [property: JsonPropertyName("volgnummer")] int Volgnummer, - [property: JsonPropertyName("isEindstatus")] bool IsEindstatus); + [property: JsonPropertyName("isEindstatus")] bool IsEindstatus, + [property: JsonPropertyName("omschrijving")] string? Omschrijving); private sealed record ResultaatDto( [property: JsonPropertyName("zaak")] string Zaak, @@ -237,7 +274,8 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : [property: JsonPropertyName("results")] IReadOnlyList? Results); private sealed record ResultaattypeDto( - [property: JsonPropertyName("url")] string Url); + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("omschrijving")] string? Omschrijving); private sealed record CreatedDto( [property: JsonPropertyName("url")] string Url); diff --git a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs index c570fc0..a94f783 100644 --- a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs +++ b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs @@ -410,7 +410,7 @@ public class OpenZaakGatewayTests Gateway(ApprovalStub(rec, new OzRoutes { ResultaattypenJson = "{}" })) .SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4))); - Assert.Contains("No resultaattypen found", ex.Message); + Assert.Contains("'Geregistreerd' resultaattype", ex.Message); // Resolved the eindstatus + queried resultaattypen, but posted nothing. Assert.Equal(-1, rec.IndexOf("/resultaten")); Assert.Equal(-1, rec.IndexOf("/statussen"));