diff --git a/services/acl/Acl.Api/Program.cs b/services/acl/Acl.Api/Program.cs
index d601366..f220cb1 100644
--- a/services/acl/Acl.Api/Program.cs
+++ b/services/acl/Acl.Api/Program.cs
@@ -20,7 +20,7 @@ app.MapGet("/health", () => "Healthy");
// The ACL's single operation, exposed as a service endpoint.
app.MapPost("/zaken", async (OpenZaakRequest body, AclService acl, CancellationToken ct) =>
{
- var zaakUrl = await acl.OpenZaakAsync(new DomainRegistration(body.Bsn), ct);
+ var zaakUrl = await acl.OpenZaakAsync(new DomainRegistration(body.Bsn, body.Reference), ct);
return Results.Ok(new { zaakUrl = zaakUrl.ToString() });
});
@@ -32,10 +32,20 @@ app.MapPost("/statussen", async (SetStatusRequest body, AclService acl, Cancella
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) =>
+{
+ var reference = await acl.GetZaakReferenceAsync(new Uri(body.ZaakUrl), ct);
+ return Results.Ok(new { reference });
+});
+
app.Run();
-public sealed record OpenZaakRequest(string Bsn);
+public sealed record OpenZaakRequest(string Bsn, string Reference);
public sealed record SetStatusRequest(string ZaakUrl);
+public sealed record ZaakReferenceRequest(string ZaakUrl);
+
public partial class Program;
diff --git a/services/acl/Acl.Application/AclService.cs b/services/acl/Acl.Application/AclService.cs
index f9b866a..a70e7ca 100644
--- a/services/acl/Acl.Application/AclService.cs
+++ b/services/acl/Acl.Application/AclService.cs
@@ -13,7 +13,8 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, ICloc
defaults.VerantwoordelijkeOrganisatie,
defaults.Vertrouwelijkheidaanduiding,
defaults.ZaaktypeUrl,
- clock.Today);
+ clock.Today,
+ registration.Reference);
return gateway.OpenZaakAsync(request, ct);
}
@@ -28,4 +29,12 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, ICloc
return gateway.SetZaakToEindstatusAsync(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)
+ {
+ ArgumentNullException.ThrowIfNull(zaakUrl);
+
+ return gateway.GetZaakIdentificatieAsync(zaakUrl, ct);
+ }
}
diff --git a/services/acl/Acl.Application/DomainRegistration.cs b/services/acl/Acl.Application/DomainRegistration.cs
index de985f0..6773443 100644
--- a/services/acl/Acl.Application/DomainRegistration.cs
+++ b/services/acl/Acl.Application/DomainRegistration.cs
@@ -1,4 +1,5 @@
namespace Acl.Application;
-/// Domain-language payload handed to the ACL. No ZGW concepts here.
-public sealed record DomainRegistration(string Bsn);
+/// Domain-language payload handed to the ACL. No ZGW concepts here.
+/// is the registration's own id; the ACL records it as the zaak identificatie (adr-proposal #78).
+public sealed record DomainRegistration(string Bsn, string Reference);
diff --git a/services/acl/Acl.Application/IZaakGateway.cs b/services/acl/Acl.Application/IZaakGateway.cs
index c608c7a..583baf5 100644
--- a/services/acl/Acl.Application/IZaakGateway.cs
+++ b/services/acl/Acl.Application/IZaakGateway.cs
@@ -12,4 +12,8 @@ public interface IZaakGateway
/// the catalogus and POSTs a status against the zaak, dated .
///
Task SetZaakToEindstatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default);
+
+ /// Read the zaak's identificatie — the public-safe reference the register shows.
+ /// The Event Subscriber calls this through the ACL rather than reading ZGW itself (§8.1, #78).
+ Task GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default);
}
diff --git a/services/acl/Acl.Application/ZaakRequest.cs b/services/acl/Acl.Application/ZaakRequest.cs
index 875d162..9e49ac1 100644
--- a/services/acl/Acl.Application/ZaakRequest.cs
+++ b/services/acl/Acl.Application/ZaakRequest.cs
@@ -6,4 +6,5 @@ public sealed record ZaakRequest(
string VerantwoordelijkeOrganisatie,
string Vertrouwelijkheidaanduiding,
Uri Zaaktype,
- DateOnly Startdatum);
+ DateOnly Startdatum,
+ string Identificatie);
diff --git a/services/acl/Acl.Infrastructure/OpenZaakGateway.cs b/services/acl/Acl.Infrastructure/OpenZaakGateway.cs
index 523f215..92d85ea 100644
--- a/services/acl/Acl.Infrastructure/OpenZaakGateway.cs
+++ b/services/acl/Acl.Infrastructure/OpenZaakGateway.cs
@@ -20,7 +20,8 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
request.Zaaktype.ToString(),
request.VerantwoordelijkeOrganisatie,
request.Startdatum.ToString("yyyy-MM-dd"),
- request.Vertrouwelijkheidaanduiding)),
+ request.Vertrouwelijkheidaanduiding,
+ request.Identificatie)),
};
message.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", ZgwToken.Mint(options.ClientId, options.Secret));
@@ -61,6 +62,24 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
"Setting the zaak status", ct);
}
+ public async Task GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(zaakUrl);
+
+ using var message = new HttpRequestMessage(HttpMethod.Get, zaakUrl);
+ message.Headers.Authorization =
+ new AuthenticationHeaderValue("Bearer", ZgwToken.Mint(options.ClientId, options.Secret));
+ // The zaak is a geo resource; the Zaken API requires the CRS header even on GET.
+ message.Headers.Add("Accept-Crs", "EPSG:4326");
+
+ using var response = await http.SendAsync(message, ct);
+ await EnsureSuccessAsync(response, "Reading the zaak", ct);
+
+ var zaak = await response.Content.ReadFromJsonAsync(ct)
+ ?? throw new InvalidOperationException("OpenZaak returned an empty zaak response");
+ return zaak.Identificatie;
+ }
+
// POSTs a non-geo ZGW resource (resultaat/status — no CRS headers). Buffers the body so uwsgi gets
// a Content-Length instead of a chunked body (as with zaak-create).
private async Task PostAsync(string path, object dto, string action, CancellationToken ct)
@@ -132,11 +151,15 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
[property: JsonPropertyName("zaaktype")] string Zaaktype,
[property: JsonPropertyName("verantwoordelijkeOrganisatie")] string VerantwoordelijkeOrganisatie,
[property: JsonPropertyName("startdatum")] string Startdatum,
- [property: JsonPropertyName("vertrouwelijkheidaanduiding")] string Vertrouwelijkheidaanduiding);
+ [property: JsonPropertyName("vertrouwelijkheidaanduiding")] string Vertrouwelijkheidaanduiding,
+ [property: JsonPropertyName("identificatie")] string Identificatie);
private sealed record ZaakCreatedDto(
[property: JsonPropertyName("url")] string Url);
+ private sealed record ZaakReadDto(
+ [property: JsonPropertyName("identificatie")] string Identificatie);
+
private sealed record StatusDto(
[property: JsonPropertyName("zaak")] string Zaak,
[property: JsonPropertyName("statustype")] string Statustype,
diff --git a/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs b/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs
index dfe88b2..9cdac3c 100644
--- a/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs
+++ b/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs
@@ -22,12 +22,14 @@ public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack)
"seed it with OZ_PUBLISH=1 (`make integration` does this).");
var gateway = new OpenZaakGateway(stack.Http, stack.Options);
+ var reference = Guid.NewGuid().ToString(); // zaak identificatie must be unique per bronorganisatie
var request = new ZaakRequest(
Bronorganisatie: "517439943",
VerantwoordelijkeOrganisatie: "517439943",
Vertrouwelijkheidaanduiding: "openbaar",
Zaaktype: zaaktype!,
- Startdatum: DateOnly.FromDateTime(DateTime.UtcNow));
+ Startdatum: DateOnly.FromDateTime(DateTime.UtcNow),
+ Identificatie: reference);
var zaakUrl = await gateway.OpenZaakAsync(request);
@@ -36,11 +38,12 @@ public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack)
new Uri(stack.BaseUrl, "/zaken/api/v1/zaken/").ToString(),
zaakUrl.ToString());
- // ...and that zaak is really persisted with the default-filled fields.
+ // ...and that zaak is really persisted with the default-filled fields + the reference identificatie.
var zaak = await stack.GetZaakAsync(zaakUrl);
Assert.Equal(zaaktype.ToString(), zaak.GetProperty("zaaktype").GetString());
Assert.Equal("517439943", zaak.GetProperty("bronorganisatie").GetString());
Assert.Equal("openbaar", zaak.GetProperty("vertrouwelijkheidaanduiding").GetString());
+ Assert.Equal(reference, zaak.GetProperty("identificatie").GetString());
}
[Fact]
@@ -57,7 +60,8 @@ public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack)
VerantwoordelijkeOrganisatie: "517439943",
Vertrouwelijkheidaanduiding: "openbaar",
Zaaktype: zaaktype!,
- Startdatum: DateOnly.FromDateTime(DateTime.UtcNow)));
+ Startdatum: DateOnly.FromDateTime(DateTime.UtcNow),
+ Identificatie: Guid.NewGuid().ToString()));
await gateway.SetZaakToEindstatusAsync(zaakUrl, zaaktype!, DateOnly.FromDateTime(DateTime.UtcNow));
diff --git a/services/acl/Acl.Tests/AclServiceTests.cs b/services/acl/Acl.Tests/AclServiceTests.cs
index a51a132..2e74c8f 100644
--- a/services/acl/Acl.Tests/AclServiceTests.cs
+++ b/services/acl/Acl.Tests/AclServiceTests.cs
@@ -22,6 +22,14 @@ public class AclServiceTests
Approved = (zaakUrl, zaaktypeUrl, datumStatusGezet);
return Task.CompletedTask;
}
+
+ public Uri? ReadReferenceFor;
+
+ public Task GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default)
+ {
+ ReadReferenceFor = zaakUrl;
+ return Task.FromResult("REG-FROM-ZAAK");
+ }
}
private static AclDefaults Defaults() => new()
@@ -50,7 +58,7 @@ public class AclServiceTests
};
var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
- var url = await service.OpenZaakAsync(new DomainRegistration("123456782"));
+ var url = await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-77"));
Assert.Equal(gateway.Result, url);
var req = Assert.IsType(gateway.Captured);
@@ -59,6 +67,8 @@ public class AclServiceTests
Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding);
Assert.Equal(defaults.ZaaktypeUrl, req.Zaaktype);
Assert.Equal(new DateOnly(2026, 6, 4), req.Startdatum);
+ // The registration reference becomes the zaak identificatie (#78).
+ Assert.Equal("reg-77", req.Identificatie);
}
[Fact]
@@ -103,4 +113,27 @@ public class AclServiceTests
await Assert.ThrowsAsync(() => service.ApproveZaakAsync(null!));
Assert.Null(gateway.Approved);
}
+
+ [Fact]
+ public async Task Reading_a_zaak_reference_returns_the_zaaks_identificatie()
+ {
+ var gateway = new FakeGateway();
+ var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4)));
+ var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
+
+ var reference = await service.GetZaakReferenceAsync(zaak);
+
+ Assert.Equal("REG-FROM-ZAAK", reference);
+ Assert.Equal(zaak, gateway.ReadReferenceFor);
+ }
+
+ [Fact]
+ public async Task Reading_a_null_zaak_reference_is_rejected()
+ {
+ var gateway = new FakeGateway();
+ var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4)));
+
+ await Assert.ThrowsAsync(() => service.GetZaakReferenceAsync(null!));
+ Assert.Null(gateway.ReadReferenceFor);
+ }
}
diff --git a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs
index 7ca0271..af568c8 100644
--- a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs
+++ b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs
@@ -22,7 +22,7 @@ public class OpenZaakGatewayTests
private static ZaakRequest SampleRequest() => new(
"517439943", "517439943", "openbaar",
- new("http://openzaak/catalogi/api/v1/zaaktypen/big"), new DateOnly(2026, 6, 4));
+ new("http://openzaak/catalogi/api/v1/zaaktypen/big"), new DateOnly(2026, 6, 4), "REG-REF-1");
private static StubHandler Created(out RequestCapture capture)
{
@@ -67,6 +67,8 @@ public class OpenZaakGatewayTests
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", capture.Body);
Assert.Contains("\"startdatum\":\"2026-06-04\"", capture.Body);
Assert.Contains("\"zaaktype\":\"http://openzaak/catalogi/api/v1/zaaktypen/big\"", capture.Body);
+ // The registration reference is set as the zaak identificatie (#78).
+ Assert.Contains("\"identificatie\":\"REG-REF-1\"", capture.Body);
}
[Fact]
@@ -357,6 +359,61 @@ public class OpenZaakGatewayTests
Assert.Equal(4, rec.Requests.Count);
}
+ [Fact]
+ public async Task Reading_a_zaak_returns_its_identificatie_with_bearer_and_crs()
+ {
+ RequestCapture? capture = null;
+ var handler = new StubHandler(req =>
+ {
+ capture = new RequestCapture { Seen = req };
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = JsonContent.Create(new { identificatie = "REG-XYZ", url = ZaakUrl }),
+ });
+ });
+
+ var reference = await Gateway(handler).GetZaakIdentificatieAsync(new Uri(ZaakUrl));
+
+ Assert.Equal("REG-XYZ", reference);
+ Assert.Equal(HttpMethod.Get, capture!.Seen!.Method);
+ Assert.Equal(ZaakUrl, capture.Seen.RequestUri!.ToString());
+ Assert.Equal("Bearer", capture.Seen.Headers.Authorization!.Scheme);
+ Assert.Equal("EPSG:4326", Assert.Single(capture.Seen.Headers.GetValues("Accept-Crs")));
+ }
+
+ [Fact]
+ public async Task Reading_a_zaak_throws_when_openzaak_rejects_it()
+ {
+ var handler = new StubHandler(_ =>
+ Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("nope") }));
+
+ var ex = await Assert.ThrowsAsync(
+ () => Gateway(handler).GetZaakIdentificatieAsync(new Uri(ZaakUrl)));
+ Assert.Contains("Reading the zaak", ex.Message);
+ }
+
+ [Fact]
+ public async Task Reading_a_zaak_throws_when_openzaak_returns_an_empty_body()
+ {
+ var handler = new StubHandler(_ =>
+ Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("null", System.Text.Encoding.UTF8, "application/json"),
+ }));
+
+ var ex = await Assert.ThrowsAsync(
+ () => Gateway(handler).GetZaakIdentificatieAsync(new Uri(ZaakUrl)));
+ Assert.Contains("empty zaak response", ex.Message);
+ }
+
+ [Fact]
+ public async Task Reading_a_null_zaak_is_rejected()
+ {
+ var handler = new StubHandler(_ => throw new InvalidOperationException("should not be sent"));
+
+ await Assert.ThrowsAsync(() => Gateway(handler).GetZaakIdentificatieAsync(null!));
+ }
+
[Fact]
public async Task Approving_rejects_a_null_zaak_or_zaaktype()
{
diff --git a/tests/acceptance/Steps/EenZaakOpenenSteps.cs b/tests/acceptance/Steps/EenZaakOpenenSteps.cs
index 8c191bc..04780e1 100644
--- a/tests/acceptance/Steps/EenZaakOpenenSteps.cs
+++ b/tests/acceptance/Steps/EenZaakOpenenSteps.cs
@@ -18,7 +18,7 @@ public sealed class EenZaakOpenenSteps
[Given("a domain registration for BSN \"(.*)\"")]
public void GivenADomainRegistrationForBsn(string bsn)
- => _registration = new DomainRegistration(bsn);
+ => _registration = new DomainRegistration(bsn, "ACC-REF-1");
[Given("the ACL is configured with these defaults:")]
public void GivenTheAclIsConfiguredWithTheseDefaults(DataTable defaults)
diff --git a/tests/acceptance/Support/InMemoryZaakGateway.cs b/tests/acceptance/Support/InMemoryZaakGateway.cs
index 683e0d7..09c6414 100644
--- a/tests/acceptance/Support/InMemoryZaakGateway.cs
+++ b/tests/acceptance/Support/InMemoryZaakGateway.cs
@@ -24,4 +24,7 @@ public sealed class InMemoryZaakGateway : IZaakGateway
Approved = (zaakUrl, zaaktypeUrl, datumStatusGezet);
return Task.CompletedTask;
}
+
+ public Task GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default)
+ => Task.FromResult("ACC-REF-1");
}