seatlayer 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +73 -24
- data/lib/seatlayer/channels.rb +130 -0
- data/lib/seatlayer/http_client.rb +38 -20
- data/lib/seatlayer/inventory.rb +112 -34
- data/lib/seatlayer/resources.rb +65 -16
- data/lib/seatlayer/version.rb +1 -1
- data/lib/seatlayer.rb +9 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07a3046d14b181d770f7dddf673476c7b0d695d31d725adc042555afa145c144
|
|
4
|
+
data.tar.gz: 5923f05115c9741d3ab20ac8598616351eeac43d74a91b5da0c590275f0105b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9b16c52a2dad95aab45a2d0d75dad7be100ddb60497b554ce9adc1a86dacdab4e8a6dd273314ba67dd93a7156eebb5984550ebf9a5ad8c2cdc519a356ccf70a
|
|
7
|
+
data.tar.gz: 0fbe576e636045d4cdfc6ff3cffcbd16403fc3e595913832d352bdcebe66cc03fc128dcc2ab7bca3fc490558e099c77838a4fe6a88d54f66305c9a7928ea0ea8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- **Security/reliability:** Mutations now default to a single attempt. Automatic header-replay
|
|
6
|
+
retries are limited to chart create/copy, event create, and workspace create, preventing
|
|
7
|
+
transient failures from duplicating holds or best-available results and from issuing extra
|
|
8
|
+
show-once credentials.
|
|
9
|
+
- Aligned event, inventory, webhook, manage-session, and Designer requests with the generated
|
|
10
|
+
public contract: event metadata, chart-update acknowledgement, trusted hold extension,
|
|
11
|
+
scheduled block release, hold-TTL reset, webhook envelopes and delivery filters, and Designer
|
|
12
|
+
safe-mode/feature-policy fields are now represented directly.
|
|
13
|
+
- Removed unsupported `state` and `cursor` keywords from buyer-access-session listing; the route
|
|
14
|
+
supports only `limit`.
|
|
15
|
+
- Added deterministic transport-contract coverage for stable error-code fallback, HTTP status,
|
|
16
|
+
decoded body and `X-Request-ID` exposure, typed 429 `Retry-After` precedence, non-JSON gateway
|
|
17
|
+
failures, and single-attempt unsafe mutations.
|
|
18
|
+
- Reached all 71 public operation wrappers with raw event-poster upload/removal and the complete
|
|
19
|
+
hosted access-link lifecycle. One-time capability reveals remain single-attempt.
|
|
20
|
+
- Added chart copy/metadata overrides, event-log pagination filters, and explicit-null support for
|
|
21
|
+
buyer-session and workspace fields.
|
|
22
|
+
|
|
23
|
+
## 0.2.0 — 2026-08-12
|
|
24
|
+
|
|
25
|
+
- Added the `channels` resource for allocation management, access previews,
|
|
26
|
+
channel reporting, and origin-bound buyer access sessions.
|
|
27
|
+
- Added inventory booking lifecycle reads and channel-aware hold/book options.
|
|
28
|
+
- Require a non-empty stable booking reference for booking, unbooking, and
|
|
29
|
+
booking-detail reads.
|
|
30
|
+
- Expanded registry, documentation, and agent-discovery links in the README.
|
|
31
|
+
|
|
3
32
|
## 0.1.0 — 2026-08-04
|
|
4
33
|
|
|
5
34
|
First public release: RubyGems `seatlayer`.
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# SeatLayer Ruby SDK
|
|
2
2
|
|
|
3
|
+
[](https://github.com/seatlayer/seatlayer-ruby/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/seatlayer)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
3
8
|
Official Ruby server SDK for the [SeatLayer](https://seatlayer.io) reserved-seating API.
|
|
4
9
|
|
|
5
10
|
> **Server-side only.** This gem authenticates with your secret key. Never load it anywhere a
|
|
@@ -38,6 +43,9 @@ held = client.inventory.hold_best_available(event["key"], qty: 4)
|
|
|
38
43
|
client.inventory.book(event["key"], hold_id: held["holdId"], booking_ref: "order-8842")
|
|
39
44
|
```
|
|
40
45
|
|
|
46
|
+
Nullable event-create fields distinguish omission from an explicit reset: passing, for example,
|
|
47
|
+
`venue: nil` sends JSON `null`; leaving `venue` out sends no field.
|
|
48
|
+
|
|
41
49
|
## Test vs live
|
|
42
50
|
|
|
43
51
|
Keys carry their own mode. `sk_test_…` keys can only touch test-mode events and `sk_live_…` only
|
|
@@ -74,6 +82,27 @@ client.inventory.book_best_available(event_key, qty: 2, booking_ref: "phone-1183
|
|
|
74
82
|
client.inventory.box_office_book(event_key, labels: ["A-1", "A-2"], booking_ref: "comp-14")
|
|
75
83
|
```
|
|
76
84
|
|
|
85
|
+
## Private and partner sales
|
|
86
|
+
|
|
87
|
+
Channels split event inventory into explicit allocations. A channel id is
|
|
88
|
+
reporting/routing metadata, not browser authority. Authenticate the buyer in
|
|
89
|
+
your backend and mint a short-lived token restricted to the event, origin, and
|
|
90
|
+
allowed allocations:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
access = client.channels.create_buyer_access_session(
|
|
94
|
+
event_key,
|
|
95
|
+
channel_ids: ["chn_partner_a"],
|
|
96
|
+
include_public: false,
|
|
97
|
+
allowed_origin: "https://tickets.example"
|
|
98
|
+
)
|
|
99
|
+
# Return access["token"] to the in-memory buyerAccessTokenProvider only.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Never log or persist the returned `bse_…` bearer. Allocation setup, previews,
|
|
103
|
+
pause/archive controls, audit-safe session listing, and channel reports are on
|
|
104
|
+
`client.channels`.
|
|
105
|
+
|
|
77
106
|
## Listing and pagination
|
|
78
107
|
|
|
79
108
|
`list` returns one page plus a `nextCursor`. `list_all` pages for you and returns a lazy
|
|
@@ -129,11 +158,9 @@ session = client.sessions.create_manage_session(
|
|
|
129
158
|
)
|
|
130
159
|
```
|
|
131
160
|
|
|
132
|
-
`capabilities` is **required** by this SDK even though the API defaults
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
payment gateway**. That is real money, moved by a token you handed to a browser; it should not
|
|
136
|
-
arrive by forgetting an argument. Grant the smallest set the page needs.
|
|
161
|
+
`capabilities` is **required** by this SDK even though the raw API safely defaults an omitted list
|
|
162
|
+
to view-only (`event:view`). Keeping the argument required makes browser authority visible at every
|
|
163
|
+
call site. Grant the smallest set the page needs.
|
|
137
164
|
|
|
138
165
|
The full set, all opt-in:
|
|
139
166
|
|
|
@@ -145,13 +172,27 @@ The full set, all opt-in:
|
|
|
145
172
|
| `event:reports` | Read sales and availability reports |
|
|
146
173
|
| `event:channels:view` | Read sales channels and their allocations |
|
|
147
174
|
| `event:channels:manage` | Create, pause and archive channels; rotate access links |
|
|
175
|
+
| `event:orders:read` | Read SeatLayer-managed orders |
|
|
176
|
+
| `event:refund` | Refund a SeatLayer-managed order |
|
|
177
|
+
| `event:tickets:send` | Send SeatLayer-managed tickets |
|
|
178
|
+
| `event:door:view` | Read the door list |
|
|
179
|
+
| `event:door:checkin` | Check tickets in and out |
|
|
180
|
+
| `event:boxoffice` | Use the managed box-office surface |
|
|
148
181
|
|
|
149
182
|
The two `event:channels:*` capabilities are **not** in the default — a token minted before sales
|
|
150
183
|
channels existed must not silently acquire channel authority — so ask for them explicitly if the
|
|
151
184
|
page manages channels.
|
|
152
185
|
|
|
186
|
+
Designer minting returns the API envelope unchanged: read the token and effective safe-mode and
|
|
187
|
+
feature policy under `result["session"]`. Pass `safe_mode_options` only with `mode: "safe"`.
|
|
188
|
+
|
|
153
189
|
## Webhooks
|
|
154
190
|
|
|
191
|
+
Subscription responses use the wire envelopes exactly: `list` returns `{"subs" => [...]}`,
|
|
192
|
+
`create` returns `{"sub" => ..., "secret" => ...}` (the secret is shown once), and `update`
|
|
193
|
+
returns `{"sub" => ...}`. `SeatLayer::Webhooks::EVENT_NAMES` is the exact eight-name event set;
|
|
194
|
+
delivery history accepts `limit`, `status` (`"ok"` or `"failed"`), and `before`.
|
|
195
|
+
|
|
155
196
|
Verify every delivery against the **raw** body. Re-encoding a parsed Hash changes the bytes and
|
|
156
197
|
verification will fail.
|
|
157
198
|
|
|
@@ -210,15 +251,20 @@ error carries `status`, `code`, `body` and `request_id` — quote the request id
|
|
|
210
251
|
|
|
211
252
|
## Reliability
|
|
212
253
|
|
|
213
|
-
**Retries.** 429, 408 and 5xx
|
|
214
|
-
wins when the server sends it.
|
|
254
|
+
**Retries.** Reads (`GET`/`HEAD`) retry 429, 408 and 5xx with exponential backoff and full jitter;
|
|
255
|
+
`Retry-After` wins when the server sends it. Automatic mutation retries are limited to the four
|
|
256
|
+
operations backed by exact response replay: `charts.create`, `charts.copy`, `events.create`, and
|
|
257
|
+
`workspaces.create`. Other 4xx responses are never retried.
|
|
215
258
|
|
|
216
|
-
**Idempotency.**
|
|
217
|
-
one
|
|
218
|
-
|
|
259
|
+
**Idempotency.** Those four replay-backed operations carry an `Idempotency-Key`, generated when you
|
|
260
|
+
do not supply one and reused across attempts. Other mutations are single-attempt and receive no
|
|
261
|
+
automatic key. A caller-supplied key is forwarded but does not enable retries. This includes
|
|
262
|
+
inventory holds and bookings, show-once credential or secret creation, unsupported operations, and
|
|
263
|
+
raw `request` mutations. Keep `booking_ref` in the booking body for reconciliation, but handle an
|
|
264
|
+
unknown network outcome explicitly instead of automatically repeating the sale.
|
|
219
265
|
|
|
220
266
|
```ruby
|
|
221
|
-
client.
|
|
267
|
+
client.events.create(chart_id: chart_id, idempotency_key: "provision-event-#{event_id}")
|
|
222
268
|
```
|
|
223
269
|
|
|
224
270
|
```ruby
|
|
@@ -231,7 +277,8 @@ SeatLayer::Client.new(
|
|
|
231
277
|
|
|
232
278
|
## Escape hatch
|
|
233
279
|
|
|
234
|
-
For surface this SDK does not wrap yet
|
|
280
|
+
For surface this SDK does not wrap yet, `request` keeps auth and error mapping. Raw reads retain the
|
|
281
|
+
read retry policy; raw mutations are always single-attempt because their replay contract is unknown:
|
|
235
282
|
|
|
236
283
|
```ruby
|
|
237
284
|
client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 })
|
|
@@ -242,8 +289,9 @@ client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 })
|
|
|
242
289
|
| Resource | Methods |
|
|
243
290
|
| --- | --- |
|
|
244
291
|
| `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
|
|
245
|
-
| `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` |
|
|
246
|
-
| `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` |
|
|
292
|
+
| `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` |
|
|
293
|
+
| `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` |
|
|
294
|
+
| `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` |
|
|
247
295
|
| `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
|
|
248
296
|
| `webhooks` | `list` `create` `update` `delete` `list_deliveries` |
|
|
249
297
|
| `workspaces` | `list` `create` `retrieve` `update` |
|
|
@@ -273,24 +321,25 @@ the public manifest, not just from the wrapper.
|
|
|
273
321
|
- [Webhook verification](https://docs.seatlayer.io/server-sdk/webhooks/)
|
|
274
322
|
- [Server API reference](https://docs.seatlayer.io/server-api/events/)
|
|
275
323
|
- [OpenAPI description](https://docs.seatlayer.io/openapi.json)
|
|
324
|
+
- [Agent-readable documentation](https://docs.seatlayer.io/llms.txt)
|
|
276
325
|
- [SeatLayer GitHub organization](https://github.com/seatlayer)
|
|
277
326
|
|
|
278
327
|
### Other SeatLayer SDKs
|
|
279
328
|
|
|
280
329
|
| Surface | Package |
|
|
281
330
|
|---|---|
|
|
282
|
-
| Browser (vanilla) | [`@seatlayer/js`](https://
|
|
283
|
-
| React | [`@seatlayer/react`](https://
|
|
284
|
-
| React Native | [`@seatlayer/react-native`](https://
|
|
331
|
+
| Browser (vanilla) | [`@seatlayer/js`](https://www.npmjs.com/package/@seatlayer/js) |
|
|
332
|
+
| React | [`@seatlayer/react`](https://www.npmjs.com/package/@seatlayer/react) |
|
|
333
|
+
| React Native | [`@seatlayer/react-native`](https://www.npmjs.com/package/@seatlayer/react-native) |
|
|
285
334
|
| iOS | [`seatlayer-ios`](https://github.com/seatlayer/seatlayer-ios) |
|
|
286
335
|
| Android | [`seatlayer-android`](https://github.com/seatlayer/seatlayer-android) |
|
|
287
|
-
| Flutter | [`
|
|
288
|
-
| Node.js (server) | [`@seatlayer/server`](https://
|
|
289
|
-
| Python (server) | [`seatlayer`](https://
|
|
290
|
-
| PHP (server) | [`seatlayer/seatlayer-php`](https://
|
|
291
|
-
| Java (server) | [`io.seatlayer:seatlayer-java`](https://
|
|
292
|
-
| Go (server) | [`github.com/seatlayer/seatlayer-go`](https://github.com/seatlayer/seatlayer-go) |
|
|
293
|
-
| .NET (server) | [`SeatLayer`](https://
|
|
336
|
+
| Flutter | [`seatlayer`](https://pub.dev/packages/seatlayer) |
|
|
337
|
+
| Node.js (server) | [`@seatlayer/server`](https://www.npmjs.com/package/@seatlayer/server) |
|
|
338
|
+
| Python (server) | [`seatlayer`](https://pypi.org/project/seatlayer/) |
|
|
339
|
+
| PHP (server) | [`seatlayer/seatlayer-php`](https://packagist.org/packages/seatlayer/seatlayer-php) |
|
|
340
|
+
| Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java/0.1.0) |
|
|
341
|
+
| Go (server) | [`github.com/seatlayer/seatlayer-go`](https://pkg.go.dev/github.com/seatlayer/seatlayer-go) |
|
|
342
|
+
| .NET (server) | [`SeatLayer`](https://www.nuget.org/packages/SeatLayer) |
|
|
294
343
|
|
|
295
344
|
## Development
|
|
296
345
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SeatLayer
|
|
4
|
+
# Private allocations, reporting, and origin-bound buyer access.
|
|
5
|
+
class Channels < Resource
|
|
6
|
+
def list_channels(event_key, include_archived: false)
|
|
7
|
+
query = include_archived ? { "includeArchived" => "1" } : nil
|
|
8
|
+
@client.get(path(event_key), query)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create_channel(event_key, name:, color: nil, marker: nil, external_ref: nil,
|
|
12
|
+
access_intent: nil, reason: nil, idempotency_key: nil)
|
|
13
|
+
body = compact({ "name" => name, "color" => color, "marker" => marker,
|
|
14
|
+
"externalRef" => external_ref, "accessIntent" => access_intent,
|
|
15
|
+
"reason" => reason })
|
|
16
|
+
@client.post(path(event_key), body, idempotency_key: idempotency_key)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update_channel(event_key, channel_id, name: nil, access_intent: nil,
|
|
20
|
+
acknowledge_live_access: nil, reason: nil)
|
|
21
|
+
body = compact({ "name" => name, "accessIntent" => access_intent,
|
|
22
|
+
"acknowledgeLiveAccess" => acknowledge_live_access, "reason" => reason })
|
|
23
|
+
@client.patch(path(event_key, "/#{encode(channel_id)}"), body)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update_assignments(event_key, labels:, assignment_version:, target_channel_id: nil,
|
|
27
|
+
reason: nil, idempotency_key: nil)
|
|
28
|
+
body = compact({ "targetChannelId" => target_channel_id, "labels" => labels,
|
|
29
|
+
"assignmentVersion" => assignment_version, "reason" => reason })
|
|
30
|
+
body["targetChannelId"] = nil if target_channel_id.nil?
|
|
31
|
+
@client.post(path(event_key, "/assignments"), body, idempotency_key: idempotency_key)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list_allocation(event_key, after_label: nil, limit: nil)
|
|
35
|
+
@client.get(path(event_key, "/allocation"), compact({ "afterLabel" => after_label, "limit" => limit }))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def retrieve_access_preview(event_key, channel_ids: nil, include_public: nil)
|
|
39
|
+
include_public_value = if include_public.nil?
|
|
40
|
+
nil
|
|
41
|
+
else
|
|
42
|
+
include_public ? "1" : "0"
|
|
43
|
+
end
|
|
44
|
+
query = compact({ "channelIds" => channel_ids&.join(","),
|
|
45
|
+
"includePublic" => include_public_value })
|
|
46
|
+
@client.get(path(event_key, "/preview"), query)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def retrieve_report(event_key) = @client.get(path(event_key, "/report"))
|
|
50
|
+
|
|
51
|
+
def pause(event_key, channel_id, reason: nil)
|
|
52
|
+
@client.post(path(event_key, "/#{encode(channel_id)}/pause"), compact({ "reason" => reason }))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def unpause(event_key, channel_id, reason: nil)
|
|
56
|
+
@client.post(path(event_key, "/#{encode(channel_id)}/unpause"), compact({ "reason" => reason }))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def archive(event_key, channel_id, destination:, reason: nil)
|
|
60
|
+
body = compact({ "reason" => reason })
|
|
61
|
+
body["destination"] = destination
|
|
62
|
+
@client.post(path(event_key, "/#{encode(channel_id)}/archive"), body)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Explicit keywords document each security boundary carried by the token.
|
|
66
|
+
# rubocop:disable Metrics/ParameterLists
|
|
67
|
+
def create_buyer_access_session(event_key, include_public:, allowed_origin:, channel_ids: nil,
|
|
68
|
+
expires_in_seconds: nil, max_quantity: UNSET, buyer_ref: UNSET,
|
|
69
|
+
partner_ref: UNSET, client_request_id: UNSET, idempotency_key: nil)
|
|
70
|
+
body = compact({ "channelIds" => channel_ids, "includePublic" => include_public,
|
|
71
|
+
"allowedOrigin" => allowed_origin, "expiresInSeconds" => expires_in_seconds })
|
|
72
|
+
body.merge!(supplied({ "maxQuantity" => max_quantity, "buyerRef" => buyer_ref,
|
|
73
|
+
"partnerRef" => partner_ref,
|
|
74
|
+
"clientRequestId" => client_request_id }))
|
|
75
|
+
@client.post("/v1/events/#{encode(event_key)}/buyer-access-sessions", body,
|
|
76
|
+
idempotency_key: idempotency_key)
|
|
77
|
+
end
|
|
78
|
+
# rubocop:enable Metrics/ParameterLists
|
|
79
|
+
|
|
80
|
+
def list_buyer_access_sessions(event_key, limit: nil)
|
|
81
|
+
@client.get("/v1/events/#{encode(event_key)}/buyer-access-sessions",
|
|
82
|
+
compact({ "limit" => limit }))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def revoke_buyer_access_session(event_key, session_id)
|
|
86
|
+
@client.delete("/v1/events/#{encode(event_key)}/buyer-access-sessions/#{encode(session_id)}")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The URL and capability in this response are revealed once. Persist them
|
|
90
|
+
# immediately; this mutation is deliberately never retried automatically.
|
|
91
|
+
# rubocop:disable Metrics/ParameterLists
|
|
92
|
+
def create_access_link(event_key, channel_id, label: UNSET, expires_at: nil,
|
|
93
|
+
max_redemptions: nil, max_quantity: nil,
|
|
94
|
+
session_ttl_seconds: nil, include_public: nil, reason: nil,
|
|
95
|
+
idempotency_key: nil)
|
|
96
|
+
body = compact({ "expiresAt" => expires_at, "maxRedemptions" => max_redemptions,
|
|
97
|
+
"maxQuantity" => max_quantity,
|
|
98
|
+
"sessionTtlSeconds" => session_ttl_seconds,
|
|
99
|
+
"includePublic" => include_public, "reason" => reason })
|
|
100
|
+
body.merge!(supplied({ "label" => label }))
|
|
101
|
+
@client.post(path(event_key, "/#{encode(channel_id)}/access-links"), body,
|
|
102
|
+
idempotency_key: idempotency_key)
|
|
103
|
+
end
|
|
104
|
+
# rubocop:enable Metrics/ParameterLists
|
|
105
|
+
|
|
106
|
+
# Status only: the API never returns a previously revealed capability.
|
|
107
|
+
def list_access_links(event_key, channel_id)
|
|
108
|
+
@client.get(path(event_key, "/#{encode(channel_id)}/access-links"))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def rotate_access_link(event_key, channel_id, link_id, end_active_sessions:, reason: nil)
|
|
112
|
+
body = compact({ "endActiveSessions" => end_active_sessions, "reason" => reason })
|
|
113
|
+
@client.post(
|
|
114
|
+
path(event_key, "/#{encode(channel_id)}/access-links/#{encode(link_id)}/rotate"), body
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def revoke_access_link(event_key, channel_id, link_id, end_active_sessions: false, reason: nil)
|
|
119
|
+
query = compact({ "endActiveSessions" => end_active_sessions ? "1" : nil,
|
|
120
|
+
"reason" => reason })
|
|
121
|
+
@client.delete(
|
|
122
|
+
path(event_key, "/#{encode(channel_id)}/access-links/#{encode(link_id)}"), query
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def path(event_key, suffix = "") = "/v1/events/#{encode(event_key)}/channels#{suffix}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -62,7 +62,8 @@ module SeatLayer
|
|
|
62
62
|
URI.encode_www_form_component(segment.to_s).gsub("+", "%20")
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def request(method, path, query: nil, body: nil,
|
|
65
|
+
def request(method, path, query: nil, body: nil, raw_body: nil, content_type: nil,
|
|
66
|
+
idempotency_key: nil, retry_policy: :none)
|
|
66
67
|
url = @base_url + path
|
|
67
68
|
if query
|
|
68
69
|
pairs = query.compact
|
|
@@ -74,56 +75,73 @@ module SeatLayer
|
|
|
74
75
|
"Accept" => "application/json",
|
|
75
76
|
"User-Agent" => "seatlayer-ruby"
|
|
76
77
|
}
|
|
77
|
-
payload =
|
|
78
|
-
if body
|
|
79
|
-
payload = JSON.generate(body)
|
|
80
|
-
headers["Content-Type"] = "application/json"
|
|
81
|
-
end
|
|
78
|
+
payload = build_payload(body, raw_body, content_type, headers)
|
|
82
79
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
80
|
+
# Only operations with exact server-side response replay get an automatic
|
|
81
|
+
# key. A caller key on any other mutation is forwarded, but cannot opt that
|
|
82
|
+
# operation into automatic retries.
|
|
86
83
|
unless %w[GET HEAD].include?(method)
|
|
87
|
-
key = idempotency_key
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
key = idempotency_key
|
|
85
|
+
key ||= SecureRandom.uuid if retry_policy == :header_replay
|
|
86
|
+
if key
|
|
87
|
+
self.class.validate_idempotency_key!(key)
|
|
88
|
+
headers["Idempotency-Key"] = key
|
|
89
|
+
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
retry_allowed = %w[GET HEAD].include?(method) || retry_policy == :header_replay
|
|
93
|
+
execute(method, url, headers, payload, retry_allowed: retry_allowed)
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
def get(path, query = nil)
|
|
96
97
|
request("GET", path, query: query)
|
|
97
98
|
end
|
|
98
99
|
|
|
99
|
-
def post(path, body = nil, idempotency_key: nil)
|
|
100
|
-
request("POST", path, body: body, idempotency_key: idempotency_key)
|
|
100
|
+
def post(path, body = nil, idempotency_key: nil, retry_policy: :none)
|
|
101
|
+
request("POST", path, body: body, idempotency_key: idempotency_key, retry_policy: retry_policy)
|
|
101
102
|
end
|
|
102
103
|
|
|
103
104
|
def put(path, body)
|
|
104
105
|
request("PUT", path, body: body)
|
|
105
106
|
end
|
|
106
107
|
|
|
108
|
+
def put_raw(path, raw_body, content_type: "application/octet-stream")
|
|
109
|
+
request("PUT", path, raw_body: raw_body, content_type: content_type)
|
|
110
|
+
end
|
|
111
|
+
|
|
107
112
|
def patch(path, body)
|
|
108
113
|
request("PATCH", path, body: body)
|
|
109
114
|
end
|
|
110
115
|
|
|
111
|
-
def delete(path)
|
|
112
|
-
request("DELETE", path)
|
|
116
|
+
def delete(path, query = nil)
|
|
117
|
+
request("DELETE", path, query: query)
|
|
113
118
|
end
|
|
114
119
|
|
|
115
120
|
private
|
|
116
121
|
|
|
122
|
+
def build_payload(body, raw_body, content_type, headers)
|
|
123
|
+
raise ArgumentError, "body and raw_body are mutually exclusive" if !body.nil? && !raw_body.nil?
|
|
124
|
+
|
|
125
|
+
unless raw_body.nil?
|
|
126
|
+
headers["Content-Type"] = content_type || "application/octet-stream"
|
|
127
|
+
return raw_body
|
|
128
|
+
end
|
|
129
|
+
return if body.nil?
|
|
130
|
+
|
|
131
|
+
headers["Content-Type"] = "application/json"
|
|
132
|
+
JSON.generate(body)
|
|
133
|
+
end
|
|
134
|
+
|
|
117
135
|
# The retry loop, extracted from #request so each piece stays readable: the
|
|
118
136
|
# public method builds the call, this one decides how many times to make it.
|
|
119
|
-
def execute(method, url, headers, payload)
|
|
137
|
+
def execute(method, url, headers, payload, retry_allowed:)
|
|
120
138
|
last_error = nil
|
|
121
139
|
|
|
122
140
|
@max_retries.times do |attempt|
|
|
123
141
|
begin
|
|
124
142
|
response = send_request(method, url, headers, payload)
|
|
125
143
|
rescue ConnectionError => e
|
|
126
|
-
raise e
|
|
144
|
+
raise e unless retry_allowed && attempt < @max_retries - 1
|
|
127
145
|
|
|
128
146
|
last_error = e
|
|
129
147
|
sleep(backoff(attempt, nil))
|
|
@@ -136,7 +154,7 @@ module SeatLayer
|
|
|
136
154
|
error_body = decode_error_body(response[:body])
|
|
137
155
|
retry_after = parse_retry_after(response[:headers], error_body)
|
|
138
156
|
|
|
139
|
-
if retryable?(status) && attempt < @max_retries - 1
|
|
157
|
+
if retry_allowed && retryable?(status) && attempt < @max_retries - 1
|
|
140
158
|
sleep(backoff(attempt, status == 429 ? retry_after : nil))
|
|
141
159
|
next
|
|
142
160
|
end
|
data/lib/seatlayer/inventory.rb
CHANGED
|
@@ -11,10 +11,16 @@ module SeatLayer
|
|
|
11
11
|
# Never price from what the browser tells you. +retrieve_hold+ is the
|
|
12
12
|
# authoritative answer, which is why it is a separate call.
|
|
13
13
|
class Inventory < Resource
|
|
14
|
+
# Explicit keywords keep allocation authority visible at every sale call.
|
|
15
|
+
# rubocop:disable Metrics/ParameterLists
|
|
14
16
|
def hold(event_key, labels: nil, selections: nil, ttl_ms: nil,
|
|
15
|
-
replace_hold_id: nil,
|
|
17
|
+
replace_hold_id: nil, channel_ids: nil, ignore_channel_restrictions: nil,
|
|
18
|
+
reason: nil, idempotency_key: nil)
|
|
16
19
|
body = compact({ "labels" => labels, "selections" => selections,
|
|
17
|
-
"ttlMs" => ttl_ms, "replaceHoldId" => replace_hold_id
|
|
20
|
+
"ttlMs" => ttl_ms, "replaceHoldId" => replace_hold_id,
|
|
21
|
+
"channelIds" => channel_ids,
|
|
22
|
+
"ignoreChannelRestrictions" => ignore_channel_restrictions,
|
|
23
|
+
"reason" => reason })
|
|
18
24
|
@client.post(path(event_key, "/hold"), body, idempotency_key: idempotency_key)
|
|
19
25
|
end
|
|
20
26
|
|
|
@@ -24,9 +30,13 @@ module SeatLayer
|
|
|
24
30
|
# order get the same answer for the same inventory. A +qty+ above the server
|
|
25
31
|
# cap is clamped, not rejected.
|
|
26
32
|
def hold_best_available(event_key, qty:, category_key: nil, zone_id: nil,
|
|
27
|
-
ttl_ms: nil,
|
|
33
|
+
ttl_ms: nil, channel_ids: nil, ignore_channel_restrictions: nil,
|
|
34
|
+
reason: nil, idempotency_key: nil)
|
|
28
35
|
body = compact({ "qty" => qty, "categoryKey" => category_key,
|
|
29
|
-
"zoneId" => zone_id, "ttlMs" => ttl_ms
|
|
36
|
+
"zoneId" => zone_id, "ttlMs" => ttl_ms,
|
|
37
|
+
"channelIds" => channel_ids,
|
|
38
|
+
"ignoreChannelRestrictions" => ignore_channel_restrictions,
|
|
39
|
+
"reason" => reason })
|
|
30
40
|
@client.post(path(event_key, "/best-available"), body, idempotency_key: idempotency_key)
|
|
31
41
|
end
|
|
32
42
|
|
|
@@ -35,11 +45,16 @@ module SeatLayer
|
|
|
35
45
|
# Prefer this over hold-then-book when payment is already taken: a failure
|
|
36
46
|
# between two calls would strand inventory until the TTL expired.
|
|
37
47
|
def book_best_available(event_key, qty:, booking_ref:, category_key: nil,
|
|
38
|
-
zone_id: nil,
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
zone_id: nil, channel_ids: nil, ignore_channel_restrictions: nil,
|
|
49
|
+
reason: nil, idempotency_key: nil)
|
|
50
|
+
body = compact({ "qty" => qty, "bookingRef" => normalise_booking_ref(booking_ref),
|
|
51
|
+
"categoryKey" => category_key, "zoneId" => zone_id,
|
|
52
|
+
"channelIds" => channel_ids,
|
|
53
|
+
"ignoreChannelRestrictions" => ignore_channel_restrictions,
|
|
54
|
+
"reason" => reason })
|
|
41
55
|
@client.post(path(event_key, "/best-available-book"), body, idempotency_key: idempotency_key)
|
|
42
56
|
end
|
|
57
|
+
# rubocop:enable Metrics/ParameterLists
|
|
43
58
|
|
|
44
59
|
# Push an active hold's expiry out by a fresh window before it lapses.
|
|
45
60
|
#
|
|
@@ -47,8 +62,13 @@ module SeatLayer
|
|
|
47
62
|
# the checkout window — invoiced sales, a phone order on hold. Releasing
|
|
48
63
|
# first hands the seats to whoever is racing for them in between. A hold that
|
|
49
64
|
# is gone, expired, or at its renewal cap answers 409 +cannot_extend+.
|
|
50
|
-
def extend_hold(event_key, hold_id, ttl_ms: nil
|
|
51
|
-
|
|
65
|
+
def extend_hold(event_key, hold_id, ttl_ms: nil, channel_ids: nil,
|
|
66
|
+
ignore_channel_restrictions: nil, reason: nil)
|
|
67
|
+
body = compact({ "holdId" => hold_id, "ttlMs" => ttl_ms,
|
|
68
|
+
"channelIds" => channel_ids,
|
|
69
|
+
"ignoreChannelRestrictions" => ignore_channel_restrictions,
|
|
70
|
+
"reason" => reason })
|
|
71
|
+
@client.post(path(event_key, "/extend"), body)
|
|
52
72
|
end
|
|
53
73
|
|
|
54
74
|
# Authoritative items and prices. Charge from this, not the browser.
|
|
@@ -60,25 +80,32 @@ module SeatLayer
|
|
|
60
80
|
@client.post(path(event_key, "/release"), { "labels" => labels, "holdId" => hold_id })
|
|
61
81
|
end
|
|
62
82
|
|
|
63
|
-
def book(event_key, hold_id: nil, labels: nil, booking_ref: nil,
|
|
64
|
-
|
|
83
|
+
def book(event_key, hold_id: nil, labels: nil, booking_ref: nil, channel_ids: nil,
|
|
84
|
+
ignore_channel_restrictions: nil, reason: nil, idempotency_key: nil)
|
|
85
|
+
body = compact({ "holdId" => hold_id, "labels" => labels,
|
|
86
|
+
"bookingRef" => normalise_booking_ref(booking_ref),
|
|
87
|
+
"channelIds" => channel_ids,
|
|
88
|
+
"ignoreChannelRestrictions" => ignore_channel_restrictions,
|
|
89
|
+
"reason" => reason })
|
|
65
90
|
@client.post(path(event_key, "/book"), body, idempotency_key: idempotency_key)
|
|
66
91
|
end
|
|
67
92
|
|
|
68
93
|
def box_office_book(event_key, labels:, booking_ref:, idempotency_key: nil)
|
|
69
94
|
@client.post(path(event_key, "/box-book"),
|
|
70
|
-
{ "labels" => labels, "bookingRef" => booking_ref },
|
|
95
|
+
{ "labels" => labels, "bookingRef" => normalise_booking_ref(booking_ref) },
|
|
71
96
|
idempotency_key: idempotency_key)
|
|
72
97
|
end
|
|
73
98
|
|
|
74
99
|
# Reverse a booking. Requires a key with cancel authority.
|
|
75
|
-
def unbook(event_key, labels:)
|
|
76
|
-
@client.post(path(event_key, "/unbook"),
|
|
100
|
+
def unbook(event_key, labels:, booking_ref:)
|
|
101
|
+
@client.post(path(event_key, "/unbook"),
|
|
102
|
+
{ "labels" => labels, "bookingRef" => normalise_booking_ref(booking_ref) })
|
|
77
103
|
end
|
|
78
104
|
|
|
79
105
|
# Hold inventory back from sale (house seats, production holds).
|
|
80
|
-
def block(event_key, labels:)
|
|
81
|
-
@client.post(path(event_key, "/block"),
|
|
106
|
+
def block(event_key, labels:, release_at: nil)
|
|
107
|
+
@client.post(path(event_key, "/block"),
|
|
108
|
+
compact({ "labels" => labels, "releaseAt" => release_at }))
|
|
82
109
|
end
|
|
83
110
|
|
|
84
111
|
def unblock(event_key, labels:)
|
|
@@ -97,11 +124,28 @@ module SeatLayer
|
|
|
97
124
|
@client.post(path(event_key, "/availability"), fields)
|
|
98
125
|
end
|
|
99
126
|
|
|
127
|
+
# One page of inventory booking lifecycles, newest first.
|
|
128
|
+
def list_bookings(event_key, query: nil, state: nil, limit: nil, cursor: nil)
|
|
129
|
+
@client.get(path(event_key, "/bookings"),
|
|
130
|
+
compact({ "q" => query, "state" => state, "limit" => limit, "cursor" => cursor }))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def retrieve_booking(event_key, booking_ref)
|
|
134
|
+
@client.get(path(event_key, "/bookings/#{encode(normalise_booking_ref(booking_ref))}"))
|
|
135
|
+
end
|
|
136
|
+
|
|
100
137
|
private
|
|
101
138
|
|
|
102
139
|
def path(event_key, suffix)
|
|
103
140
|
"/v1/events/#{encode(event_key)}#{suffix}"
|
|
104
141
|
end
|
|
142
|
+
|
|
143
|
+
def normalise_booking_ref(booking_ref)
|
|
144
|
+
value = booking_ref&.strip
|
|
145
|
+
return value unless value.nil? || value.empty?
|
|
146
|
+
|
|
147
|
+
raise ArgumentError, "booking_ref is required and must be a non-empty stable reference"
|
|
148
|
+
end
|
|
105
149
|
end
|
|
106
150
|
|
|
107
151
|
# Short-lived, origin-bound browser tokens.
|
|
@@ -109,24 +153,29 @@ module SeatLayer
|
|
|
109
153
|
# The governing rule: the SDK mints tokens, widgets consume them. Your secret
|
|
110
154
|
# key never reaches a browser.
|
|
111
155
|
class Sessions < Resource
|
|
112
|
-
CAPABILITIES = [
|
|
156
|
+
CAPABILITIES = [
|
|
157
|
+
"event:view", "event:block", "event:cancel", "event:reports",
|
|
158
|
+
"event:channels:view", "event:channels:manage", "event:orders:read",
|
|
159
|
+
"event:refund", "event:tickets:send", "event:door:view",
|
|
160
|
+
"event:door:checkin", "event:boxoffice"
|
|
161
|
+
].freeze
|
|
113
162
|
|
|
114
163
|
# Mint a manage-session token for the control room.
|
|
115
164
|
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def create_manage_session(event_key, allowed_origin:, capabilities:, expires_in_seconds: nil)
|
|
165
|
+
# The raw API defaults an omitted list to view-only (+event:view+). This SDK
|
|
166
|
+
# still requires an explicit set so browser authority is visible at each call.
|
|
167
|
+
def create_manage_session(event_key, allowed_origin:, capabilities:, expires_in_seconds: nil,
|
|
168
|
+
workspace_id: nil)
|
|
121
169
|
if capabilities.nil? || capabilities.empty?
|
|
122
170
|
raise ArgumentError,
|
|
123
|
-
|
|
124
|
-
'["event:view"]. Omitting it server-side grants event:cancel, ' \
|
|
125
|
-
"which can reverse paid bookings."
|
|
171
|
+
'capabilities is required: pass the smallest set the page needs, e.g. ["event:view"].'
|
|
126
172
|
end
|
|
173
|
+
unknown = capabilities - CAPABILITIES
|
|
174
|
+
raise ArgumentError, "unsupported manage capabilities: #{unknown.join(", ")}" unless unknown.empty?
|
|
127
175
|
|
|
128
176
|
body = compact({ "allowedOrigin" => allowed_origin, "capabilities" => capabilities,
|
|
129
|
-
"expiresInSeconds" => expires_in_seconds
|
|
177
|
+
"expiresInSeconds" => expires_in_seconds,
|
|
178
|
+
"workspaceId" => workspace_id })
|
|
130
179
|
@client.post("/v1/events/#{encode(event_key)}/manage-sessions", body)
|
|
131
180
|
end
|
|
132
181
|
|
|
@@ -136,13 +185,19 @@ module SeatLayer
|
|
|
136
185
|
|
|
137
186
|
# Mint a designer token so an organiser can edit a chart inside your own UI.
|
|
138
187
|
# Requires a chart id that already exists — create or copy one first.
|
|
188
|
+
# Explicit keywords keep each security and feature-policy boundary visible.
|
|
189
|
+
# rubocop:disable Metrics/ParameterLists
|
|
139
190
|
def create_designer_session(workspace_id:, chart_id:, allowed_origin:,
|
|
140
|
-
authority: nil,
|
|
191
|
+
authority: nil, can_publish: nil, mode: nil,
|
|
192
|
+
safe_mode_options: nil, features: nil, expires_in_seconds: nil)
|
|
141
193
|
body = compact({ "workspaceId" => workspace_id, "chartId" => chart_id,
|
|
142
194
|
"allowedOrigin" => allowed_origin, "authority" => authority,
|
|
143
|
-
"
|
|
195
|
+
"canPublish" => can_publish, "mode" => mode,
|
|
196
|
+
"safeModeOptions" => safe_mode_options, "features" => features,
|
|
197
|
+
"expiresInSeconds" => expires_in_seconds })
|
|
144
198
|
@client.post("/v1/designer/sessions", body)
|
|
145
199
|
end
|
|
200
|
+
# rubocop:enable Metrics/ParameterLists
|
|
146
201
|
|
|
147
202
|
def revoke_designer_session(session_id)
|
|
148
203
|
@client.delete("/v1/designer/sessions/#{encode(session_id)}")
|
|
@@ -151,15 +206,23 @@ module SeatLayer
|
|
|
151
206
|
|
|
152
207
|
# Manage webhook subscriptions. To VERIFY a delivery, see SeatLayer::Webhook.
|
|
153
208
|
class Webhooks < Resource
|
|
209
|
+
EVENT_NAMES = [
|
|
210
|
+
"seat.booked", "seat.released", "seat.blocked", "hold.expired",
|
|
211
|
+
"hold.created", "hold.extended", "event.created", "event.soldout"
|
|
212
|
+
].freeze
|
|
213
|
+
|
|
154
214
|
def list
|
|
155
215
|
@client.get("/v1/webhooks")
|
|
156
216
|
end
|
|
157
217
|
|
|
158
218
|
def create(url:, events:)
|
|
219
|
+
validate_events!(events)
|
|
159
220
|
@client.post("/v1/webhooks", { "url" => url, "events" => events })
|
|
160
221
|
end
|
|
161
222
|
|
|
162
|
-
def update(webhook_id,
|
|
223
|
+
def update(webhook_id, url: nil, events: nil, disabled: nil)
|
|
224
|
+
validate_events!(events) unless events.nil?
|
|
225
|
+
fields = compact({ "url" => url, "events" => events, "disabled" => disabled })
|
|
163
226
|
@client.patch("/v1/webhooks/#{encode(webhook_id)}", fields)
|
|
164
227
|
end
|
|
165
228
|
|
|
@@ -167,8 +230,20 @@ module SeatLayer
|
|
|
167
230
|
@client.delete("/v1/webhooks/#{encode(webhook_id)}")
|
|
168
231
|
end
|
|
169
232
|
|
|
170
|
-
def list_deliveries(webhook_id)
|
|
171
|
-
|
|
233
|
+
def list_deliveries(webhook_id, limit: nil, status: nil, before: nil)
|
|
234
|
+
raise ArgumentError, "status must be ok or failed" unless status.nil? || %w[ok failed].include?(status)
|
|
235
|
+
|
|
236
|
+
query = compact({ "limit" => limit, "status" => status, "before" => before })
|
|
237
|
+
@client.get("/v1/webhooks/#{encode(webhook_id)}/deliveries", query)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def validate_events!(events)
|
|
243
|
+
unknown = Array(events) - EVENT_NAMES
|
|
244
|
+
return if !Array(events).empty? && unknown.empty?
|
|
245
|
+
|
|
246
|
+
raise ArgumentError, "events must contain only supported SeatLayer webhook event names"
|
|
172
247
|
end
|
|
173
248
|
end
|
|
174
249
|
|
|
@@ -178,9 +253,12 @@ module SeatLayer
|
|
|
178
253
|
@client.get("/v1/workspaces")
|
|
179
254
|
end
|
|
180
255
|
|
|
181
|
-
def create(name:, external_ref:
|
|
182
|
-
body =
|
|
183
|
-
|
|
256
|
+
def create(name:, external_ref: UNSET, idempotency_key: nil)
|
|
257
|
+
body = { "name" => name }
|
|
258
|
+
body.merge!(supplied({ "externalRef" => external_ref }))
|
|
259
|
+
@client.post(
|
|
260
|
+
"/v1/workspaces", body, idempotency_key: idempotency_key, retry_policy: :header_replay
|
|
261
|
+
)
|
|
184
262
|
end
|
|
185
263
|
|
|
186
264
|
def retrieve(workspace_id)
|
data/lib/seatlayer/resources.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module SeatLayer
|
|
4
4
|
# Shared plumbing for the resource namespaces.
|
|
5
5
|
class Resource
|
|
6
|
+
UNSET = Object.new.freeze
|
|
7
|
+
|
|
6
8
|
def initialize(client)
|
|
7
9
|
@client = client
|
|
8
10
|
end
|
|
@@ -15,6 +17,11 @@ module SeatLayer
|
|
|
15
17
|
hash.compact
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
# Drop only the private sentinel, preserving nil as explicit JSON null.
|
|
21
|
+
def supplied(hash)
|
|
22
|
+
hash.reject { |_key, value| value.equal?(UNSET) }
|
|
23
|
+
end
|
|
24
|
+
|
|
18
25
|
def encode(segment)
|
|
19
26
|
HTTPClient.encode(segment)
|
|
20
27
|
end
|
|
@@ -58,14 +65,16 @@ module SeatLayer
|
|
|
58
65
|
def create(name:, doc: nil, external_ref: nil, workspace_id: nil, idempotency_key: nil)
|
|
59
66
|
body = compact({ "name" => name, "doc" => doc,
|
|
60
67
|
"externalRef" => external_ref, "workspaceId" => workspace_id })
|
|
61
|
-
@client.post(
|
|
68
|
+
@client.post(
|
|
69
|
+
"/v1/charts", body, idempotency_key: idempotency_key, retry_policy: :header_replay
|
|
70
|
+
)
|
|
62
71
|
end
|
|
63
72
|
|
|
64
73
|
def retrieve(chart_id)
|
|
65
74
|
@client.get("/v1/charts/#{encode(chart_id)}")
|
|
66
75
|
end
|
|
67
76
|
|
|
68
|
-
# Replace a chart document.
|
|
77
|
+
# Replace a chart document or update metadata only.
|
|
69
78
|
#
|
|
70
79
|
# +expected_updated_at+ is required for optimistic concurrency and is not
|
|
71
80
|
# optional here either: without it two concurrent writers silently overwrite
|
|
@@ -74,8 +83,20 @@ module SeatLayer
|
|
|
74
83
|
#
|
|
75
84
|
# The Designer is the authoring surface. Use this for bulk programmatic edits
|
|
76
85
|
# and migrations, not for drawing.
|
|
77
|
-
def update(chart_id, doc
|
|
78
|
-
|
|
86
|
+
def update(chart_id, doc: UNSET, expected_updated_at: UNSET, name: nil, issues: nil,
|
|
87
|
+
external_ref: UNSET)
|
|
88
|
+
doc_supplied = !doc.equal?(UNSET)
|
|
89
|
+
expected_supplied = !expected_updated_at.equal?(UNSET)
|
|
90
|
+
unless doc_supplied == expected_supplied
|
|
91
|
+
raise ArgumentError, "doc and expected_updated_at must be supplied together"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
body = compact({ "name" => name, "issues" => issues })
|
|
95
|
+
if doc_supplied
|
|
96
|
+
body["doc"] = doc
|
|
97
|
+
body["expectedUpdatedAt"] = expected_updated_at
|
|
98
|
+
end
|
|
99
|
+
body.merge!(supplied({ "externalRef" => external_ref }))
|
|
79
100
|
@client.put("/v1/charts/#{encode(chart_id)}", body)
|
|
80
101
|
end
|
|
81
102
|
|
|
@@ -84,8 +105,14 @@ module SeatLayer
|
|
|
84
105
|
end
|
|
85
106
|
|
|
86
107
|
# Copy a chart — the usual way to provision a venue from a template.
|
|
87
|
-
def copy(chart_id, idempotency_key: nil
|
|
88
|
-
|
|
108
|
+
def copy(chart_id, idempotency_key: nil, name: nil, external_ref: UNSET,
|
|
109
|
+
workspace_id: nil)
|
|
110
|
+
body = compact({ "name" => name, "workspaceId" => workspace_id })
|
|
111
|
+
body.merge!(supplied({ "externalRef" => external_ref }))
|
|
112
|
+
@client.post(
|
|
113
|
+
"/v1/charts/#{encode(chart_id)}/duplicate", body.empty? ? nil : body,
|
|
114
|
+
idempotency_key: idempotency_key, retry_policy: :header_replay
|
|
115
|
+
)
|
|
89
116
|
end
|
|
90
117
|
|
|
91
118
|
def archive(chart_id)
|
|
@@ -131,13 +158,22 @@ module SeatLayer
|
|
|
131
158
|
end
|
|
132
159
|
end
|
|
133
160
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
# rubocop:disable Metrics/ParameterLists
|
|
162
|
+
def create(chart_id:, name: nil, slug: nil, starts_at: UNSET, venue: UNSET,
|
|
163
|
+
external_ref: UNSET, currency: UNSET, idempotency_key: nil,
|
|
164
|
+
description: UNSET, ends_at: UNSET, timezone: UNSET, locale: UNSET,
|
|
165
|
+
poster_asset_id: UNSET, mode: nil)
|
|
166
|
+
body = compact({ "chartId" => chart_id, "name" => name, "slug" => slug, "mode" => mode })
|
|
167
|
+
body.merge!(supplied({ "startsAt" => starts_at, "venue" => venue,
|
|
168
|
+
"externalRef" => external_ref, "currency" => currency,
|
|
169
|
+
"description" => description, "endsAt" => ends_at,
|
|
170
|
+
"timezone" => timezone, "locale" => locale,
|
|
171
|
+
"posterAssetId" => poster_asset_id }))
|
|
172
|
+
@client.post(
|
|
173
|
+
"/v1/events", body, idempotency_key: idempotency_key, retry_policy: :header_replay
|
|
174
|
+
)
|
|
140
175
|
end
|
|
176
|
+
# rubocop:enable Metrics/ParameterLists
|
|
141
177
|
|
|
142
178
|
def retrieve(event_key)
|
|
143
179
|
@client.get("/v1/events/#{encode(event_key)}")
|
|
@@ -151,9 +187,20 @@ module SeatLayer
|
|
|
151
187
|
@client.delete("/v1/events/#{encode(event_key)}")
|
|
152
188
|
end
|
|
153
189
|
|
|
190
|
+
# Upload raw PNG, JPEG, or WebP bytes (maximum 5 MiB).
|
|
191
|
+
def update_poster(event_key, image, content_type: "application/octet-stream")
|
|
192
|
+
@client.put_raw("/v1/events/#{encode(event_key)}/poster", image, content_type: content_type)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def delete_poster(event_key)
|
|
196
|
+
@client.delete("/v1/events/#{encode(event_key)}/poster")
|
|
197
|
+
end
|
|
198
|
+
|
|
154
199
|
# Move a live event onto the latest published version of its chart.
|
|
155
|
-
def update_chart(event_key)
|
|
156
|
-
|
|
200
|
+
def update_chart(event_key, acknowledge_dropped_assignments: nil, reason: nil)
|
|
201
|
+
body = compact({ "acknowledgeDroppedAssignments" => acknowledge_dropped_assignments,
|
|
202
|
+
"reason" => reason })
|
|
203
|
+
@client.post("/v1/events/#{encode(event_key)}/update-chart", body)
|
|
157
204
|
end
|
|
158
205
|
|
|
159
206
|
# Stop buyer sales. Existing holds keep their TTL.
|
|
@@ -174,6 +221,7 @@ module SeatLayer
|
|
|
174
221
|
end
|
|
175
222
|
|
|
176
223
|
def update_hold_ttl(event_key, hold_ttl_ms)
|
|
224
|
+
# +nil+ restores the event default and must remain an explicit JSON null.
|
|
177
225
|
@client.post("/v1/events/#{encode(event_key)}/hold-ttl", { "holdTtlMs" => hold_ttl_ms })
|
|
178
226
|
end
|
|
179
227
|
|
|
@@ -181,8 +229,9 @@ module SeatLayer
|
|
|
181
229
|
@client.get("/v1/events/#{encode(event_key)}/report")
|
|
182
230
|
end
|
|
183
231
|
|
|
184
|
-
def retrieve_log(event_key)
|
|
185
|
-
@client.get("/v1/events/#{encode(event_key)}/log"
|
|
232
|
+
def retrieve_log(event_key, limit: nil, before: nil)
|
|
233
|
+
@client.get("/v1/events/#{encode(event_key)}/log",
|
|
234
|
+
compact({ "limit" => limit, "before" => before }))
|
|
186
235
|
end
|
|
187
236
|
end
|
|
188
237
|
end
|
data/lib/seatlayer/version.rb
CHANGED
data/lib/seatlayer.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "seatlayer/errors"
|
|
|
5
5
|
require_relative "seatlayer/http_client"
|
|
6
6
|
require_relative "seatlayer/resources"
|
|
7
7
|
require_relative "seatlayer/inventory"
|
|
8
|
+
require_relative "seatlayer/channels"
|
|
8
9
|
require_relative "seatlayer/webhook"
|
|
9
10
|
|
|
10
11
|
# Official Ruby server SDK for the SeatLayer reserved-seating API.
|
|
@@ -18,7 +19,7 @@ require_relative "seatlayer/webhook"
|
|
|
18
19
|
module SeatLayer
|
|
19
20
|
# The SeatLayer client.
|
|
20
21
|
class Client
|
|
21
|
-
attr_reader :charts, :events, :inventory, :sessions, :webhooks, :workspaces
|
|
22
|
+
attr_reader :charts, :events, :inventory, :channels, :sessions, :webhooks, :workspaces
|
|
22
23
|
|
|
23
24
|
def initialize(secret_key, base_url: HTTPClient::DEFAULT_BASE_URL,
|
|
24
25
|
max_retries: HTTPClient::DEFAULT_MAX_RETRIES,
|
|
@@ -29,6 +30,7 @@ module SeatLayer
|
|
|
29
30
|
@charts = Charts.new(@http)
|
|
30
31
|
@events = Events.new(@http)
|
|
31
32
|
@inventory = Inventory.new(@http)
|
|
33
|
+
@channels = Channels.new(@http)
|
|
32
34
|
@sessions = Sessions.new(@http)
|
|
33
35
|
@webhooks = Webhooks.new(@http)
|
|
34
36
|
@workspaces = Workspaces.new(@http)
|
|
@@ -44,10 +46,12 @@ module SeatLayer
|
|
|
44
46
|
@http.get("/health/ready")
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
# Escape hatch for surface this SDK does not wrap yet.
|
|
48
|
-
#
|
|
49
|
-
def request(method, path, query: nil, body: nil,
|
|
50
|
-
|
|
49
|
+
# Escape hatch for surface this SDK does not wrap yet. Reads retain retries;
|
|
50
|
+
# raw mutations are single-attempt because their replay contract is unknown.
|
|
51
|
+
def request(method, path, query: nil, body: nil, raw_body: nil, content_type: nil,
|
|
52
|
+
idempotency_key: nil)
|
|
53
|
+
@http.request(method, path, query: query, body: body, raw_body: raw_body,
|
|
54
|
+
content_type: content_type, idempotency_key: idempotency_key)
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
57
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: seatlayer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SeatLayer
|
|
@@ -21,6 +21,7 @@ files:
|
|
|
21
21
|
- LICENSE
|
|
22
22
|
- README.md
|
|
23
23
|
- lib/seatlayer.rb
|
|
24
|
+
- lib/seatlayer/channels.rb
|
|
24
25
|
- lib/seatlayer/errors.rb
|
|
25
26
|
- lib/seatlayer/http_client.rb
|
|
26
27
|
- lib/seatlayer/inventory.rb
|