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()
{