diff --git a/CHANGELOG.md b/CHANGELOG.md index 3342fb8..2448c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +## 0.5.0 — 2026-08-21 + +- Added `performanceGroups`, the trusted server resource for fixed two-to-eight + performance runs. It creates and activates groups, mints one-time browser + access, retrieves authoritative group holds, and confirms bookings with + stable action and order references. Browser-only group routes remain outside + this secret-key SDK. + - Added `templates->instantiateTemplate()` and the ticket-release lifecycle on `events` (`listTicketReleases`, `updateTicketReleases`, and `closeTicketRelease`). Template instantiation sends `{}` when no overrides diff --git a/src/Resources/PerformanceGroups.php b/src/Resources/PerformanceGroups.php new file mode 100644 index 0000000..ebc1d86 --- /dev/null +++ b/src/Resources/PerformanceGroups.php @@ -0,0 +1,201 @@ + */ + public function list( + ?string $workspaceId = null, + ?string $externalRef = null, + ?string $state = null, + ?int $limit = null, + ?string $cursor = null, + ): array { + /** @var array */ + return (array) $this->http->get('/v1/performance-groups', array_filter([ + 'workspaceId' => $workspaceId, + 'externalRef' => $externalRef, + 'state' => $state, + 'limit' => $limit, + 'cursor' => $cursor, + ], static fn (mixed $value): bool => $value !== null)); + } + + /** + * Create a draft run. The same idempotency key safely replays the original response. + * + * @param list $eventKeys + * @return array + */ + public function create( + string $name, + array $eventKeys, + ?string $externalRef = null, + ?string $idempotencyKey = null, + ): array { + $body = ['name' => $name, 'eventKeys' => $eventKeys]; + if ($externalRef !== null) { + $body['externalRef'] = $externalRef; + } + + /** @var array */ + return (array) $this->http->postWithHeaderReplay('/v1/performance-groups', $body, $idempotencyKey); + } + + /** @return array */ + public function retrieve(string $performanceGroupKey): array + { + /** @var array */ + return (array) $this->http->get(self::path($performanceGroupKey)); + } + + /** Delete a draft only; activated runs remain available for audit. */ + public function delete(string $performanceGroupKey): void + { + $this->http->delete(self::path($performanceGroupKey)); + } + + /** + * Start activation. If lifecycleOperation.terminal is false, poll retrieveLifecycle(). + * + * @return array + */ + public function activate(string $performanceGroupKey, int $expectedRevision): array + { + /** @var array */ + return (array) $this->http->post(self::path($performanceGroupKey, '/activate'), [ + 'expectedRevision' => $expectedRevision, + ]); + } + + /** + * Stop new group sales. Poll retrieveLifecycle() until the close becomes terminal. + * + * @return array + */ + public function close(string $performanceGroupKey, int $expectedRevision): array + { + /** @var array */ + return (array) $this->http->post(self::path($performanceGroupKey, '/close'), [ + 'expectedRevision' => $expectedRevision, + ]); + } + + /** @return array */ + public function retrieveLifecycle(string $performanceGroupKey, string $operationId): array + { + /** @var array */ + return (array) $this->http->get(self::path( + $performanceGroupKey, + '/lifecycle/' . HttpClient::encode($operationId), + )); + } + + /** + * Reveal one origin-bound browser bearer. This call remains single-attempt. + * + * @param array>|null $channelIdsByEvent + * @return array + */ + public function createBuyerAccessSession( + string $performanceGroupKey, + string $allowedOrigin, + bool $includePublic, + ?array $channelIdsByEvent = null, + ?int $expiresInSeconds = null, + ?int $maxQuantity = null, + ?string $buyerRef = null, + ?string $partnerRef = null, + ): array { + $body = array_filter([ + 'allowedOrigin' => $allowedOrigin, + 'includePublic' => $includePublic, + 'channelIdsByEvent' => $channelIdsByEvent, + 'expiresInSeconds' => $expiresInSeconds, + 'maxQuantity' => $maxQuantity, + 'buyerRef' => $buyerRef, + 'partnerRef' => $partnerRef, + ], static fn (mixed $value): bool => $value !== null); + + /** @var array */ + return (array) $this->http->post(self::path($performanceGroupKey, '/buyer-access-sessions'), $body); + } + + /** @return array */ + public function listBuyerAccessSessions(string $performanceGroupKey, ?int $limit = null): array + { + /** @var array */ + return (array) $this->http->get( + self::path($performanceGroupKey, '/buyer-access-sessions'), + $limit === null ? null : ['limit' => $limit], + ); + } + + /** @return array */ + public function revokeBuyerAccessSession(string $performanceGroupKey, string $sessionId): array + { + /** @var array */ + return (array) $this->http->delete(self::path( + $performanceGroupKey, + '/buyer-access-sessions/' . HttpClient::encode($sessionId), + )); + } + + /** @return array */ + public function retrieveHold(string $performanceGroupKey, string $operationId): array + { + /** @var array */ + return (array) $this->http->get(self::path( + $performanceGroupKey, + '/holds/' . HttpClient::encode($operationId), + )); + } + + /** + * Confirm an already-authorized payment. Reuse both stable IDs and poll retrieveBooking() while pending. + * + * @return array + */ + public function bookHold( + string $performanceGroupKey, + string $operationId, + string $bookActionId, + string $bookingRef, + ): array { + /** @var array */ + return (array) $this->http->post(self::path( + $performanceGroupKey, + '/holds/' . HttpClient::encode($operationId) . '/book', + ), ['bookActionId' => $bookActionId, 'bookingRef' => $bookingRef]); + } + + /** @return array */ + public function retrieveBooking(string $performanceGroupKey, string $actionId): array + { + /** @var array */ + return (array) $this->http->get(self::path( + $performanceGroupKey, + '/bookings/' . HttpClient::encode($actionId), + )); + } +} diff --git a/src/SeatLayer.php b/src/SeatLayer.php index 81b0469..bce2f8c 100644 --- a/src/SeatLayer.php +++ b/src/SeatLayer.php @@ -8,6 +8,7 @@ use SeatLayer\Resources\Channels; use SeatLayer\Resources\Events; use SeatLayer\Resources\Inventory; +use SeatLayer\Resources\PerformanceGroups; use SeatLayer\Resources\Sessions; use SeatLayer\Resources\Templates; use SeatLayer\Resources\Webhooks; @@ -25,6 +26,7 @@ final class SeatLayer public readonly Channels $channels; public readonly Events $events; public readonly Inventory $inventory; + public readonly PerformanceGroups $performanceGroups; public readonly Sessions $sessions; public readonly Templates $templates; public readonly Webhooks $webhooks; @@ -52,6 +54,7 @@ public function __construct( $this->channels = new Channels($this->http); $this->events = new Events($this->http); $this->inventory = new Inventory($this->http); + $this->performanceGroups = new PerformanceGroups($this->http); $this->sessions = new Sessions($this->http); $this->templates = new Templates($this->http); $this->webhooks = new Webhooks($this->http); diff --git a/tests/ClientTest.php b/tests/ClientTest.php index a4c4e3a..02fb474 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -201,6 +201,57 @@ public function testDropsNullQueryParameters(): void self::assertSame('https://api.seatlayer.io/v1/charts?workspaceId=ws_1', $this->call(0)['url']); } + public function testMapsTheFullPerformanceGroupLifecycle(): void + { + $sdk = $this->client([ + ['status' => 200, 'body' => ['performanceGroups' => [], 'nextCursor' => null]], + ['status' => 201, 'body' => ['performanceGroup' => []]], + ['status' => 200, 'body' => ['performanceGroup' => []]], + ['status' => 204, 'body' => []], + ['status' => 202, 'body' => ['lifecycleOperation' => ['terminal' => false]]], + ['status' => 200, 'body' => ['lifecycleOperation' => ['terminal' => true]]], + ['status' => 200, 'body' => ['lifecycleOperation' => []]], + ['status' => 201, 'body' => ['token' => 'bsg_secret']], + ['status' => 200, 'body' => ['sessions' => []]], + ['status' => 200, 'body' => ['ok' => true]], + ['status' => 200, 'body' => ['hold' => []]], + ['status' => 202, 'body' => ['booking' => ['state' => 'book_pending']]], + ['status' => 200, 'body' => ['booking' => ['state' => 'booked']]], + ]); + $groupKey = 'pg_a/b'; + + $sdk->performanceGroups->list(workspaceId: 'ws_1', state: 'draft'); + $sdk->performanceGroups->create('Weekend run', ['ev_1', 'ev_2'], idempotencyKey: 'weekend-run-1'); + $sdk->performanceGroups->retrieve($groupKey); + $sdk->performanceGroups->delete($groupKey); + $sdk->performanceGroups->activate($groupKey, 1); + $sdk->performanceGroups->close($groupKey, 2); + $sdk->performanceGroups->retrieveLifecycle($groupKey, 'pga_1'); + $sdk->performanceGroups->createBuyerAccessSession($groupKey, 'https://tickets.example.test', true); + $sdk->performanceGroups->listBuyerAccessSessions($groupKey, 25); + $sdk->performanceGroups->revokeBuyerAccessSession($groupKey, 'pgbs_1'); + $sdk->performanceGroups->retrieveHold($groupKey, 'pgh_1'); + $sdk->performanceGroups->bookHold($groupKey, 'pgh_1', 'book_1', 'order_1'); + $sdk->performanceGroups->retrieveBooking($groupKey, 'book_1'); + + $base = 'https://api.seatlayer.io/v1/performance-groups/pg_a%2Fb'; + self::assertSame('https://api.seatlayer.io/v1/performance-groups?workspaceId=ws_1&state=draft', $this->call(0)['url']); + self::assertSame('weekend-run-1', $this->call(1)['headers']['Idempotency-Key']); + self::assertSame($base, $this->call(2)['url']); + self::assertSame('DELETE', $this->call(3)['method']); + self::assertSame($base . '/activate', $this->call(4)['url']); + self::assertSame($base . '/close', $this->call(5)['url']); + self::assertSame($base . '/lifecycle/pga_1', $this->call(6)['url']); + self::assertSame($base . '/buyer-access-sessions', $this->call(7)['url']); + self::assertArrayNotHasKey('Idempotency-Key', $this->call(7)['headers']); + self::assertSame($base . '/buyer-access-sessions?limit=25', $this->call(8)['url']); + self::assertSame($base . '/buyer-access-sessions/pgbs_1', $this->call(9)['url']); + self::assertSame($base . '/holds/pgh_1', $this->call(10)['url']); + self::assertSame($base . '/holds/pgh_1/book', $this->call(11)['url']); + self::assertArrayNotHasKey('Idempotency-Key', $this->call(11)['headers']); + self::assertSame($base . '/bookings/book_1', $this->call(12)['url']); + } + // ---------- errors ---------- public function testModeMismatchIsSelfExplaining(): void