From 3f347a26f31a72da09087105de5ae725901424dc Mon Sep 17 00:00:00 2001 From: navin10sharma <3096611+navin10sharma@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:01:49 +0530 Subject: [PATCH 1/3] feat(events): add configuration binding --- CHANGELOG.md | 4 ++ README.md | 2 +- src/SeatLayer/Services.cs | 51 +++++++++++++++++++++++ tests/SeatLayer.Tests/ClientTests.cs | 62 ++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 169003e..8ed3c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Added exact immutable Event configuration binding reads and compare-and-set + attach/detach through `Events.RetrieveConfigurationBindingAsync` and + `Events.UpdateConfigurationBindingAsync`. Updates remain deliberately single-attempt. + - **Security/reliability:** Mutations now default to a single attempt. Automatic header-replay retries are limited to chart create/copy, template instantiation, event create, and workspace create, preventing transient failures from duplicating holds or best-available results and from diff --git a/README.md b/README.md index 3eeeb2c..4231e99 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ await client.SendAsync(HttpMethod.Post, "/v1/events/ev_1/some-new-route", | --- | --- | | `Charts` | `ListAsync` `ListAllAsync` `CreateAsync` `RetrieveAsync` `UpdateAsync` `DeleteAsync` `CopyAsync` `ArchiveAsync` `UnarchiveAsync` `PublishAsync` | | `Templates` | `InstantiateTemplateAsync` | -| `Events` | `ListAsync` `ListAllAsync` `CreateAsync` `RetrieveAsync` `UpdateAsync` `DeleteAsync` `UpdateChartAsync` `CloseAsync` `ReopenAsync` `ArchiveAsync` `RetrieveHoldTtlAsync` `UpdateHoldTtlAsync` `ListTicketReleasesAsync` `UpdateTicketReleasesAsync` `CloseTicketReleaseAsync` `RetrieveReportAsync` `RetrieveLogAsync` | +| `Events` | `ListAsync` `ListAllAsync` `CreateAsync` `RetrieveAsync` `RetrieveConfigurationBindingAsync` `UpdateConfigurationBindingAsync` `UpdateAsync` `DeleteAsync` `UpdateChartAsync` `CloseAsync` `ReopenAsync` `ArchiveAsync` `RetrieveHoldTtlAsync` `UpdateHoldTtlAsync` `ListTicketReleasesAsync` `UpdateTicketReleasesAsync` `CloseTicketReleaseAsync` `RetrieveReportAsync` `RetrieveLogAsync` | | `Channels` | `ListAsync` `CreateAsync` `UpdateAsync` `UpdateAssignmentsAsync` `ListAllocationAsync` `RetrieveAccessPreviewAsync` `RetrieveReportAsync` `PauseAsync` `UnpauseAsync` `ArchiveAsync` `CreateBuyerAccessSessionAsync` `ListBuyerAccessSessionsAsync` `RevokeBuyerAccessSessionAsync` | | `Inventory` | `HoldAsync` `HoldBestAvailableAsync` `BookBestAvailableAsync` `ExtendHoldAsync` `RetrieveHoldAsync` `ReleaseAsync` `BookAsync` `BookLabelsAsync` `BoxOfficeBookAsync` `UnbookAsync` `BlockAsync` `UnblockAsync` `UnblockAllAsync` `RetrieveAvailabilityAsync` `UpdateAvailabilityAsync` `ListBookingsAsync` `RetrieveBookingAsync` | | `Sessions` | `CreateManageSessionAsync` `RevokeManageSessionAsync` `CreateDesignerSessionAsync` `RevokeDesignerSessionAsync` | diff --git a/src/SeatLayer/Services.cs b/src/SeatLayer/Services.cs index 15d1c95..6a05028 100644 --- a/src/SeatLayer/Services.cs +++ b/src/SeatLayer/Services.cs @@ -353,6 +353,27 @@ public sealed class TicketReleaseReplaceRequest public required IReadOnlyList Releases { get; set; } } +/// Selects one immutable published Event configuration version. +/// Configuration identity remains separate from the chart's venue geometry. +public sealed class EventConfigurationReference +{ + /// Configuration library id. + public required string Id { get; set; } + + /// Exact immutable version to attach. + public required long Version { get; set; } +} + +/// Compare-and-set request for an Event's configuration binding. +public sealed class EventConfigurationBindingUpdateRequest +{ + /// Revision returned by the latest binding read. + public required long ExpectedRevision { get; set; } + + /// Published version to attach, or null to explicitly detach. + public EventConfigurationReference? Configuration { get; set; } +} + /// Event lifecycle, metadata and reports. public sealed class EventsService { @@ -433,6 +454,36 @@ public async Task ListAsync( string eventKey, CancellationToken cancellationToken = default) => _client.GetAsync($"/v1/events/{SeatLayerClient.Escape(eventKey)}", null, cancellationToken); + /// Reads the Event's exact immutable configuration selection and audit trail. + public Task> RetrieveConfigurationBindingAsync( + string eventKey, CancellationToken cancellationToken = default) + => _client.GetAsync( + $"/v1/events/{SeatLayerClient.Escape(eventKey)}/event-configuration", + null, + cancellationToken); + + /// Attaches an exact published configuration version, or explicitly detaches it. + /// + /// The expected revision prevents concurrent administrative changes from silently overwriting + /// each other. This mutation remains single-attempt because it has no header-replay contract. + /// + public Task> UpdateConfigurationBindingAsync( + string eventKey, + EventConfigurationBindingUpdateRequest request, + CancellationToken cancellationToken = default) + { + // Body.Of removes null optional values, but null is the explicit detach instruction here. + var body = new Dictionary + { + ["expectedRevision"] = request.ExpectedRevision, + ["configuration"] = request.Configuration, + }; + return _client.PutAsync( + $"/v1/events/{SeatLayerClient.Escape(eventKey)}/event-configuration", + body, + cancellationToken); + } + /// Updates event metadata. public Task> UpdateAsync( string eventKey, IDictionary fields, CancellationToken cancellationToken = default) diff --git a/tests/SeatLayer.Tests/ClientTests.cs b/tests/SeatLayer.Tests/ClientTests.cs index 6cf5e08..5ca8a28 100644 --- a/tests/SeatLayer.Tests/ClientTests.cs +++ b/tests/SeatLayer.Tests/ClientTests.cs @@ -203,6 +203,68 @@ await Assert.ThrowsAsync( Assert.False(handler.Calls[2].Headers.Contains("Idempotency-Key")); } + [Fact] + public async Task EventConfigurationBindingUsesExactVersionsExplicitDetachAndSingleAttempts() + { + const string binding = "{\"configuration\":{\"id\":\"ec_touring\",\"version\":3}," + + "\"revision\":7,\"changedBy\":\"api-key:key_1\",\"changedAt\":123,\"audit\":[" + + "{\"id\":\"eca_1\",\"from\":null,\"to\":{\"id\":\"ec_touring\",\"version\":3}," + + "\"revision\":7,\"actor\":\"api-key:key_1\",\"createdAt\":123}]}"; + var retryNow = new Dictionary { ["Retry-After"] = "0" }; + var (client, handler) = Build(new[] + { + Ok(binding), + Ok(binding), + Ok("{\"configuration\":null,\"revision\":8,\"changedBy\":null,\"changedAt\":null,\"audit\":[]}"), + Status((HttpStatusCode)429, "{\"error\":\"rate_limited\"}", retryNow), + }); + + var retrieved = await client.Events.RetrieveConfigurationBindingAsync("ev / main"); + var attached = await client.Events.UpdateConfigurationBindingAsync( + "ev / main", + new EventConfigurationBindingUpdateRequest + { + ExpectedRevision = 6, + Configuration = new EventConfigurationReference { Id = "ec_touring", Version = 3 }, + }); + var detached = await client.Events.UpdateConfigurationBindingAsync( + "ev / main", + new EventConfigurationBindingUpdateRequest { ExpectedRevision = 7, Configuration = null }); + await Assert.ThrowsAsync( + () => client.Events.UpdateConfigurationBindingAsync( + "ev_1", + new EventConfigurationBindingUpdateRequest { ExpectedRevision = 8, Configuration = null })); + + var configuration = Assert.IsAssignableFrom>( + retrieved["configuration"]); + Assert.Equal("ec_touring", configuration["id"]); + var audit = Assert.IsAssignableFrom>(retrieved["audit"]); + var firstAudit = Assert.IsAssignableFrom>(audit[0]); + Assert.Null(firstAudit["from"]); + Assert.NotNull(attached["configuration"]); + Assert.Null(detached["configuration"]); + + Assert.Equal(4, handler.Calls.Count); + for (var index = 0; index < 3; index++) + { + Assert.Equal( + "https://api.seatlayer.io/v1/events/ev%20%2F%20main/event-configuration", + handler.Calls[index].Url.AbsoluteUri); + } + Assert.Equal(HttpMethod.Get, handler.Calls[0].Method); + Assert.Equal(HttpMethod.Put, handler.Calls[1].Method); + Assert.Equal(HttpMethod.Put, handler.Calls[2].Method); + using var attachBody = JsonDocument.Parse(handler.Calls[1].Body!); + Assert.Equal(6, attachBody.RootElement.GetProperty("expectedRevision").GetInt64()); + Assert.Equal("ec_touring", attachBody.RootElement.GetProperty("configuration") + .GetProperty("id").GetString()); + using var detachBody = JsonDocument.Parse(handler.Calls[2].Body!); + Assert.Equal(JsonValueKind.Null, detachBody.RootElement.GetProperty("configuration").ValueKind); + Assert.False(handler.Calls[1].Headers.Contains("Idempotency-Key")); + Assert.False(handler.Calls[2].Headers.Contains("Idempotency-Key")); + Assert.False(handler.Calls[3].Headers.Contains("Idempotency-Key")); + } + [Fact] public async Task HonoursCallerIdempotencyKey() { From f3522f6c88ff364b3f4ca60561edaeb916b4dcc3 Mon Sep 17 00:00:00 2001 From: navin10sharma <3096611+navin10sharma@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:30:32 +0530 Subject: [PATCH 2/3] chore(release): prepare v0.5.0 --- CHANGELOG.md | 2 +- src/SeatLayer/SeatLayer.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed3c79..e45693f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.5.0 — 2026-08-23 - Added exact immutable Event configuration binding reads and compare-and-set attach/detach through `Events.RetrieveConfigurationBindingAsync` and diff --git a/src/SeatLayer/SeatLayer.csproj b/src/SeatLayer/SeatLayer.csproj index 9dd0127..acc9207 100644 --- a/src/SeatLayer/SeatLayer.csproj +++ b/src/SeatLayer/SeatLayer.csproj @@ -14,7 +14,7 @@ SeatLayer - 0.4.0 + 0.5.0 SeatLayer SeatLayer Official .NET server SDK for the SeatLayer reserved-seating API. Server-side only. From 769520ab947fda73a048ec83152434d34a73116a Mon Sep 17 00:00:00 2001 From: navin10sharma <3096611+navin10sharma@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:36:22 +0530 Subject: [PATCH 3/3] chore(release): move configuration binding to v0.6.0 --- CHANGELOG.md | 2 +- src/SeatLayer/SeatLayer.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c916f0..79abc1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.5.0 — 2026-08-23 +## 0.6.0 — 2026-08-23 - Added exact immutable Event configuration binding reads and compare-and-set attach/detach through `Events.RetrieveConfigurationBindingAsync` and diff --git a/src/SeatLayer/SeatLayer.csproj b/src/SeatLayer/SeatLayer.csproj index acc9207..505be63 100644 --- a/src/SeatLayer/SeatLayer.csproj +++ b/src/SeatLayer/SeatLayer.csproj @@ -14,7 +14,7 @@ SeatLayer - 0.5.0 + 0.6.0 SeatLayer SeatLayer Official .NET server SDK for the SeatLayer reserved-seating API. Server-side only.