seatlayer 0.8.0 → 0.9.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.
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SeatLayer
4
+ # Season holders and customers: contact details, sessions, tickets, ticket
5
+ # transfers between holders, returns, and renewals.
6
+ class Holders < Resource
7
+ # One page of customers, or the one matching +email+ or +holder_ref+.
8
+ def list_customers(email: nil, holder_ref: nil, cursor: nil, limit: nil)
9
+ @client.get("/v1/customers", compact({ "email" => email, "holderRef" => holder_ref,
10
+ "cursor" => cursor, "limit" => limit }))
11
+ end
12
+
13
+ # Read one holder.
14
+ def retrieve_holder(holder_ref)
15
+ @client.get(path(holder_ref))
16
+ end
17
+
18
+ # Set a holder's email, name or phone. Pass +nil+ for +name+ or +phone+ to
19
+ # send an explicit JSON null.
20
+ def update_holder(holder_ref, email: nil, name: UNSET, phone: UNSET, idempotency_key: nil)
21
+ body = compact({ "email" => email })
22
+ body.merge!(supplied({ "name" => name, "phone" => phone }))
23
+ @client.mutation_with_header_replay(
24
+ "PATCH", path(holder_ref), body, idempotency_key: idempotency_key
25
+ )
26
+ end
27
+
28
+ # Sign a holder out of every session, or only out of +session_id+.
29
+ # Single-attempt.
30
+ def revoke_holder_sessions(holder_ref, session_id: nil)
31
+ @client.post(path(holder_ref, "/sessions/revoke"), compact({ "sessionId" => session_id }))
32
+ end
33
+
34
+ # List the Seasons a holder belongs to.
35
+ def list_holder_seasons(holder_ref)
36
+ @client.get(path(holder_ref, "/seasons"))
37
+ end
38
+
39
+ # One page of a holder's tickets. +from+ and +to+ are epoch milliseconds.
40
+ def list_holder_tickets(holder_ref, season_key: nil, from: nil, to: nil, cursor: nil,
41
+ limit: nil)
42
+ @client.get(path(holder_ref, "/tickets"),
43
+ compact({ "seasonKey" => season_key, "from" => from, "to" => to,
44
+ "cursor" => cursor, "limit" => limit }))
45
+ end
46
+
47
+ # Read one of a holder's tickets.
48
+ def retrieve_holder_ticket(holder_ref, ticket_id)
49
+ @client.get(path(holder_ref, "/tickets/#{encode(ticket_id)}"))
50
+ end
51
+
52
+ # Offer tickets to another person by email. Turning customer sends off in
53
+ # Settings › Ticket site doesn't stop your server; cut-offs still apply.
54
+ def create_holder_transfer(holder_ref, ticket_ids:, to_email:, idempotency_key: nil)
55
+ @client.post(path(holder_ref, "/transfers"),
56
+ { "ticketIds" => ticket_ids, "toEmail" => to_email },
57
+ idempotency_key: idempotency_key, retry_policy: :header_replay)
58
+ end
59
+
60
+ # List a holder's ticket transfers.
61
+ def list_holder_transfers(holder_ref)
62
+ @client.get(path(holder_ref, "/transfers"))
63
+ end
64
+
65
+ # Cancel a transfer that has not been accepted yet. Single-attempt.
66
+ def cancel_holder_transfer(holder_ref, transfer_id)
67
+ @client.post(path(holder_ref, "/transfers/#{encode(transfer_id)}/cancel"))
68
+ end
69
+
70
+ # Accept a transfer for its recipient. Single-attempt.
71
+ def accept_holder_transfer(holder_ref, transfer_id)
72
+ @client.post(path(holder_ref, "/transfers/#{encode(transfer_id)}/accept"))
73
+ end
74
+
75
+ # Give back a holder's seat on one date, one per ticket: 1 to 20 tickets,
76
+ # all of one Season. A repeat with the same key answers with the same return.
77
+ # Turning give-backs off in Settings › Ticket site doesn't stop your server;
78
+ # cut-offs still apply.
79
+ def create_holder_return(holder_ref, ticket_ids:, idempotency_key: nil)
80
+ @client.post(path(holder_ref, "/returns"), { "ticketIds" => ticket_ids },
81
+ idempotency_key: idempotency_key, retry_policy: :header_replay)
82
+ end
83
+
84
+ # Start a SeatLayer checkout that renews one of a holder's offers (+renewalOffer+
85
+ # on +list_holder_seasons+). Send the buyer to +payment.url+. Retried with one
86
+ # Idempotency-Key, which answers the same order.
87
+ def create_holder_renewal_checkout(holder_ref, offer_id, return_url:, buyer_email: nil,
88
+ buyer_name: nil, provider: nil, idempotency_key: nil)
89
+ @client.post(path(holder_ref, "/renewal-offers/#{encode(offer_id)}/checkout"),
90
+ compact({ "returnUrl" => return_url, "buyerEmail" => buyer_email,
91
+ "buyerName" => buyer_name, "provider" => provider }),
92
+ idempotency_key: idempotency_key, retry_policy: :header_replay)
93
+ end
94
+
95
+ # Decline one of a holder's renewal offers and release its seats. +reason+
96
+ # (1 to 200 characters) is kept on the offer as +declineReason+; declining
97
+ # again answers the same offer, with the first reason. Single-attempt.
98
+ def decline_holder_renewal_offer(holder_ref, offer_id, reason: nil)
99
+ @client.post(path(holder_ref, "/renewal-offers/#{encode(offer_id)}/decline"),
100
+ compact({ "reason" => reason }))
101
+ end
102
+
103
+ private
104
+
105
+ def path(holder_ref, suffix = "")
106
+ "/v1/holders/#{encode(holder_ref)}#{suffix}"
107
+ end
108
+ end
109
+ end
@@ -64,17 +64,8 @@ module SeatLayer
64
64
 
65
65
  def request(method, path, query: nil, body: nil, raw_body: nil, content_type: nil,
66
66
  idempotency_key: nil, retry_policy: :none)
67
- url = @base_url + path
68
- if query
69
- pairs = query.compact
70
- url += "?#{URI.encode_www_form(pairs)}" unless pairs.empty?
71
- end
72
-
73
- headers = {
74
- "Authorization" => "Bearer #{@secret_key}",
75
- "Accept" => "application/json",
76
- "User-Agent" => "seatlayer-ruby"
77
- }
67
+ url = build_url(path, query)
68
+ headers = default_headers
78
69
  payload = build_payload(body, raw_body, content_type, headers)
79
70
 
80
71
  # Only operations with exact server-side response replay get an automatic
@@ -97,6 +88,13 @@ module SeatLayer
97
88
  request("GET", path, query: query)
98
89
  end
99
90
 
91
+ # Internal path for CSV exports. Same auth, retries and error mapping as
92
+ # #get, but a success body comes back as the raw String, never parsed.
93
+ def get_text(path, query = nil)
94
+ headers = default_headers.merge("Accept" => "text/csv")
95
+ execute("GET", build_url(path, query), headers, nil, retry_allowed: true, text: true)
96
+ end
97
+
100
98
  def post(path, body = nil, idempotency_key: nil, retry_policy: :none)
101
99
  request("POST", path, body: body, idempotency_key: idempotency_key, retry_policy: retry_policy)
102
100
  end
@@ -125,6 +123,22 @@ module SeatLayer
125
123
 
126
124
  private
127
125
 
126
+ def build_url(path, query)
127
+ url = @base_url + path
128
+ return url unless query
129
+
130
+ pairs = query.compact
131
+ pairs.empty? ? url : "#{url}?#{URI.encode_www_form(pairs)}"
132
+ end
133
+
134
+ def default_headers
135
+ {
136
+ "Authorization" => "Bearer #{@secret_key}",
137
+ "Accept" => "application/json",
138
+ "User-Agent" => "seatlayer-ruby"
139
+ }
140
+ end
141
+
128
142
  def build_payload(body, raw_body, content_type, headers)
129
143
  raise ArgumentError, "body and raw_body are mutually exclusive" if !body.nil? && !raw_body.nil?
130
144
 
@@ -140,7 +154,7 @@ module SeatLayer
140
154
 
141
155
  # The retry loop, extracted from #request so each piece stays readable: the
142
156
  # public method builds the call, this one decides how many times to make it.
143
- def execute(method, url, headers, payload, retry_allowed:)
157
+ def execute(method, url, headers, payload, retry_allowed:, text: false)
144
158
  last_error = nil
145
159
 
146
160
  @max_retries.times do |attempt|
@@ -155,7 +169,7 @@ module SeatLayer
155
169
  end
156
170
 
157
171
  status = response[:status]
158
- return parse_success(status, response[:body]) if status >= 200 && status < 300
172
+ return parse_success(status, response[:body], text) if status >= 200 && status < 300
159
173
 
160
174
  error_body = decode_error_body(response[:body])
161
175
  retry_after = parse_retry_after(response[:headers], error_body)
@@ -183,7 +197,8 @@ module SeatLayer
183
197
  {}
184
198
  end
185
199
 
186
- def parse_success(status, body)
200
+ def parse_success(status, body, text)
201
+ return body.to_s if text
187
202
  return {} if status == 204 || body.nil? || body.empty?
188
203
 
189
204
  JSON.parse(body)
@@ -205,9 +205,26 @@ module SeatLayer
205
205
 
206
206
  # Manage webhook subscriptions. To VERIFY a delivery, see SeatLayer::Webhook.
207
207
  class Webhooks < Resource
208
+ # All 58 public event names, in the order the API lists them.
208
209
  EVENT_NAMES = [
209
- "seat.booked", "seat.released", "seat.blocked", "hold.expired",
210
- "hold.created", "hold.extended", "event.created", "event.soldout"
210
+ "seat.booked", "seat.released", "seat.blocked", "hold.expired", "hold.created",
211
+ "hold.extended", "event.created", "event.soldout", "object.resale.listed",
212
+ "object.resale.unlisted", "object.resale.sold", "customer.created", "customer.email_changed",
213
+ "order.paid", "order.refunded", "order.cancelled", "season.ticket.transfer_sent",
214
+ "season.ticket.transferred", "season.ticket.transfer_cancelled", "season.ticket.issued",
215
+ "season.ticket.voided", "season.structure.activated", "season.structure.closed",
216
+ "season.structure.archived", "season.plan.published", "season.plan.superseded",
217
+ "season.sales.opened", "season.sales.paused", "season.sales.ended", "season.hold.committed",
218
+ "season.hold.released", "season.hold.expired", "season.booking.confirmed",
219
+ "season.booking.cancelled", "season.booking.emergency_requested",
220
+ "season.occurrence.returned", "season.occurrence.reclaimed", "season.renewal.offered",
221
+ "season.renewal.intent_received", "season.renewal.renewed", "season.renewal.declined",
222
+ "season.renewal.lapsed", "season.renewal.released", "season.operation.partial_terminal",
223
+ "season.amendment.applied", "season.performance.added", "season.performance.removed",
224
+ "season.performance.past", "season.performance.postponed", "season.performance.cancelled",
225
+ "season.performance.rescheduled", "season.bulk_action.completed", "season.block.ready",
226
+ "season.block.released", "season.order.paid", "season.order.refunded", "series.created",
227
+ "series.dates_added"
211
228
  ].freeze
212
229
 
213
230
  def list
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SeatLayer
4
+ # Orders SeatLayer took through its own checkout: reads, CSV export,
5
+ # refunds, cancellations and ticket resends.
6
+ class Orders < Resource
7
+ # One page of orders. +from+ and +to+ are epoch milliseconds. +holder_ref+ lists
8
+ # one holder's orders: the Season order with that id and renewals offered to that
9
+ # reference. Each order carries "holderRef" and "customerId"; "buyer"/"email" is
10
+ # nil for a box office sale taken without one.
11
+ def list_orders(event_key: nil, season_key: nil, status: nil, from: nil, to: nil,
12
+ cursor: nil, limit: nil, holder_ref: nil)
13
+ @client.get("/v1/orders", compact({ "eventKey" => event_key, "seasonKey" => season_key,
14
+ "holderRef" => holder_ref,
15
+ "status" => status, "from" => from, "to" => to,
16
+ "cursor" => cursor, "limit" => limit }))
17
+ end
18
+
19
+ # Read one order.
20
+ def retrieve_order(order_id)
21
+ @client.get("/v1/orders/#{encode(order_id)}")
22
+ end
23
+
24
+ # List one order's tickets.
25
+ def list_order_tickets(order_id)
26
+ @client.get("/v1/orders/#{encode(order_id)}/tickets")
27
+ end
28
+
29
+ # One page of an event's orders. +search+ is sent as the API's +q+
30
+ # parameter; +from+ and +to+ are epoch milliseconds.
31
+ def list_event_orders(event_key, search: nil, status: nil, paid_by: nil, from: nil, to: nil,
32
+ cursor: nil, limit: nil)
33
+ @client.get("/v1/events/#{encode(event_key)}/orders",
34
+ compact({ "q" => search, "status" => status, "paidBy" => paid_by,
35
+ "from" => from, "to" => to, "cursor" => cursor, "limit" => limit }))
36
+ end
37
+
38
+ # Read one of an event's orders.
39
+ def retrieve_event_order(event_key, order_id)
40
+ @client.get("/v1/events/#{encode(event_key)}/orders/#{encode(order_id)}")
41
+ end
42
+
43
+ # Download an event's orders as CSV, with the same filters as
44
+ # +list_event_orders+. Returns the raw CSV text.
45
+ def export_event_orders_csv(event_key, search: nil, status: nil, paid_by: nil, from: nil,
46
+ to: nil)
47
+ @client.get_text("/v1/events/#{encode(event_key)}/orders.csv",
48
+ compact({ "q" => search, "status" => status, "paidBy" => paid_by,
49
+ "from" => from, "to" => to }))
50
+ end
51
+
52
+ # Refund an event order, in full or by +amount_minor+.
53
+ def refund_event_order(event_key, order_id, amount_minor: nil, idempotency_key: nil)
54
+ @client.post("/v1/events/#{encode(event_key)}/orders/#{encode(order_id)}/refund",
55
+ compact({ "amountMinor" => amount_minor }),
56
+ idempotency_key: idempotency_key, retry_policy: :header_replay)
57
+ end
58
+
59
+ # Cancel an event order.
60
+ def cancel_event_order(event_key, order_id, reason:, note: nil, refund_confirmed: nil,
61
+ used_tickets_acknowledged: nil, notify_buyer: nil, idempotency_key: nil)
62
+ body = compact({ "reason" => reason, "note" => note, "refundConfirmed" => refund_confirmed,
63
+ "usedTicketsAcknowledged" => used_tickets_acknowledged,
64
+ "notifyBuyer" => notify_buyer })
65
+ @client.post("/v1/events/#{encode(event_key)}/orders/#{encode(order_id)}/cancel", body,
66
+ idempotency_key: idempotency_key, retry_policy: :header_replay)
67
+ end
68
+
69
+ # Send an event order's tickets again. Single-attempt: never retried automatically.
70
+ def resend_event_order_tickets(event_key, order_id, email: nil)
71
+ @client.post("/v1/events/#{encode(event_key)}/orders/#{encode(order_id)}/resend",
72
+ compact({ "email" => email }))
73
+ end
74
+ end
75
+ end
@@ -157,11 +157,14 @@ module SeatLayer
157
157
  #
158
158
  # Live availability counts cost one round-trip per event server-side. They
159
159
  # are on by default because most callers of a single page want them; pass
160
- # <tt>counts: false</tt> when paging a whole catalogue.
161
- def list(workspace_id: nil, external_ref: nil, limit: nil, cursor: nil, counts: true)
160
+ # <tt>counts: false</tt> when paging a whole catalogue. +series_key+ lists only
161
+ # the dates of one event series.
162
+ def list(workspace_id: nil, external_ref: nil, limit: nil, cursor: nil, counts: true,
163
+ series_key: nil)
162
164
  query = compact({ "workspaceId" => workspace_id, "externalRef" => external_ref,
163
165
  "limit" => limit, "cursor" => cursor })
164
166
  query["counts"] = "0" unless counts
167
+ query["seriesKey"] = series_key unless series_key.nil?
165
168
  @client.get("/v1/events", query)
166
169
  end
167
170
 
@@ -249,6 +252,8 @@ module SeatLayer
249
252
  @client.post("/v1/events/#{encode(event_key)}/close")
250
253
  end
251
254
 
255
+ # Open a paused event's sales again. A date its season cancelled never opens
256
+ # again: 409 season_date_cancelled.
252
257
  def reopen(event_key)
253
258
  @client.post("/v1/events/#{encode(event_key)}/reopen")
254
259
  end
@@ -289,5 +294,101 @@ module SeatLayer
289
294
  @client.get("/v1/events/#{encode(event_key)}/log",
290
295
  compact({ "limit" => limit, "before" => before }))
291
296
  end
297
+
298
+ # Read who takes payment for the event, and what blocks a change.
299
+ def retrieve_event_buyer_checkout(event_key)
300
+ @client.get("/v1/events/#{encode(event_key)}/buyer-checkout")
301
+ end
302
+
303
+ # Choose who takes payment: "seatlayer" or "integrator". Single-attempt;
304
+ # +expected_revision+ stops two writers overwriting each other.
305
+ def replace_event_buyer_checkout(event_key, buyer_checkout:, expected_revision:)
306
+ @client.put("/v1/events/#{encode(event_key)}/buyer-checkout",
307
+ { "buyerCheckout" => buyer_checkout, "expectedRevision" => expected_revision })
308
+ end
309
+
310
+ # Read the payment gateway the event uses, optionally for one +currency+.
311
+ def retrieve_event_payment_gateway(event_key, currency: nil)
312
+ @client.get("/v1/events/#{encode(event_key)}/payment-gateway",
313
+ compact({ "currency" => currency }))
314
+ end
315
+
316
+ # Assign a payment gateway connection to the event. Pass +nil+ to send an
317
+ # explicit JSON null. Single-attempt: never retried automatically.
318
+ def replace_event_payment_gateway(event_key, connection_id: UNSET, provider: UNSET)
319
+ @client.put("/v1/events/#{encode(event_key)}/payment-gateway",
320
+ supplied({ "connectionId" => connection_id, "provider" => provider }))
321
+ end
322
+
323
+ # Read what the event still needs before it can sell.
324
+ def retrieve_event_sales_readiness(event_key)
325
+ @client.get("/v1/events/#{encode(event_key)}/sales/readiness")
326
+ end
327
+
328
+ # Check one order against the event's sales setup. Single-attempt.
329
+ def preflight_event_sales(event_key, order_id:)
330
+ @client.post("/v1/events/#{encode(event_key)}/sales/preflight", { "orderId" => order_id })
331
+ end
332
+
333
+ # Move the event's sales to another state (setup, testable, live, paused or ended).
334
+ # A date its season cancelled never opens again: moving it to testable or live
335
+ # answers 409 season_date_cancelled.
336
+ def transition_event_sales(event_key, to:, expected_revision:, reason: nil)
337
+ @client.post("/v1/events/#{encode(event_key)}/sales/transition",
338
+ compact({ "to" => to, "expectedRevision" => expected_revision,
339
+ "reason" => reason }))
340
+ end
341
+
342
+ # Read the event's booking fee.
343
+ def retrieve_event_booking_fee(event_key)
344
+ @client.get("/v1/events/#{encode(event_key)}/booking-fee")
345
+ end
346
+
347
+ # Set the booking fee in minor units; +nil+ clears it and stays an explicit
348
+ # JSON null. Single-attempt: never retried automatically.
349
+ def replace_event_booking_fee(event_key, booking_fee_minor:)
350
+ @client.put("/v1/events/#{encode(event_key)}/booking-fee",
351
+ { "bookingFeeMinor" => booking_fee_minor })
352
+ end
353
+
354
+ # List the event's discount codes.
355
+ def list_event_discount_codes(event_key)
356
+ @client.get("/v1/events/#{encode(event_key)}/discount-codes")
357
+ end
358
+
359
+ # Create a discount code. +kind+ is "percent" or "amount". Pass +nil+ for
360
+ # +starts_at+, +ends_at+ or +max_uses+ to send an explicit JSON null.
361
+ # Single-attempt: never retried automatically.
362
+ # rubocop:disable-next Metrics/ParameterLists
363
+ def create_event_discount_code(event_key, code:, kind:, percent: nil, amount_minor: nil,
364
+ starts_at: UNSET, ends_at: UNSET, max_uses: UNSET, active: nil)
365
+ body = compact({ "code" => code, "kind" => kind, "percent" => percent,
366
+ "amountMinor" => amount_minor, "active" => active })
367
+ body.merge!(supplied({ "startsAt" => starts_at, "endsAt" => ends_at, "maxUses" => max_uses }))
368
+ @client.post("/v1/events/#{encode(event_key)}/discount-codes", body)
369
+ end
370
+
371
+ # Change a discount code. Only the fields you pass are sent; pass +nil+ for
372
+ # +starts_at+, +ends_at+ or +max_uses+ to send an explicit JSON null.
373
+ # Single-attempt: never retried automatically.
374
+ # rubocop:disable-next Metrics/ParameterLists
375
+ def update_event_discount_code(event_key, code_id, code: nil, kind: nil, percent: nil,
376
+ amount_minor: nil, starts_at: UNSET, ends_at: UNSET,
377
+ max_uses: UNSET, active: nil, state: nil)
378
+ body = compact({ "code" => code, "kind" => kind, "percent" => percent,
379
+ "amountMinor" => amount_minor, "active" => active, "state" => state })
380
+ body.merge!(supplied({ "startsAt" => starts_at, "endsAt" => ends_at, "maxUses" => max_uses }))
381
+ @client.patch("/v1/events/#{encode(event_key)}/discount-codes/#{encode(code_id)}", body)
382
+ end
383
+
384
+ # End a discount code now, keeping its history.
385
+ def end_event_discount_code(event_key, code_id)
386
+ @client.post("/v1/events/#{encode(event_key)}/discount-codes/#{encode(code_id)}/end")
387
+ end
388
+
389
+ # Delete a discount code. The API answers 204, so this returns an empty Hash.
390
+ def delete_event_discount_code(event_key, code_id)
391
+ @client.delete("/v1/events/#{encode(event_key)}/discount-codes/#{encode(code_id)}")
392
+ end
292
393
  end
293
394
  end