feat(acl): gateway cancels a zaak via Geannuleerd status + Vervallen resultaat (refs #106)
SetZaakToCancellationStatusAsync resolves the cancellation statustype and resultaat by omschrijving and posts resultaat-then-status, mirroring the approval path. Approval now resolves its Geregistreerd resultaat by name too, since a second resultaattype now exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,12 @@ 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);
|
||||||
@@ -48,7 +54,9 @@ 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);
|
||||||
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
|
// 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.
|
||||||
@@ -62,8 +70,26 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
|
|||||||
"Setting the zaak status", ct);
|
"Setting the zaak status", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SetZaakToCancellationStatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default)
|
public async Task SetZaakToCancellationStatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default)
|
||||||
=> throw new NotImplementedException();
|
{
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
@@ -177,13 +203,23 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
|
|||||||
return new Uri(eindstatus.Url);
|
return new Uri(eindstatus.Url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Resolve the zaaktype's resultaattype from the catalogus (the seed defines one).</summary>
|
/// <summary>Resolve a specific statustype from the catalogus by its omschrijving (e.g. "Geannuleerd").</summary>
|
||||||
private async Task<Uri> ResolveResultaattypeAsync(Uri zaaktypeUrl, CancellationToken ct)
|
private async Task<Uri> ResolveStatustypeByOmschrijvingAsync(Uri zaaktypeUrl, string omschrijving, 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 resultaattype = (page.Results ?? []).FirstOrDefault()
|
var match = (page.Results ?? []).FirstOrDefault(r => r.Omschrijving == omschrijving)
|
||||||
?? throw new InvalidOperationException($"No resultaattypen found for zaaktype {zaaktypeUrl}");
|
?? throw new InvalidOperationException($"No '{omschrijving}' resultaattype found for zaaktype {zaaktypeUrl}");
|
||||||
return new Uri(resultaattype.Url);
|
return new Uri(match.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).
|
||||||
@@ -227,7 +263,8 @@ 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,
|
||||||
@@ -237,7 +274,8 @@ 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);
|
||||||
|
|||||||
@@ -410,7 +410,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("No resultaattypen found", ex.Message);
|
Assert.Contains("'Geregistreerd' resultaattype", 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"));
|
||||||
|
|||||||
Reference in New Issue
Block a user