diff --git a/services/acl/Acl.Api/Program.cs b/services/acl/Acl.Api/Program.cs index 72ca25c..659f1e6 100644 --- a/services/acl/Acl.Api/Program.cs +++ b/services/acl/Acl.Api/Program.cs @@ -90,6 +90,16 @@ app.MapPost("/zaken/reference", async (ZaakReferenceRequest body, AclService acl return Results.Ok(new { reference }); }); +// Read the register record an object in Objecten holds. The Event Subscriber projects a register +// write from the notification NRC delivers, which carries only the object URL, and may not talk to +// Objecten itself (§8.1, ADR-0028/ADR-0030). 404 when the object holds no record — the subscriber +// treats that as "nothing to project" rather than an error (§8.6). +app.MapPost("/register-records/read", async (RegisterRecordReadRequest body, AclService acl, CancellationToken ct) => +{ + var record = await acl.GetRegisterRecordAsync(new Uri(body.ObjectUrl), ct); + return record is null ? Results.NotFound() : Results.Ok(record); +}); + // Store an uploaded diploma against a zaak (S-10b): the domain sends the file as base64; the ACL // creates the ZGW enkelvoudiginformatieobject and relates it to the zaak (§8.1). Returns its URL. app.MapPost("/documenten", async (StoreDocumentRequest body, AclService acl, CancellationToken ct) => @@ -131,6 +141,9 @@ public sealed record CancelZaakRequest(string ZaakUrl); public sealed record ZaakReferenceRequest(string ZaakUrl); +/// The object whose register record the Event Subscriber wants read back (S-19b-2). +public sealed record RegisterRecordReadRequest(string ObjectUrl); + public sealed record StoreDocumentRequest(string ZaakUrl, string ContentBase64, string FileName, string ContentType); public partial class Program; diff --git a/services/acl/Acl.Application/AclService.cs b/services/acl/Acl.Application/AclService.cs index f3b7973..1195df2 100644 --- a/services/acl/Acl.Application/AclService.cs +++ b/services/acl/Acl.Application/AclService.cs @@ -24,7 +24,16 @@ public sealed class AclService( clock.Today, registration.Reference); - return await gateway.OpenZaakAsync(request, ct); + var zaakUrl = await gateway.OpenZaakAsync(request, ct); + + // The register — not ZGW — is what the read projection is sourced from (ADR-0028/ADR-0030), + // so the record exists from submission, not only from approval. Same two-writes-converging + // posture as ApproveZaakAsync: the upsert is keyed on the zaak id, so a retried submit + // updates the record rather than adding a second one (§8.6). + await register.UpsertAsync( + new RegisterRecord(ZaakId(zaakUrl), RegisterRecordStatus.Ingediend, registration.Reference), ct); + + return zaakUrl; } /// diff --git a/services/acl/Acl.Infrastructure/ObjectenGateway.cs b/services/acl/Acl.Infrastructure/ObjectenGateway.cs index 80aa715..36603c4 100644 --- a/services/acl/Acl.Infrastructure/ObjectenGateway.cs +++ b/services/acl/Acl.Infrastructure/ObjectenGateway.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json.Serialization; @@ -38,8 +39,29 @@ 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(); + public async Task GetAsync(Uri objectUrl, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(objectUrl); + + // Fetched by the URL the notification carried, so no objecttype resolution and no search — + // unlike a write, which has to find the object for a registration id. + using var message = new HttpRequestMessage(HttpMethod.Get, objectUrl); + message.Headers.Authorization = new AuthenticationHeaderValue("Token", options.Token); + message.Headers.Add("Accept-Crs", "EPSG:4326"); + + using var response = await http.SendAsync(message, ct); + // The object may be gone by the time a (possibly redelivered) notification is handled — + // there is simply nothing to project, which is not a failure (§8.6). + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + + await EnsureSuccessAsync(response, "Reading the register record", ct); + + var body = await response.Content.ReadFromJsonAsync(ct) + ?? throw new InvalidOperationException("Objecten returned an empty object response"); + var data = body.Record?.Data; + return data is null ? null : new RegisterRecord(data.Id, data.Status, data.Reference); + } private RecordDto NewRecord(int typeVersion, RecordDataDto data) => new(typeVersion, data, clock.Today.ToString("yyyy-MM-dd")); @@ -144,6 +166,12 @@ public sealed class ObjectenGateway(HttpClient http, ObjectenOptions options, IC private sealed record ObjectDto( [property: JsonPropertyName("url")] string Url); + private sealed record ReadObjectDto( + [property: JsonPropertyName("record")] ReadRecordDto? Record); + + private sealed record ReadRecordDto( + [property: JsonPropertyName("data")] RecordDataDto? Data); + private sealed record CreateObjectDto( [property: JsonPropertyName("type")] string Type, [property: JsonPropertyName("record")] RecordDto Record);