diff --git a/services/acl/Acl.Application/AclService.cs b/services/acl/Acl.Application/AclService.cs
index cab5577..f3b7973 100644
--- a/services/acl/Acl.Application/AclService.cs
+++ b/services/acl/Acl.Application/AclService.cs
@@ -52,6 +52,18 @@ public sealed class AclService(
ct);
}
+ ///
+ /// The register record held by an object in Objecten, for the Event Subscriber (S-19b-2). The
+ /// subscriber gets only an object URL on the notification and may not read Objecten itself
+ /// (§8.1, ADR-0028), so the ACL reads it back.
+ ///
+ public Task GetRegisterRecordAsync(Uri objectUrl, CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(objectUrl);
+
+ return register.GetAsync(objectUrl, ct);
+ }
+
/// The zaak's UUID — the key the register record and the read projection rows share.
private static string ZaakId(Uri zaakUrl) => zaakUrl.Segments[^1].TrimEnd('/');
diff --git a/services/acl/Acl.Application/IRegisterRecordGateway.cs b/services/acl/Acl.Application/IRegisterRecordGateway.cs
index 4540861..6e8d4bd 100644
--- a/services/acl/Acl.Application/IRegisterRecordGateway.cs
+++ b/services/acl/Acl.Application/IRegisterRecordGateway.cs
@@ -13,6 +13,14 @@ public interface IRegisterRecordGateway
/// the existing object instead of creating a second one (§8.6).
///
Task UpsertAsync(RegisterRecord record, CancellationToken ct = default);
+
+ ///
+ /// The register record held by the object at , or null if that
+ /// object holds none. The Event Subscriber projects a register write from the notification NRC
+ /// delivers, which carries only the object URL — so it reads the record back through the ACL
+ /// rather than talking to Objecten itself (§8.1, S-19b-2).
+ ///
+ Task GetAsync(Uri objectUrl, CancellationToken ct = default);
}
///
diff --git a/services/acl/Acl.Infrastructure/ObjectenGateway.cs b/services/acl/Acl.Infrastructure/ObjectenGateway.cs
index aa43e68..80aa715 100644
--- a/services/acl/Acl.Infrastructure/ObjectenGateway.cs
+++ b/services/acl/Acl.Infrastructure/ObjectenGateway.cs
@@ -38,6 +38,9 @@ public sealed class ObjectenGateway(HttpClient http, ObjectenOptions options, IC
"Updating the register record", ct);
}
+ public Task GetAsync(Uri objectUrl, CancellationToken ct = default)
+ => throw new NotImplementedException();
+
private RecordDto NewRecord(int typeVersion, RecordDataDto data) =>
new(typeVersion, data, clock.Today.ToString("yyyy-MM-dd"));
diff --git a/services/acl/Acl.Tests/AclServiceTests.cs b/services/acl/Acl.Tests/AclServiceTests.cs
index 158be35..52e1d61 100644
--- a/services/acl/Acl.Tests/AclServiceTests.cs
+++ b/services/acl/Acl.Tests/AclServiceTests.cs
@@ -79,11 +79,21 @@ public class AclServiceTests
{
public readonly List Upserted = [];
+ public RegisterRecord? Stored;
+
+ public Uri? ReadFrom;
+
public Task UpsertAsync(RegisterRecord record, CancellationToken ct = default)
{
Upserted.Add(record);
return Task.CompletedTask;
}
+
+ public Task GetAsync(Uri objectUrl, CancellationToken ct = default)
+ {
+ ReadFrom = objectUrl;
+ return Task.FromResult(Stored);
+ }
}
private static AclDefaults Defaults() => new()
@@ -130,6 +140,52 @@ public class AclServiceTests
Assert.Equal("reg-77", req.Identificatie);
}
+ [Fact]
+ public async Task Opening_a_zaak_also_writes_an_ingediend_register_record(/* S-19b-2 */)
+ {
+ var gateway = new FakeGateway();
+ var register = new FakeRegisterRecordGateway();
+ var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4));
+
+ await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-77"));
+
+ // The register — not ZGW — is what the read projection is sourced from (ADR-0028), so a
+ // submitted registration has to exist there the moment the zaak is opened, not only on
+ // approval. Approval upserts this same record to INGESCHREVEN.
+ var record = Assert.Single(register.Upserted);
+ Assert.Equal("abc", record.Id);
+ Assert.Equal("INGEDIEND", record.Status);
+ // The reference comes from the registration itself — no ZGW read-back needed on this path.
+ Assert.Equal("reg-77", record.Reference);
+ }
+
+ [Fact]
+ public async Task Reading_a_register_record_goes_through_the_objecten_gateway(/* S-19b-2 */)
+ {
+ var gateway = new FakeGateway();
+ var register = new FakeRegisterRecordGateway { Stored = new RegisterRecord("abc", "INGESCHREVEN", "reg-77") };
+ var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4));
+ var objectUrl = new Uri("http://objecten.local:8000/api/v2/objects/9de4a2ca");
+
+ var record = await service.GetRegisterRecordAsync(objectUrl);
+
+ Assert.Equal(objectUrl, register.ReadFrom);
+ Assert.Equal("abc", record!.Id);
+ Assert.Equal("INGESCHREVEN", record.Status);
+ Assert.Equal("reg-77", record.Reference);
+ }
+
+ [Fact]
+ public async Task Reading_a_register_record_from_a_null_url_is_rejected(/* S-19b-2 */)
+ {
+ var gateway = new FakeGateway();
+ var register = new FakeRegisterRecordGateway();
+ var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4));
+
+ await Assert.ThrowsAsync(() => service.GetRegisterRecordAsync(null!));
+ Assert.Null(register.ReadFrom);
+ }
+
[Fact]
public async Task Opening_a_zaak_reflects_a_default_fill_update(/* S-15b */)
{
diff --git a/services/acl/Acl.Tests/ObjectenGatewayTests.cs b/services/acl/Acl.Tests/ObjectenGatewayTests.cs
index 60c3a6f..d095764 100644
--- a/services/acl/Acl.Tests/ObjectenGatewayTests.cs
+++ b/services/acl/Acl.Tests/ObjectenGatewayTests.cs
@@ -83,6 +83,43 @@ public class ObjectenGatewayTests
private static RegisterRecord Record() => new("zaak-uuid-1", RegisterRecordStatus.Ingeschreven, "REG-2026-0001");
+ [Fact]
+ public async Task Reads_a_register_record_back_from_its_object_url(/* S-19b-2 */)
+ {
+ var sent = new List();
+ var objectUrl = new Uri("http://objecten:8000/api/v2/objects/obj-9");
+ var gateway = Gateway(sent, _ => Json(new
+ {
+ url = objectUrl.ToString(),
+ record = new { data = new { id = "zaak-uuid-1", status = "INGESCHREVEN", reference = "REG-2026-0001" } },
+ }));
+
+ var record = await gateway.GetAsync(objectUrl);
+
+ // The object is fetched directly by the URL the notification carried — no objecttype
+ // resolution and no search, unlike a write.
+ var read = Assert.Single(sent);
+ Assert.Equal(HttpMethod.Get, read.Method);
+ Assert.Equal(objectUrl, read.Uri);
+ // Objecten is a geo API: the CRS header is required on reads too.
+ Assert.Equal("EPSG:4326", read.AcceptCrs);
+ Assert.Equal("Token objecten-token", read.Auth);
+ Assert.Equal("zaak-uuid-1", record!.Id);
+ Assert.Equal("INGESCHREVEN", record.Status);
+ Assert.Equal("REG-2026-0001", record.Reference);
+ }
+
+ [Fact]
+ public async Task Reading_an_object_that_is_gone_yields_no_record(/* S-19b-2 */)
+ {
+ var sent = new List();
+ var gateway = Gateway(sent, _ => new HttpResponseMessage(HttpStatusCode.NotFound));
+
+ // A record deleted between the notification and the read is not an error — there is simply
+ // nothing to project (§8.6: the subscriber tolerates whatever order deliveries arrive in).
+ Assert.Null(await gateway.GetAsync(new Uri("http://objecten:8000/api/v2/objects/gone")));
+ }
+
[Fact]
public async Task Creates_the_object_when_none_exists_for_the_registration()
{
diff --git a/tests/acceptance/Support/InMemoryZaakGateway.cs b/tests/acceptance/Support/InMemoryZaakGateway.cs
index 71142fc..8c3b6c1 100644
--- a/tests/acceptance/Support/InMemoryZaakGateway.cs
+++ b/tests/acceptance/Support/InMemoryZaakGateway.cs
@@ -65,4 +65,8 @@ public sealed class InMemoryRegisterRecordGateway : IRegisterRecordGateway
Upserted.Add(record);
return Task.CompletedTask;
}
+
+ /// The most recently written record — scenarios never read one back by object URL.
+ public Task GetAsync(Uri objectUrl, CancellationToken ct = default)
+ => Task.FromResult(Upserted.Count == 0 ? null : Upserted[^1]);
}