diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b7e2f..52346ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,14 @@ ## Unreleased +- Added `templates.instantiate_template` and the ticket-release lifecycle on + `events` (`list_ticket_releases`, `update_ticket_releases`, and + `close_ticket_release`). Template instantiation sends `{}` when no overrides + are supplied, URI-escapes identifiers, and uses exact-response replay; + ticket-release writes remain single-attempt. + - **Security/reliability:** Mutations now default to a single attempt. Automatic header-replay - retries are limited to chart create/copy, event create, and workspace create, preventing + 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 issuing extra show-once credentials. - Aligned event, inventory, webhook, manage-session, and Designer requests with the generated diff --git a/README.md b/README.md index bd9a6c8..ef5fd4c 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ require "seatlayer" client = SeatLayer::Client.new(ENV.fetch("SEATLAYER_SECRET_KEY")) -# 1. Provision a venue for a new organiser from one of your templates. -chart = client.charts.copy("c_template_arena")["meta"] +# 1. Provision a venue for a new organiser from a public template. +chart = client.templates.instantiate_template("arena-standard")["meta"] client.charts.publish(chart["id"]) # 2. Create an event on it. @@ -252,11 +252,12 @@ error carries `status`, `code`, `body` and `request_id` — quote the request id ## Reliability **Retries.** Reads (`GET`/`HEAD`) retry 429, 408 and 5xx with exponential backoff and full jitter; -`Retry-After` wins when the server sends it. Automatic mutation retries are limited to the four -operations backed by exact response replay: `charts.create`, `charts.copy`, `events.create`, and -`workspaces.create`. Other 4xx responses are never retried. +`Retry-After` wins when the server sends it. Automatic mutation retries are limited to the five +operations backed by exact response replay: `charts.create`, `charts.copy`, +`templates.instantiate_template`, `events.create`, and `workspaces.create`. Other 4xx responses +are never retried. -**Idempotency.** Those four replay-backed operations carry an `Idempotency-Key`, generated when you +**Idempotency.** Those five replay-backed operations carry an `Idempotency-Key`, generated when you do not supply one and reused across attempts. Other mutations are single-attempt and receive no automatic key. A caller-supplied key is forwarded but does not enable retries. This includes inventory holds and bookings, show-once credential or secret creation, unsupported operations, and @@ -289,7 +290,8 @@ client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 }) | Resource | Methods | | --- | --- | | `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` | -| `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` | +| `templates` | `instantiate_template` | +| `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `list_ticket_releases` `update_ticket_releases` `close_ticket_release` `retrieve_report` `retrieve_log` | | `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `list_bookings` `retrieve_booking` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` | | `channels` | `list_channels` `create_channel` `update_channel` `update_assignments` `list_allocation` `retrieve_access_preview` `retrieve_report` `pause` `unpause` `archive` `create_buyer_access_session` `list_buyer_access_sessions` `revoke_buyer_access_session` `create_access_link` `list_access_links` `rotate_access_link` `revoke_access_link` | | `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` | @@ -337,7 +339,7 @@ the public manifest, not just from the wrapper. | Node.js (server) | [`@seatlayer/server`](https://www.npmjs.com/package/@seatlayer/server) | | Python (server) | [`seatlayer`](https://pypi.org/project/seatlayer/) | | PHP (server) | [`seatlayer/seatlayer-php`](https://packagist.org/packages/seatlayer/seatlayer-php) | -| Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java/0.1.0) | +| Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java/0.3.0) | | Go (server) | [`github.com/seatlayer/seatlayer-go`](https://pkg.go.dev/github.com/seatlayer/seatlayer-go) | | .NET (server) | [`SeatLayer`](https://www.nuget.org/packages/SeatLayer) | diff --git a/lib/seatlayer.rb b/lib/seatlayer.rb index 309b516..8facd0e 100644 --- a/lib/seatlayer.rb +++ b/lib/seatlayer.rb @@ -19,7 +19,7 @@ module SeatLayer # The SeatLayer client. class Client - attr_reader :charts, :events, :inventory, :channels, :sessions, :webhooks, :workspaces + attr_reader :charts, :events, :inventory, :channels, :sessions, :webhooks, :workspaces, :templates def initialize(secret_key, base_url: HTTPClient::DEFAULT_BASE_URL, max_retries: HTTPClient::DEFAULT_MAX_RETRIES, @@ -32,6 +32,7 @@ def initialize(secret_key, base_url: HTTPClient::DEFAULT_BASE_URL, @inventory = Inventory.new(@http) @channels = Channels.new(@http) @sessions = Sessions.new(@http) + @templates = Templates.new(@http) @webhooks = Webhooks.new(@http) @workspaces = Workspaces.new(@http) end diff --git a/lib/seatlayer/resources.rb b/lib/seatlayer/resources.rb index 91643d3..7ab3055 100644 --- a/lib/seatlayer/resources.rb +++ b/lib/seatlayer/resources.rb @@ -129,6 +129,23 @@ def publish(chart_id) end end + # Published SeatLayer catalogue templates. + # + # Instantiation creates an independent draft chart. Publish that returned + # chart before creating an event from it. + class Templates < Resource + # Instantiate a public template into a new draft chart. + # + # +fields+ intentionally defaults to an empty Hash: the API requires a JSON + # object even when there are no overrides, and JSON.generate({}) is `{}`. + def instantiate_template(template_id, fields: {}, idempotency_key: nil) + @client.post( + "/v1/templates/#{encode(template_id)}/instantiate", fields, + idempotency_key: idempotency_key, retry_policy: :header_replay + ) + end + end + # Event lifecycle, metadata and reports. class Events < Resource # One page of events. @@ -225,6 +242,21 @@ def update_hold_ttl(event_key, hold_ttl_ms) @client.post("/v1/events/#{encode(event_key)}/hold-ttl", { "holdTtlMs" => hold_ttl_ms }) end + def list_ticket_releases(event_key) + @client.get("/v1/events/#{encode(event_key)}/releases") + end + + # Replace every ticket release for an event. This stays single-attempt: the + # route does not promise exact idempotent-response replay. + def update_ticket_releases(event_key, releases:) + @client.put("/v1/events/#{encode(event_key)}/releases", { "releases" => releases }) + end + + # Close one release while preserving its audit provenance. + def close_ticket_release(event_key, release_id) + @client.post("/v1/events/#{encode(event_key)}/releases/#{encode(release_id)}/close") + end + def retrieve_report(event_key) @client.get("/v1/events/#{encode(event_key)}/report") end diff --git a/lib/seatlayer/version.rb b/lib/seatlayer/version.rb index 70750b2..50771a1 100644 --- a/lib/seatlayer/version.rb +++ b/lib/seatlayer/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SeatLayer - VERSION = "0.3.0" + VERSION = "0.4.0" end diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 454f96c..dd567ad 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -430,6 +430,46 @@ end describe "generated public wire contract" do + it "instantiates templates with an empty object and exact-response replay" do + client, transport = build_client([ + { status: 429, body: '{"error":"rate_limited"}', + headers: { "retry-after" => "0" } }, + { status: 201, body: '{"meta":{"id":"c_1"}}' } + ]) + client.templates.instantiate_template("arena/standard") + + expect(transport.calls).to have_attributes(length: 2) + expect(transport.calls[0].url).to end_with("/v1/templates/arena%2Fstandard/instantiate") + expect(transport.calls[0].body).to eq("{}") + expect(transport.calls[0].headers["Idempotency-Key"]) + .to eq(transport.calls[1].headers["Idempotency-Key"]) + end + + it "wraps ticket release routes with replacement bodies and single-attempt writes" do + client, transport = build_client([ + { status: 200, body: '{"releases":[]}' }, + { status: 200, body: '{"releases":[]}' }, + { status: 429, body: '{"error":"rate_limited"}', + headers: { "retry-after" => "0" } } + ]) + client.events.list_ticket_releases("ev/1") + client.events.update_ticket_releases( + "ev/1", releases: [{ "id" => "rel_1", "name" => "Early", "price" => 2500, "action" => "buy" }] + ) + + expect { client.events.close_ticket_release("ev/1", "rel/1") } + .to raise_error(SeatLayer::RateLimitError) + + expect(transport.calls).to have_attributes(length: 3) + expect(transport.calls[0].http_method).to eq("GET") + expect(transport.calls[0].url).to end_with("/v1/events/ev%2F1/releases") + expect(JSON.parse(transport.calls[1].body)).to eq( + "releases" => [{ "id" => "rel_1", "name" => "Early", "price" => 2500, "action" => "buy" }] + ) + expect(transport.calls[2].url).to end_with("/v1/events/ev%2F1/releases/rel%2F1/close") + expect(transport.calls[2].headers).not_to have_key("Idempotency-Key") + end + it "sends the expanded event, hold, and block fields" do client, transport = build_client([ { status: 201, body: '{"meta":{"key":"ev_1"}}' },