diff --git a/services/acl/Acl.Infrastructure/ObjectenGateway.cs b/services/acl/Acl.Infrastructure/ObjectenGateway.cs
index 722f62c..1d89ca0 100644
--- a/services/acl/Acl.Infrastructure/ObjectenGateway.cs
+++ b/services/acl/Acl.Infrastructure/ObjectenGateway.cs
@@ -41,7 +41,7 @@ public sealed class ObjectenGateway(HttpClient http, ObjectenOptions options, IC
private RecordDto NewRecord(int typeVersion, RecordDataDto data) =>
new(typeVersion, data, clock.Today.ToString("yyyy-MM-dd"));
- /// The URL + latest version number of the configured objecttype, read from Objecttypen.
+ /// The URL + latest published version of the configured objecttype, read from Objecttypen.
private async Task ResolveObjecttypeAsync(CancellationToken ct)
{
var page = await GetAsync(
@@ -52,10 +52,16 @@ public sealed class ObjectenGateway(HttpClient http, ObjectenOptions options, IC
?? throw new InvalidOperationException(
$"No objecttype '{options.ObjecttypeName}' registered in Objecttypen — is the RegisterRecord seed applied?");
- // `versions` lists the objecttype's version URLs; the count is the latest version number.
- var version = match.Versions?.Count
- ?? throw new InvalidOperationException($"Objecttype '{options.ObjecttypeName}' has no published version");
- return new Objecttype(new Uri(match.Url), version);
+ var url = new Uri(match.Url);
+ // Write against the highest *published* version: a draft version's schema is still being
+ // shaped, and objects written against it would be validated by a moving target.
+ var versions = await GetAsync>(
+ new Uri(url + "/versions"), options.ObjecttypenToken, crs: false, "objecttype versions", ct);
+ var latest = versions.Where(v => v.Status == "published").Select(v => v.Version).DefaultIfEmpty(0).Max();
+ if (latest == 0)
+ throw new InvalidOperationException($"Objecttype '{options.ObjecttypeName}' has no published version");
+
+ return new Objecttype(url, latest);
}
/// The URL of the object already holding this registration's record, or null if there is none.
@@ -115,8 +121,11 @@ public sealed class ObjectenGateway(HttpClient http, ObjectenOptions options, IC
private sealed record ObjecttypeDto(
[property: JsonPropertyName("url")] string Url,
- [property: JsonPropertyName("name")] string? Name,
- [property: JsonPropertyName("versions")] IReadOnlyList? Versions);
+ [property: JsonPropertyName("name")] string? Name);
+
+ private sealed record ObjecttypeVersionDto(
+ [property: JsonPropertyName("version")] int Version,
+ [property: JsonPropertyName("status")] string? Status);
private sealed record ObjectPage(
[property: JsonPropertyName("results")] IReadOnlyList? Results);
diff --git a/services/acl/Acl.Tests/ObjectenGatewayTests.cs b/services/acl/Acl.Tests/ObjectenGatewayTests.cs
index bbedc84..6a3de55 100644
--- a/services/acl/Acl.Tests/ObjectenGatewayTests.cs
+++ b/services/acl/Acl.Tests/ObjectenGatewayTests.cs
@@ -46,24 +46,32 @@ public class ObjectenGatewayTests
},
new FixedClock(new DateOnly(2026, 6, 4)));
- // The Objecttypen lookup (by name) and the Objecten search (by data attribute) that precede every
- // write. `respondToWrite` decides what the POST/PATCH returns.
+ // A stack that answers the three reads every write is preceded by: the objecttype list (matched by
+ // name), that objecttype's versions, and the Objecten search for an existing record.
private static HttpResponseMessage Route(HttpRequestMessage req, object[] existingObjects) =>
- req.RequestUri!.AbsolutePath.StartsWith("/api/v2/objecttypes", StringComparison.Ordinal)
- ? new HttpResponseMessage(HttpStatusCode.OK)
+ req.RequestUri!.AbsolutePath.EndsWith("/versions", StringComparison.Ordinal)
+ ? Json(new object[]
{
- Content = JsonContent.Create(new
+ new { version = 1, status = "published" },
+ new { version = 2, status = "published" },
+ // A draft must never be written against, even though it is the highest version.
+ new { version = 3, status = "draft" },
+ })
+ : req.RequestUri.AbsolutePath.StartsWith("/api/v2/objecttypes", StringComparison.Ordinal)
+ ? Json(new
{
results = new[]
{
- new { url = "http://objecttypen:8000/api/v2/objecttypes/other", name = "SomethingElse", versions = new[] { "…/versions/1" } },
- new { url = ObjecttypeUrl, name = "RegisterRecord", versions = new[] { "…/versions/1", "…/versions/2" } },
+ new { url = "http://objecttypen:8000/api/v2/objecttypes/other", name = "SomethingElse" },
+ new { url = ObjecttypeUrl, name = "RegisterRecord" },
},
- }),
- }
- : req.Method == HttpMethod.Get
- ? new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(new { results = existingObjects }) }
- : new HttpResponseMessage(HttpStatusCode.Created) { Content = JsonContent.Create(new { url = "http://objecten:8000/api/v2/objects/obj-1" }) };
+ })
+ : req.Method == HttpMethod.Get
+ ? Json(new { results = existingObjects })
+ : new HttpResponseMessage(HttpStatusCode.Created) { Content = JsonContent.Create(new { url = "http://objecten:8000/api/v2/objects/obj-1" }) };
+
+ private static HttpResponseMessage Json(object body) =>
+ new(HttpStatusCode.OK) { Content = JsonContent.Create(body) };
private static RegisterRecord Record() => new("zaak-uuid-1", RegisterRecordStatus.Ingeschreven, "REG-2026-0001");
@@ -76,6 +84,7 @@ public class ObjectenGatewayTests
var write = sent.Single(s => s.Method == HttpMethod.Post && s.Uri.AbsolutePath == "/api/v2/objects");
Assert.Contains($"\"type\":\"{ObjecttypeUrl}\"", write.Body);
+ // The highest *published* version (2), not the highest version (a draft 3).
Assert.Contains("\"typeVersion\":2", write.Body);
Assert.Contains("\"id\":\"zaak-uuid-1\"", write.Body);
Assert.Contains("\"status\":\"INGESCHREVEN\"", write.Body);
@@ -145,7 +154,19 @@ public class ObjectenGatewayTests
await gateway.UpsertAsync(Record());
await gateway.UpsertAsync(Record() with { Id = "zaak-uuid-2" });
- Assert.Single(sent, s => s.Uri.AbsolutePath.StartsWith("/api/v2/objecttypes", StringComparison.Ordinal));
+ Assert.Single(sent, s => s.Uri.AbsolutePath == "/api/v2/objecttypes");
+ }
+
+ [Fact]
+ public async Task Fails_loudly_when_the_objecttype_has_no_published_version()
+ {
+ var sent = new List();
+ var gateway = Gateway(sent, req => req.RequestUri!.AbsolutePath.EndsWith("/versions", StringComparison.Ordinal)
+ ? Json(new object[] { new { version = 1, status = "draft" } })
+ : Route(req, []));
+
+ var error = await Assert.ThrowsAsync(() => gateway.UpsertAsync(Record()));
+ Assert.Contains("published version", error.Message);
}
[Fact]