From 9275cfeecd216cd6144fe5b106aa9a850c48a659 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 21 Jul 2026 14:32:01 +0200 Subject: [PATCH] feat(acl): CancelZaakAsync service + POST /annuleringen endpoint (S-10c) (refs #106) The ACL exposes zaak cancellation as a service operation the domain calls on document-timeout expiry; it default-fills the zaaktype and dates the status today, translating the domain intent to the ZGW cancellation status/resultaat. Co-Authored-By: Claude Opus 4.8 (1M context) --- services/acl/Acl.Api/Program.cs | 10 ++++++++ services/acl/Acl.Application/AclService.cs | 12 ++++++++++ services/acl/Acl.Tests/AclServiceTests.cs | 28 ++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/services/acl/Acl.Api/Program.cs b/services/acl/Acl.Api/Program.cs index 9581455..1b1bf7f 100644 --- a/services/acl/Acl.Api/Program.cs +++ b/services/acl/Acl.Api/Program.cs @@ -32,6 +32,14 @@ app.MapPost("/statussen", async (SetStatusRequest body, AclService acl, Cancella 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 // the read projection without reading ZGW itself (§8.1, #78). app.MapPost("/zaken/reference", async (ZaakReferenceRequest body, AclService acl, CancellationToken ct) => @@ -55,6 +63,8 @@ public sealed record OpenZaakRequest(string Bsn, string Reference); public sealed record SetStatusRequest(string ZaakUrl); +public sealed record CancelZaakRequest(string ZaakUrl); + public sealed record ZaakReferenceRequest(string ZaakUrl); public sealed record StoreDocumentRequest(string ZaakUrl, string ContentBase64, string FileName, string ContentType); diff --git a/services/acl/Acl.Application/AclService.cs b/services/acl/Acl.Application/AclService.cs index f692041..6f00fef 100644 --- a/services/acl/Acl.Application/AclService.cs +++ b/services/acl/Acl.Application/AclService.cs @@ -30,6 +30,18 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, ICloc return gateway.SetZaakToEindstatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct); } + /// + /// 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). + /// + public Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(zaakUrl); + + return gateway.SetZaakToCancellationStatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct); + } + /// The zaak's reference (its ZGW identificatie), for the read projection (#78). public Task GetZaakReferenceAsync(Uri zaakUrl, CancellationToken ct = default) { diff --git a/services/acl/Acl.Tests/AclServiceTests.cs b/services/acl/Acl.Tests/AclServiceTests.cs index 6d0e79c..01cbcef 100644 --- a/services/acl/Acl.Tests/AclServiceTests.cs +++ b/services/acl/Acl.Tests/AclServiceTests.cs @@ -134,6 +134,34 @@ public class AclServiceTests 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(() => service.CancelZaakAsync(null!)); + Assert.Null(gateway.Cancelled); + } + [Fact] public async Task Storing_a_diploma_default_fills_the_document_fields_and_returns_its_url() {