@theatrical/sdk 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2116 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AuthenticationError: () => AuthenticationError,
24
+ MockHTTPAdapter: () => MockHTTPAdapter,
25
+ NotFoundError: () => NotFoundError,
26
+ RateLimitError: () => RateLimitError,
27
+ SDK_USER_AGENT: () => SDK_USER_AGENT,
28
+ SDK_VERSION: () => SDK_VERSION,
29
+ ServerError: () => ServerError,
30
+ TheatricalClient: () => TheatricalClient,
31
+ TheatricalError: () => TheatricalError,
32
+ ValidationError: () => ValidationError
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/utils/validate-config.ts
37
+ var import_zod2 = require("zod");
38
+
39
+ // src/types/config.ts
40
+ var import_zod = require("zod");
41
+ var theatricalConfigSchema = import_zod.z.object({
42
+ apiKey: import_zod.z.string().min(1, "apiKey must not be empty").regex(/^[A-Za-z0-9_-]+$/, "apiKey must contain only alphanumeric characters, hyphens, and underscores"),
43
+ environment: import_zod.z.enum(["sandbox", "staging", "production"]).optional().default("sandbox"),
44
+ baseUrl: import_zod.z.string().url("baseUrl must be a valid URL").optional(),
45
+ timeout: import_zod.z.number().int("timeout must be an integer").positive("timeout must be a positive number").max(12e4, "timeout must not exceed 120000ms").optional().default(3e4),
46
+ maxRetries: import_zod.z.number().int("maxRetries must be an integer").min(0, "maxRetries must be 0 or greater").max(10, "maxRetries must not exceed 10").optional().default(3),
47
+ debug: import_zod.z.boolean().optional().default(false)
48
+ });
49
+
50
+ // src/errors/codes.ts
51
+ var VISTA_ERROR_CODES = {
52
+ // Auth
53
+ AUTH_TOKEN_EXPIRED: "AUTH_TOKEN_EXPIRED",
54
+ AUTH_TOKEN_INVALID: "AUTH_TOKEN_INVALID",
55
+ AUTH_TOKEN_MISSING: "AUTH_TOKEN_MISSING",
56
+ AUTH_INSUFFICIENT_SCOPE: "AUTH_INSUFFICIENT_SCOPE",
57
+ AUTH_API_KEY_INVALID: "AUTH_API_KEY_INVALID",
58
+ // Rate limiting
59
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
60
+ QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
61
+ // Validation
62
+ VALIDATION_FAILED: "VALIDATION_FAILED",
63
+ INVALID_PARAMETER: "INVALID_PARAMETER",
64
+ MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
65
+ INVALID_DATE_FORMAT: "INVALID_DATE_FORMAT",
66
+ INVALID_CURRENCY: "INVALID_CURRENCY",
67
+ // Resources
68
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
69
+ SESSION_EXPIRED: "SESSION_EXPIRED",
70
+ SESSION_SOLD_OUT: "SESSION_SOLD_OUT",
71
+ SEAT_UNAVAILABLE: "SEAT_UNAVAILABLE",
72
+ FILM_NOT_FOUND: "FILM_NOT_FOUND",
73
+ SITE_NOT_FOUND: "SITE_NOT_FOUND",
74
+ ORDER_NOT_FOUND: "ORDER_NOT_FOUND",
75
+ ORDER_ALREADY_CONFIRMED: "ORDER_ALREADY_CONFIRMED",
76
+ ORDER_CANCELLATION_WINDOW_EXPIRED: "ORDER_CANCELLATION_WINDOW_EXPIRED",
77
+ MEMBER_NOT_FOUND: "MEMBER_NOT_FOUND",
78
+ // Server
79
+ INTERNAL_ERROR: "INTERNAL_ERROR",
80
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
81
+ UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT"
82
+ };
83
+ var VISTA_ERROR_MESSAGES = {
84
+ [VISTA_ERROR_CODES.AUTH_TOKEN_EXPIRED]: "Your access token has expired. Re-authenticate and retry.",
85
+ [VISTA_ERROR_CODES.AUTH_TOKEN_INVALID]: "The provided access token is invalid.",
86
+ [VISTA_ERROR_CODES.AUTH_TOKEN_MISSING]: "No access token was provided.",
87
+ [VISTA_ERROR_CODES.AUTH_INSUFFICIENT_SCOPE]: "The access token does not have the required permissions.",
88
+ [VISTA_ERROR_CODES.AUTH_API_KEY_INVALID]: "The API key is invalid or has been revoked.",
89
+ [VISTA_ERROR_CODES.RATE_LIMIT_EXCEEDED]: "Too many requests. Please slow down.",
90
+ [VISTA_ERROR_CODES.QUOTA_EXCEEDED]: "API quota exceeded for this billing period.",
91
+ [VISTA_ERROR_CODES.VALIDATION_FAILED]: "One or more fields failed validation.",
92
+ [VISTA_ERROR_CODES.INVALID_PARAMETER]: "A request parameter contains an invalid value.",
93
+ [VISTA_ERROR_CODES.MISSING_REQUIRED_FIELD]: "A required field is missing from the request.",
94
+ [VISTA_ERROR_CODES.INVALID_DATE_FORMAT]: "Date must be in ISO 8601 format (YYYY-MM-DD).",
95
+ [VISTA_ERROR_CODES.INVALID_CURRENCY]: "Currency code must be a valid ISO 4217 code (e.g. NZD, AUD).",
96
+ [VISTA_ERROR_CODES.SESSION_NOT_FOUND]: "The requested session does not exist.",
97
+ [VISTA_ERROR_CODES.SESSION_EXPIRED]: "This session has already passed.",
98
+ [VISTA_ERROR_CODES.SESSION_SOLD_OUT]: "This session is sold out.",
99
+ [VISTA_ERROR_CODES.SEAT_UNAVAILABLE]: "One or more selected seats are no longer available.",
100
+ [VISTA_ERROR_CODES.FILM_NOT_FOUND]: "The requested film does not exist.",
101
+ [VISTA_ERROR_CODES.SITE_NOT_FOUND]: "The requested cinema site does not exist.",
102
+ [VISTA_ERROR_CODES.ORDER_NOT_FOUND]: "The requested order does not exist.",
103
+ [VISTA_ERROR_CODES.ORDER_ALREADY_CONFIRMED]: "This order has already been confirmed.",
104
+ [VISTA_ERROR_CODES.ORDER_CANCELLATION_WINDOW_EXPIRED]: "The cancellation window for this order has passed.",
105
+ [VISTA_ERROR_CODES.MEMBER_NOT_FOUND]: "The requested member does not exist.",
106
+ [VISTA_ERROR_CODES.INTERNAL_ERROR]: "An internal server error occurred. Please try again.",
107
+ [VISTA_ERROR_CODES.SERVICE_UNAVAILABLE]: "The service is temporarily unavailable.",
108
+ [VISTA_ERROR_CODES.UPSTREAM_TIMEOUT]: "The upstream service timed out."
109
+ };
110
+ function resolveVistaMessage(code, fallback) {
111
+ if (!code) return fallback;
112
+ return VISTA_ERROR_MESSAGES[code] ?? fallback;
113
+ }
114
+
115
+ // src/errors/parser.ts
116
+ async function tryParseBody(response) {
117
+ try {
118
+ const text = await response.text();
119
+ if (!text) return null;
120
+ return JSON.parse(text);
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+ function isVistaEnvelope(body) {
126
+ return typeof body === "object" && body !== null && ("code" in body || "message" in body || "errors" in body);
127
+ }
128
+ function isOcapiEnvelope(body) {
129
+ return typeof body === "object" && body !== null && "fault" in body;
130
+ }
131
+ function extractFieldErrors(errors) {
132
+ if (!errors || errors.length === 0) return {};
133
+ return Object.fromEntries(errors.map((e) => [e.field, e.message]));
134
+ }
135
+ function parseRetryAfter(response) {
136
+ const raw = response.headers.get("Retry-After");
137
+ if (!raw) return 60;
138
+ const seconds = parseInt(raw, 10);
139
+ if (!isNaN(seconds) && seconds >= 0) return seconds;
140
+ const date = new Date(raw);
141
+ if (!isNaN(date.getTime())) {
142
+ const delta = Math.ceil((date.getTime() - Date.now()) / 1e3);
143
+ return Math.max(0, delta);
144
+ }
145
+ return 60;
146
+ }
147
+ function extractResource(vistaCode, requestUrl) {
148
+ const CODE_TO_RESOURCE = {
149
+ SESSION_NOT_FOUND: "Session",
150
+ FILM_NOT_FOUND: "Film",
151
+ SITE_NOT_FOUND: "Site",
152
+ ORDER_NOT_FOUND: "Order",
153
+ MEMBER_NOT_FOUND: "Member"
154
+ };
155
+ const resource = (vistaCode && CODE_TO_RESOURCE[vistaCode]) ?? inferResourceFromUrl(requestUrl);
156
+ const id = requestUrl ? new URL(requestUrl, "https://api.vista.co").pathname.split("/").filter(Boolean).pop() ?? "unknown" : "unknown";
157
+ return { resource, resourceId: id };
158
+ }
159
+ function inferResourceFromUrl(url) {
160
+ if (!url) return "Resource";
161
+ const segments = url.split("/").filter(Boolean);
162
+ if (segments.length >= 2) {
163
+ const noun = segments[segments.length - 2];
164
+ return noun.charAt(0).toUpperCase() + noun.slice(1, -1);
165
+ }
166
+ return "Resource";
167
+ }
168
+ async function parseErrorResponse(response, requestUrl) {
169
+ const body = await tryParseBody(response);
170
+ const status = response.status;
171
+ let vistaCode;
172
+ let message;
173
+ let requestId;
174
+ let fieldErrors = {};
175
+ if (body && isVistaEnvelope(body)) {
176
+ vistaCode = body.code;
177
+ message = body.message;
178
+ requestId = body.requestId;
179
+ fieldErrors = extractFieldErrors(body.errors);
180
+ } else if (body && isOcapiEnvelope(body)) {
181
+ message = body.fault?.message;
182
+ vistaCode = body.fault?.type;
183
+ }
184
+ const resolvedMessage = resolveVistaMessage(vistaCode, message ?? `Request failed with status ${status}`);
185
+ if (status === 401 || status === 403) {
186
+ return new AuthenticationError(resolvedMessage, vistaCode, requestId);
187
+ }
188
+ if (status === 429) {
189
+ const retryAfter = parseRetryAfter(response);
190
+ return new RateLimitError(retryAfter, requestId);
191
+ }
192
+ if (status === 400 || status === 422) {
193
+ return new ValidationError(resolvedMessage, fieldErrors, requestId);
194
+ }
195
+ if (status === 404) {
196
+ const { resource, resourceId } = extractResource(vistaCode, requestUrl);
197
+ return new NotFoundError(resource, resourceId, requestId);
198
+ }
199
+ if (status >= 500) {
200
+ return new ServerError(resolvedMessage, requestId);
201
+ }
202
+ return new TheatricalError(resolvedMessage, status, vistaCode, requestId);
203
+ }
204
+
205
+ // src/errors/index.ts
206
+ var TheatricalError = class extends Error {
207
+ statusCode;
208
+ vistaErrorCode;
209
+ requestId;
210
+ constructor(message, statusCode, vistaErrorCode, requestId) {
211
+ super(message);
212
+ this.name = "TheatricalError";
213
+ this.statusCode = statusCode;
214
+ this.vistaErrorCode = vistaErrorCode;
215
+ this.requestId = requestId;
216
+ }
217
+ toJSON() {
218
+ return {
219
+ name: this.name,
220
+ message: this.message,
221
+ statusCode: this.statusCode,
222
+ vistaErrorCode: this.vistaErrorCode,
223
+ requestId: this.requestId
224
+ };
225
+ }
226
+ };
227
+ var AuthenticationError = class extends TheatricalError {
228
+ constructor(message = "Authentication failed", vistaErrorCode, requestId) {
229
+ super(message, 401, vistaErrorCode, requestId);
230
+ this.name = "AuthenticationError";
231
+ }
232
+ };
233
+ var RateLimitError = class extends TheatricalError {
234
+ retryAfter;
235
+ constructor(retryAfter, requestId) {
236
+ super(`Rate limit exceeded. Retry after ${retryAfter}s`, 429, void 0, requestId);
237
+ this.name = "RateLimitError";
238
+ this.retryAfter = retryAfter;
239
+ }
240
+ };
241
+ var ValidationError = class extends TheatricalError {
242
+ fields;
243
+ constructor(message, fields = {}, requestId) {
244
+ super(message, 400, void 0, requestId);
245
+ this.name = "ValidationError";
246
+ this.fields = fields;
247
+ }
248
+ };
249
+ var NotFoundError = class extends TheatricalError {
250
+ resource;
251
+ resourceId;
252
+ constructor(resource, resourceId, requestId) {
253
+ super(`${resource} '${resourceId}' not found`, 404, void 0, requestId);
254
+ this.name = "NotFoundError";
255
+ this.resource = resource;
256
+ this.resourceId = resourceId;
257
+ }
258
+ };
259
+ var ServerError = class extends TheatricalError {
260
+ constructor(message = "Internal server error", requestId) {
261
+ super(message, 500, void 0, requestId);
262
+ this.name = "ServerError";
263
+ }
264
+ };
265
+
266
+ // src/utils/validate-config.ts
267
+ function validateConfig(config) {
268
+ try {
269
+ return theatricalConfigSchema.parse(config);
270
+ } catch (err) {
271
+ if (err instanceof import_zod2.ZodError) {
272
+ const fields = {};
273
+ for (const issue of err.issues) {
274
+ const path = issue.path.join(".");
275
+ fields[path || "unknown"] = issue.message;
276
+ }
277
+ const firstField = Object.keys(fields)[0] ?? "config";
278
+ const firstMessage = fields[firstField] ?? "Invalid configuration";
279
+ throw new ValidationError(
280
+ `Invalid TheatricalConfig: ${firstField} \u2014 ${firstMessage}`,
281
+ fields
282
+ );
283
+ }
284
+ throw err;
285
+ }
286
+ }
287
+
288
+ // src/types/session.ts
289
+ var import_zod3 = require("zod");
290
+ var sessionFormatSchema = import_zod3.z.enum([
291
+ "2D",
292
+ "3D",
293
+ "IMAX",
294
+ "IMAX3D",
295
+ "4DX",
296
+ "DOLBY_CINEMA",
297
+ "SCREENX",
298
+ "STANDARD"
299
+ ]);
300
+ var seatStatusSchema = import_zod3.z.enum([
301
+ "available",
302
+ "taken",
303
+ "reserved",
304
+ "wheelchair",
305
+ "companion",
306
+ "blocked"
307
+ ]);
308
+ var sessionSchema = import_zod3.z.object({
309
+ id: import_zod3.z.string(),
310
+ filmId: import_zod3.z.string(),
311
+ filmTitle: import_zod3.z.string(),
312
+ siteId: import_zod3.z.string(),
313
+ screenId: import_zod3.z.string(),
314
+ screenName: import_zod3.z.string(),
315
+ startTime: import_zod3.z.string(),
316
+ endTime: import_zod3.z.string(),
317
+ format: sessionFormatSchema,
318
+ isBookable: import_zod3.z.boolean(),
319
+ isSoldOut: import_zod3.z.boolean(),
320
+ seatsAvailable: import_zod3.z.number().int().nonnegative(),
321
+ seatsTotal: import_zod3.z.number().int().positive(),
322
+ priceFrom: import_zod3.z.number().nonnegative().optional(),
323
+ currency: import_zod3.z.string().length(3).optional(),
324
+ attributes: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string())
325
+ });
326
+ var sessionFilterSchema = import_zod3.z.object({
327
+ siteId: import_zod3.z.string().optional(),
328
+ filmId: import_zod3.z.string().optional(),
329
+ date: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
330
+ dateFrom: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
331
+ dateTo: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
332
+ format: sessionFormatSchema.optional(),
333
+ bookableOnly: import_zod3.z.boolean().optional(),
334
+ limit: import_zod3.z.number().int().positive().max(500).optional(),
335
+ offset: import_zod3.z.number().int().nonnegative().optional(),
336
+ cursor: import_zod3.z.string().optional()
337
+ });
338
+ var sessionListResponseSchema = import_zod3.z.object({
339
+ sessions: import_zod3.z.array(sessionSchema),
340
+ total: import_zod3.z.number().int().nonnegative(),
341
+ hasMore: import_zod3.z.boolean(),
342
+ nextOffset: import_zod3.z.number().int().nonnegative().optional(),
343
+ nextCursor: import_zod3.z.string().optional()
344
+ });
345
+ var seatSchema = import_zod3.z.object({
346
+ id: import_zod3.z.string(),
347
+ row: import_zod3.z.string(),
348
+ number: import_zod3.z.number().int().positive(),
349
+ status: seatStatusSchema,
350
+ x: import_zod3.z.number(),
351
+ y: import_zod3.z.number(),
352
+ type: import_zod3.z.string().optional(),
353
+ isAccessible: import_zod3.z.boolean()
354
+ });
355
+ var seatAvailabilitySchema = import_zod3.z.object({
356
+ sessionId: import_zod3.z.string(),
357
+ screenName: import_zod3.z.string(),
358
+ seats: import_zod3.z.array(seatSchema),
359
+ rowCount: import_zod3.z.number().int().positive(),
360
+ screenPosition: import_zod3.z.enum(["top", "bottom"]),
361
+ availableCount: import_zod3.z.number().int().nonnegative(),
362
+ totalCount: import_zod3.z.number().int().positive()
363
+ });
364
+
365
+ // src/resources/sessions.ts
366
+ var DEFAULT_PAGE_SIZE = 50;
367
+ var SessionsResource = class {
368
+ constructor(http) {
369
+ this.http = http;
370
+ }
371
+ http;
372
+ /**
373
+ * List sessions (showtimes) with optional filters.
374
+ * Response is validated at runtime using Zod — malformed API responses throw a parse error.
375
+ * @throws {TheatricalError} When the API returns an error response
376
+ * @throws {z.ZodError} When the response fails schema validation
377
+ * @see https://developer.vista.co/digital-platform/sessions/
378
+ */
379
+ async list(filters) {
380
+ const raw = await this.http.get("/ocapi/v1/sessions", {
381
+ params: filters
382
+ });
383
+ return sessionListResponseSchema.parse(raw);
384
+ }
385
+ /**
386
+ * List sessions with explicit pagination control.
387
+ *
388
+ * Returns a normalised `PaginatedResponse<Session>` envelope that includes
389
+ * the pagination strategy and cursor/offset for the next page.
390
+ *
391
+ * @param filters - Session filters (site, film, date range, etc.)
392
+ * @param pagination - Page size and cursor/offset position
393
+ * @returns A single page of sessions with pagination metadata
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * const page = await client.sessions.listPaginated(
398
+ * { siteId: 'roxy-wellington' },
399
+ * { limit: 25 },
400
+ * );
401
+ * console.log(page.data.length, page.hasMore);
402
+ * ```
403
+ */
404
+ async listPaginated(filters, pagination) {
405
+ const limit = pagination?.limit ?? DEFAULT_PAGE_SIZE;
406
+ const useCursor = pagination?.cursor !== void 0;
407
+ const response = await this.list({
408
+ ...filters,
409
+ limit,
410
+ ...useCursor ? { cursor: pagination.cursor } : { offset: pagination?.offset ?? 0 }
411
+ });
412
+ return {
413
+ data: response.sessions,
414
+ total: response.total,
415
+ hasMore: response.hasMore,
416
+ nextCursor: response.nextCursor,
417
+ nextOffset: response.nextOffset,
418
+ strategy: useCursor ? "cursor" : "offset"
419
+ };
420
+ }
421
+ /**
422
+ * Async generator that automatically paginates through all matching sessions.
423
+ *
424
+ * Yields individual `Session` objects, fetching the next page transparently
425
+ * when the current page is exhausted. Uses offset-based pagination internally
426
+ * since Vista's session endpoints support numeric offsets.
427
+ *
428
+ * @param filters - Session filters (site, film, date range, etc.)
429
+ * @param pageSize - Number of sessions to fetch per page (default: 50, max: 500)
430
+ *
431
+ * @example
432
+ * ```typescript
433
+ * const allSessions: Session[] = [];
434
+ * for await (const session of client.sessions.listAll({ siteId: 'roxy-wellington' })) {
435
+ * allSessions.push(session);
436
+ * }
437
+ * ```
438
+ *
439
+ * @example Collect with early termination
440
+ * ```typescript
441
+ * for await (const session of client.sessions.listAll({ date: '2026-04-10' })) {
442
+ * if (session.isSoldOut) continue;
443
+ * console.log(`${session.filmTitle} — ${session.screenName} @ ${session.startTime}`);
444
+ * if (!session.isBookable) break; // stop on first unbookable
445
+ * }
446
+ * ```
447
+ */
448
+ async *listAll(filters, pageSize = DEFAULT_PAGE_SIZE) {
449
+ let offset = 0;
450
+ let hasMore = true;
451
+ while (hasMore) {
452
+ const response = await this.list({
453
+ ...filters,
454
+ limit: pageSize,
455
+ offset
456
+ });
457
+ for (const session of response.sessions) {
458
+ yield session;
459
+ }
460
+ hasMore = response.hasMore;
461
+ offset = response.nextOffset ?? offset + response.sessions.length;
462
+ }
463
+ }
464
+ /**
465
+ * Get a single session by ID.
466
+ * Response is validated at runtime using Zod.
467
+ */
468
+ async get(sessionId) {
469
+ const raw = await this.http.get(`/ocapi/v1/sessions/${sessionId}`);
470
+ return sessionSchema.parse(raw);
471
+ }
472
+ /**
473
+ * Get seat availability for a session.
474
+ * Returns the complete auditorium seat map with status for each seat.
475
+ * Response is validated at runtime using Zod.
476
+ */
477
+ async availability(sessionId) {
478
+ const raw = await this.http.get(`/ocapi/v1/sessions/${sessionId}/seat-plan`);
479
+ return seatAvailabilitySchema.parse(raw);
480
+ }
481
+ };
482
+
483
+ // src/types/site.ts
484
+ var import_zod4 = require("zod");
485
+ var geoLocationSchema = import_zod4.z.object({
486
+ latitude: import_zod4.z.number().min(-90).max(90),
487
+ longitude: import_zod4.z.number().min(-180).max(180)
488
+ });
489
+ var addressSchema = import_zod4.z.object({
490
+ line1: import_zod4.z.string(),
491
+ line2: import_zod4.z.string().optional(),
492
+ city: import_zod4.z.string(),
493
+ state: import_zod4.z.string().optional(),
494
+ postalCode: import_zod4.z.string(),
495
+ country: import_zod4.z.string().length(2)
496
+ });
497
+ var screenSchema = import_zod4.z.object({
498
+ id: import_zod4.z.string(),
499
+ name: import_zod4.z.string(),
500
+ seatCount: import_zod4.z.number().int().positive(),
501
+ formats: import_zod4.z.array(import_zod4.z.string()).min(1),
502
+ isAccessible: import_zod4.z.boolean()
503
+ });
504
+ var amenitySchema = import_zod4.z.object({
505
+ /** Amenity identifier (e.g. 'parking', 'bar', 'vip_lounge', 'imax') */
506
+ id: import_zod4.z.string(),
507
+ /** Human-readable label */
508
+ label: import_zod4.z.string(),
509
+ /** Optional icon key for rendering */
510
+ icon: import_zod4.z.string().optional()
511
+ });
512
+ var siteConfigSchema = import_zod4.z.object({
513
+ bookingLeadTime: import_zod4.z.number().int().nonnegative(),
514
+ maxTicketsPerOrder: import_zod4.z.number().int().positive(),
515
+ loyaltyEnabled: import_zod4.z.boolean(),
516
+ fnbEnabled: import_zod4.z.boolean()
517
+ });
518
+ var siteSchema = import_zod4.z.object({
519
+ id: import_zod4.z.string(),
520
+ name: import_zod4.z.string(),
521
+ address: addressSchema,
522
+ location: geoLocationSchema,
523
+ screens: import_zod4.z.array(screenSchema),
524
+ config: siteConfigSchema,
525
+ timezone: import_zod4.z.string(),
526
+ currency: import_zod4.z.string().length(3),
527
+ isActive: import_zod4.z.boolean(),
528
+ amenities: import_zod4.z.array(amenitySchema).optional()
529
+ });
530
+ var siteListResponseSchema = import_zod4.z.array(siteSchema);
531
+
532
+ // src/resources/sites.ts
533
+ var SitesResource = class {
534
+ constructor(http) {
535
+ this.http = http;
536
+ }
537
+ http;
538
+ /**
539
+ * List all cinema sites, with optional search query or geographic filter.
540
+ * Response is validated at runtime using Zod.
541
+ *
542
+ * @param filters - Optional filters: `query` (text search), `latitude`/`longitude`/`radius`
543
+ * @returns Array of cinema sites
544
+ * @throws {TheatricalError} When the API returns an error response
545
+ * @throws {z.ZodError} When the response fails schema validation
546
+ *
547
+ * @example
548
+ * ```typescript
549
+ * const sites = await client.sites.list({ query: 'Embassy' });
550
+ * ```
551
+ */
552
+ async list(filters) {
553
+ const params = {};
554
+ if (filters?.query) params.query = filters.query;
555
+ if (filters?.latitude !== void 0) params.latitude = filters.latitude;
556
+ if (filters?.longitude !== void 0) params.longitude = filters.longitude;
557
+ if (filters?.radius !== void 0) params.radius = filters.radius;
558
+ const raw = await this.http.get("/ocapi/v1/sites", { params });
559
+ return siteListResponseSchema.parse(raw);
560
+ }
561
+ /**
562
+ * Get a single cinema site by ID.
563
+ * Response is validated at runtime using Zod.
564
+ *
565
+ * @param siteId - Vista site identifier
566
+ * @returns The site record
567
+ *
568
+ * @example
569
+ * ```typescript
570
+ * const roxy = await client.sites.get('site_roxy_wellington');
571
+ * console.log(roxy.screens.length); // number of auditoriums
572
+ * ```
573
+ */
574
+ async get(siteId) {
575
+ const raw = await this.http.get(`/ocapi/v1/sites/${siteId}`);
576
+ return siteSchema.parse(raw);
577
+ }
578
+ /**
579
+ * Get the screen (auditorium) configurations for a site.
580
+ * Returns each screen's capacity, supported formats, and accessibility status.
581
+ * Response is validated at runtime using Zod.
582
+ *
583
+ * @param siteId - Vista site identifier
584
+ * @returns Array of screen configurations
585
+ *
586
+ * @example
587
+ * ```typescript
588
+ * const screens = await client.sites.screens('site_embassy_wellington');
589
+ * const imax = screens.find(s => s.formats.includes('IMAX'));
590
+ * ```
591
+ */
592
+ async screens(siteId) {
593
+ const raw = await this.http.get(`/ocapi/v1/sites/${siteId}/screens`);
594
+ return screenSchema.array().parse(raw);
595
+ }
596
+ /**
597
+ * Find cinema sites within a geographic radius.
598
+ *
599
+ * Returns sites sorted by distance from the given coordinates.
600
+ * Uses Vista's OCAPI geographic search which accepts decimal lat/lng
601
+ * and a radius in kilometres.
602
+ *
603
+ * @param latitude - Decimal latitude of the search centre (−90 to 90)
604
+ * @param longitude - Decimal longitude of the search centre (−180 to 180)
605
+ * @param radiusKm - Search radius in kilometres
606
+ * @returns Sites within the radius, sorted nearest-first
607
+ *
608
+ * @example
609
+ * ```typescript
610
+ * // Find cinemas within 5 km of Roxy Cinema Wellington
611
+ * const nearby = await client.sites.nearby(-41.3007, 174.7766, 5);
612
+ * console.log(nearby.map(s => s.name));
613
+ * ```
614
+ *
615
+ * @example Discover cinemas near central Auckland
616
+ * ```typescript
617
+ * const auckland = await client.sites.nearby(-36.8509, 174.7645, 10);
618
+ * ```
619
+ */
620
+ async nearby(latitude, longitude, radiusKm) {
621
+ const raw = await this.http.get("/ocapi/v1/sites", {
622
+ params: { latitude, longitude, radius: radiusKm }
623
+ });
624
+ return siteListResponseSchema.parse(raw);
625
+ }
626
+ };
627
+
628
+ // src/types/film.ts
629
+ var import_zod5 = require("zod");
630
+ var GENRES = [
631
+ "action",
632
+ "adventure",
633
+ "animation",
634
+ "comedy",
635
+ "crime",
636
+ "documentary",
637
+ "drama",
638
+ "family",
639
+ "fantasy",
640
+ "horror",
641
+ "musical",
642
+ "mystery",
643
+ "romance",
644
+ "sci-fi",
645
+ "thriller",
646
+ "war",
647
+ "western"
648
+ ];
649
+ var FILM_FORMATS = ["2D", "3D", "IMAX", "IMAX 3D", "4DX", "Dolby Atmos", "ScreenX"];
650
+ var FILM_LANGUAGES = ["en", "es", "fr", "de", "ja", "ko", "zh", "hi", "te", "mi"];
651
+ var genreSchema = import_zod5.z.enum(GENRES);
652
+ var filmFormatSchema = import_zod5.z.enum(FILM_FORMATS);
653
+ var filmLanguageSchema = import_zod5.z.enum(FILM_LANGUAGES);
654
+ var ratingSchema = import_zod5.z.object({
655
+ classification: import_zod5.z.string(),
656
+ description: import_zod5.z.string().optional()
657
+ });
658
+ var castMemberSchema = import_zod5.z.object({
659
+ name: import_zod5.z.string(),
660
+ role: import_zod5.z.string().optional()
661
+ });
662
+ var crewMemberSchema = import_zod5.z.object({
663
+ name: import_zod5.z.string(),
664
+ department: import_zod5.z.string(),
665
+ job: import_zod5.z.string()
666
+ });
667
+ var filmRatingSchema = import_zod5.z.object({
668
+ source: import_zod5.z.string(),
669
+ score: import_zod5.z.string(),
670
+ outOf: import_zod5.z.string().optional()
671
+ });
672
+ var filmSchema = import_zod5.z.object({
673
+ id: import_zod5.z.string(),
674
+ title: import_zod5.z.string(),
675
+ synopsis: import_zod5.z.string(),
676
+ genres: import_zod5.z.array(genreSchema),
677
+ runtime: import_zod5.z.number().nonnegative(),
678
+ rating: ratingSchema,
679
+ releaseDate: import_zod5.z.string(),
680
+ posterUrl: import_zod5.z.string().optional(),
681
+ trailerUrl: import_zod5.z.string().optional(),
682
+ cast: import_zod5.z.array(castMemberSchema),
683
+ director: import_zod5.z.string(),
684
+ distributor: import_zod5.z.string().optional(),
685
+ isNowShowing: import_zod5.z.boolean(),
686
+ isComingSoon: import_zod5.z.boolean()
687
+ });
688
+ var filmDetailSchema = filmSchema.extend({
689
+ crew: import_zod5.z.array(crewMemberSchema),
690
+ ratings: import_zod5.z.array(filmRatingSchema),
691
+ formats: import_zod5.z.array(filmFormatSchema),
692
+ languages: import_zod5.z.array(filmLanguageSchema),
693
+ originalTitle: import_zod5.z.string().optional(),
694
+ productionCountries: import_zod5.z.array(import_zod5.z.string()).optional(),
695
+ budget: import_zod5.z.number().optional(),
696
+ boxOffice: import_zod5.z.number().optional(),
697
+ website: import_zod5.z.string().optional()
698
+ });
699
+ var filmSearchFilterSchema = import_zod5.z.object({
700
+ siteId: import_zod5.z.string().optional(),
701
+ genre: genreSchema.optional(),
702
+ query: import_zod5.z.string().optional(),
703
+ nowShowing: import_zod5.z.boolean().optional(),
704
+ comingSoon: import_zod5.z.boolean().optional(),
705
+ limit: import_zod5.z.number().int().positive().optional(),
706
+ offset: import_zod5.z.number().int().nonnegative().optional(),
707
+ ratingClassification: import_zod5.z.string().optional(),
708
+ format: filmFormatSchema.optional(),
709
+ language: filmLanguageSchema.optional(),
710
+ releaseDateFrom: import_zod5.z.string().optional(),
711
+ releaseDateTo: import_zod5.z.string().optional(),
712
+ minRuntime: import_zod5.z.number().nonnegative().optional(),
713
+ maxRuntime: import_zod5.z.number().nonnegative().optional(),
714
+ sortBy: import_zod5.z.enum(["title", "releaseDate", "runtime", "popularity"]).optional(),
715
+ sortOrder: import_zod5.z.enum(["asc", "desc"]).optional()
716
+ });
717
+
718
+ // src/resources/films.ts
719
+ var import_zod6 = require("zod");
720
+ var FilmsResource = class {
721
+ constructor(http) {
722
+ this.http = http;
723
+ }
724
+ http;
725
+ /**
726
+ * Parse and validate a raw film response from the API.
727
+ * Throws a ZodError if the response doesn't match the expected shape.
728
+ */
729
+ parseFilm(data) {
730
+ return filmSchema.parse(data);
731
+ }
732
+ /**
733
+ * Parse and validate an array of films from the API.
734
+ */
735
+ parseFilms(data) {
736
+ return import_zod6.z.array(filmSchema).parse(data);
737
+ }
738
+ /**
739
+ * Parse and validate a full film detail response.
740
+ */
741
+ parseFilmDetail(data) {
742
+ return filmDetailSchema.parse(data);
743
+ }
744
+ /**
745
+ * List films currently showing at a given site (or across all sites).
746
+ *
747
+ * @param filters - Optional site filter
748
+ * @returns Array of films currently in cinemas
749
+ */
750
+ async nowShowing(filters) {
751
+ const params = filters?.siteId ? { siteId: filters.siteId } : void 0;
752
+ const data = await this.http.get("/ocapi/v1/films/now-showing", { params });
753
+ return this.parseFilms(data);
754
+ }
755
+ /**
756
+ * List films coming soon — announced but not yet in cinemas.
757
+ *
758
+ * @param filters - Optional site filter
759
+ * @returns Array of upcoming films
760
+ */
761
+ async comingSoon(filters) {
762
+ const params = filters?.siteId ? { siteId: filters.siteId } : void 0;
763
+ const data = await this.http.get("/ocapi/v1/films/coming-soon", { params });
764
+ return this.parseFilms(data);
765
+ }
766
+ /**
767
+ * Retrieve a film by its unique identifier.
768
+ *
769
+ * @param filmId - The UUID of the film
770
+ * @returns The base film record
771
+ */
772
+ async get(filmId) {
773
+ const data = await this.http.get(`/ocapi/v1/films/${filmId}`);
774
+ return this.parseFilm(data);
775
+ }
776
+ /**
777
+ * Retrieve full film detail including cast, crew, ratings, formats, and languages.
778
+ *
779
+ * This returns the extended `FilmDetail` type with nested crew arrays,
780
+ * multiple rating sources (IMDB, Rotten Tomatoes, Metacritic), available
781
+ * screening formats, and language options.
782
+ *
783
+ * @param filmId - The UUID of the film
784
+ * @returns Full film detail with cast, crew, and ratings
785
+ */
786
+ async getDetail(filmId) {
787
+ const data = await this.http.get(`/ocapi/v1/films/${filmId}/detail`);
788
+ return this.parseFilmDetail(data);
789
+ }
790
+ /**
791
+ * Search films with basic filters.
792
+ *
793
+ * @throws {TheatricalError} When the API returns an error response
794
+ * @throws {z.ZodError} When the response fails schema validation
795
+ * @param filters - Genre, query string, site, and showing status filters
796
+ * @returns Array of matching films
797
+ */
798
+ async search(filters) {
799
+ const params = {};
800
+ if (filters.siteId) params.siteId = filters.siteId;
801
+ if (filters.genre) params.genre = filters.genre;
802
+ if (filters.query) params.query = filters.query;
803
+ if (filters.nowShowing !== void 0) params.nowShowing = filters.nowShowing;
804
+ if (filters.comingSoon !== void 0) params.comingSoon = filters.comingSoon;
805
+ const data = await this.http.get("/ocapi/v1/films", { params });
806
+ return this.parseFilms(data);
807
+ }
808
+ /**
809
+ * Advanced film search with extended filters.
810
+ *
811
+ * Supports filtering by format (IMAX, 3D, Dolby Atmos), language,
812
+ * rating classification, runtime range, release date range, and sorting.
813
+ *
814
+ * @param filters - Extended search filters including format, language, runtime range, sorting
815
+ * @returns Array of matching films
816
+ */
817
+ async advancedSearch(filters) {
818
+ const params = {};
819
+ if (filters.siteId) params.siteId = filters.siteId;
820
+ if (filters.genre) params.genre = filters.genre;
821
+ if (filters.query) params.query = filters.query;
822
+ if (filters.nowShowing !== void 0) params.nowShowing = String(filters.nowShowing);
823
+ if (filters.comingSoon !== void 0) params.comingSoon = String(filters.comingSoon);
824
+ if (filters.limit !== void 0) params.limit = String(filters.limit);
825
+ if (filters.offset !== void 0) params.offset = String(filters.offset);
826
+ if (filters.ratingClassification) params.ratingClassification = filters.ratingClassification;
827
+ if (filters.format) params.format = filters.format;
828
+ if (filters.language) params.language = filters.language;
829
+ if (filters.releaseDateFrom) params.releaseDateFrom = filters.releaseDateFrom;
830
+ if (filters.releaseDateTo) params.releaseDateTo = filters.releaseDateTo;
831
+ if (filters.minRuntime !== void 0) params.minRuntime = String(filters.minRuntime);
832
+ if (filters.maxRuntime !== void 0) params.maxRuntime = String(filters.maxRuntime);
833
+ if (filters.sortBy) params.sortBy = filters.sortBy;
834
+ if (filters.sortOrder) params.sortOrder = filters.sortOrder;
835
+ const data = await this.http.get("/ocapi/v1/films/search", { params });
836
+ return this.parseFilms(data);
837
+ }
838
+ };
839
+
840
+ // src/types/order.ts
841
+ var import_zod7 = require("zod");
842
+ var orderStatusSchema = import_zod7.z.enum([
843
+ "draft",
844
+ "pending",
845
+ "held",
846
+ "confirmed",
847
+ "completed",
848
+ "cancelled",
849
+ "refunded"
850
+ ]);
851
+ var ticketSchema = import_zod7.z.object({
852
+ id: import_zod7.z.string(),
853
+ type: import_zod7.z.string(),
854
+ seatId: import_zod7.z.string(),
855
+ seatLabel: import_zod7.z.string(),
856
+ price: import_zod7.z.number().nonnegative(),
857
+ discount: import_zod7.z.number().nonnegative().optional()
858
+ });
859
+ var orderItemSchema = import_zod7.z.object({
860
+ id: import_zod7.z.string(),
861
+ name: import_zod7.z.string(),
862
+ category: import_zod7.z.string(),
863
+ quantity: import_zod7.z.number().int().positive(),
864
+ unitPrice: import_zod7.z.number().nonnegative(),
865
+ totalPrice: import_zod7.z.number().nonnegative()
866
+ });
867
+ var orderSchema = import_zod7.z.object({
868
+ id: import_zod7.z.string(),
869
+ sessionId: import_zod7.z.string(),
870
+ status: orderStatusSchema,
871
+ tickets: import_zod7.z.array(ticketSchema),
872
+ items: import_zod7.z.array(orderItemSchema),
873
+ subtotal: import_zod7.z.number().nonnegative(),
874
+ tax: import_zod7.z.number().nonnegative(),
875
+ discount: import_zod7.z.number().nonnegative(),
876
+ total: import_zod7.z.number().nonnegative(),
877
+ currency: import_zod7.z.string().length(3),
878
+ loyaltyMemberId: import_zod7.z.string().optional(),
879
+ loyaltyPointsEarned: import_zod7.z.number().int().nonnegative().optional(),
880
+ loyaltyPointsRedeemed: import_zod7.z.number().int().nonnegative().optional(),
881
+ createdAt: import_zod7.z.string(),
882
+ updatedAt: import_zod7.z.string().optional(),
883
+ heldAt: import_zod7.z.string().optional(),
884
+ heldUntil: import_zod7.z.string().optional(),
885
+ confirmedAt: import_zod7.z.string().optional(),
886
+ completedAt: import_zod7.z.string().optional(),
887
+ cancelledAt: import_zod7.z.string().optional(),
888
+ refundedAt: import_zod7.z.string().optional()
889
+ });
890
+ var orderHistoryFilterSchema = import_zod7.z.object({
891
+ status: orderStatusSchema.optional(),
892
+ since: import_zod7.z.string().optional(),
893
+ until: import_zod7.z.string().optional(),
894
+ limit: import_zod7.z.number().int().positive().max(100).optional(),
895
+ cursor: import_zod7.z.string().optional()
896
+ });
897
+
898
+ // src/types/pagination.ts
899
+ var import_zod8 = require("zod");
900
+ var paginationParamsSchema = import_zod8.z.object({
901
+ limit: import_zod8.z.number().int().positive().max(500).optional(),
902
+ cursor: import_zod8.z.string().optional(),
903
+ offset: import_zod8.z.number().int().nonnegative().optional()
904
+ }).refine(
905
+ (data) => !(data.cursor && data.offset !== void 0),
906
+ { message: "Cannot specify both cursor and offset \u2014 choose one pagination strategy" }
907
+ );
908
+ var paginatedResponseSchema = import_zod8.z.object({
909
+ data: import_zod8.z.array(import_zod8.z.unknown()),
910
+ total: import_zod8.z.number().int().nonnegative(),
911
+ hasMore: import_zod8.z.boolean(),
912
+ nextCursor: import_zod8.z.string().optional(),
913
+ nextOffset: import_zod8.z.number().int().nonnegative().optional(),
914
+ strategy: import_zod8.z.enum(["cursor", "offset"])
915
+ });
916
+
917
+ // src/resources/orders.ts
918
+ var import_zod9 = require("zod");
919
+ var OrdersResource = class {
920
+ constructor(http) {
921
+ this.http = http;
922
+ }
923
+ http;
924
+ /**
925
+ * Parse and validate a raw order response from the API.
926
+ * Throws a ZodError if the response doesn't match the expected shape.
927
+ */
928
+ parseOrder(data) {
929
+ return orderSchema.parse(data);
930
+ }
931
+ /**
932
+ * Parse and validate a paginated order response from the API.
933
+ */
934
+ parsePaginatedOrders(data) {
935
+ const envelopeSchema = paginatedResponseSchema.extend({
936
+ data: import_zod9.z.array(orderSchema)
937
+ });
938
+ const parsed = envelopeSchema.parse(data);
939
+ return parsed;
940
+ }
941
+ /**
942
+ * Create a new draft order for a session.
943
+ *
944
+ * @throws {TheatricalError} When the API returns an error response
945
+ * @throws {z.ZodError} When the response fails schema validation
946
+ * @param input - Session ID, initial tickets, optional F * @param input - Session ID, initial tickets, optional F&B items, optional loyalty memberB items, optional loyalty member
947
+ * @returns The newly created order in 'draft' status
948
+ */
949
+ async create(input) {
950
+ const data = await this.http.post("/ocapi/v1/orders", { body: input });
951
+ return this.parseOrder(data);
952
+ }
953
+ /**
954
+ * Retrieve an order by its unique identifier.
955
+ *
956
+ * @param orderId - The UUID of the order to retrieve
957
+ */
958
+ async get(orderId) {
959
+ const data = await this.http.get(`/ocapi/v1/orders/${orderId}`);
960
+ return this.parseOrder(data);
961
+ }
962
+ /**
963
+ * Set tickets on a draft order. Each ticket specifies a seat and ticket type.
964
+ * The Vista OCAPI replaces all existing tickets — pass the full desired set in one call.
965
+ *
966
+ * @param orderId - The UUID of the draft order
967
+ * @param input - Tickets to set (replaces any previously assigned tickets)
968
+ * @returns The updated order with the new ticket set
969
+ */
970
+ async addTickets(orderId, input) {
971
+ const data = await this.http.post(`/ocapi/v1/orders/${orderId}/tickets`, { body: input });
972
+ return this.parseOrder(data);
973
+ }
974
+ /**
975
+ * Add concession or merchandise items to an order.
976
+ * F&B items can be added to orders in 'draft' or 'pending' status.
977
+ *
978
+ * @param orderId - The UUID of the order
979
+ * @param input - Items to add (menuItemId + quantity pairs)
980
+ * @returns The updated order with new items included
981
+ */
982
+ async addItems(orderId, input) {
983
+ const data = await this.http.post(`/ocapi/v1/orders/${orderId}/items`, { body: input });
984
+ return this.parseOrder(data);
985
+ }
986
+ /**
987
+ * Confirm an order, transitioning it from 'pending' to 'confirmed'.
988
+ * This locks in the seat selection and pricing.
989
+ *
990
+ * @param orderId - The UUID of the order to confirm
991
+ */
992
+ async confirm(orderId) {
993
+ const data = await this.http.post(`/ocapi/v1/orders/${orderId}/confirm`);
994
+ return this.parseOrder(data);
995
+ }
996
+ /**
997
+ * Cancel an order. Can be applied to 'pending' or 'confirmed' orders.
998
+ * Releases any held seats back to the pool.
999
+ *
1000
+ * @param orderId - The UUID of the order to cancel
1001
+ */
1002
+ async cancel(orderId) {
1003
+ const data = await this.http.post(`/ocapi/v1/orders/${orderId}/cancel`);
1004
+ return this.parseOrder(data);
1005
+ }
1006
+ /**
1007
+ * Apply a loyalty membership discount to an order.
1008
+ * Links the member to the order and optionally redeems loyalty points
1009
+ * for a discount on the total.
1010
+ *
1011
+ * @param orderId - The UUID of the order
1012
+ * @param input - Loyalty member ID and optional points to redeem
1013
+ * @returns The updated order with loyalty discount applied
1014
+ */
1015
+ async applyLoyalty(orderId, input) {
1016
+ const data = await this.http.post(`/ocapi/v1/orders/${orderId}/loyalty`, { body: input });
1017
+ return this.parseOrder(data);
1018
+ }
1019
+ /**
1020
+ * Request a refund for a confirmed or completed order.
1021
+ * Triggers the refund workflow in Vista's payment system.
1022
+ *
1023
+ * @param orderId - The UUID of the order to refund
1024
+ */
1025
+ async refund(orderId) {
1026
+ const data = await this.http.post(`/ocapi/v1/orders/${orderId}/refund`);
1027
+ return this.parseOrder(data);
1028
+ }
1029
+ /**
1030
+ * Mark a confirmed order as completed (e.g., after the screening).
1031
+ *
1032
+ * @param orderId - The UUID of the confirmed order
1033
+ */
1034
+ async complete(orderId) {
1035
+ const data = await this.http.post(`/ocapi/v1/orders/${orderId}/complete`);
1036
+ return this.parseOrder(data);
1037
+ }
1038
+ /**
1039
+ * Retrieve order history for a loyalty member.
1040
+ * Returns a paginated list of orders, newest first.
1041
+ *
1042
+ * @param memberId - The loyalty member's unique identifier
1043
+ * @param filter - Optional filters for status, date range, and pagination
1044
+ */
1045
+ async history(memberId, filter) {
1046
+ const params = {};
1047
+ if (filter?.status) params.status = filter.status;
1048
+ if (filter?.since) params.since = filter.since;
1049
+ if (filter?.until) params.until = filter.until;
1050
+ if (filter?.limit) params.limit = String(filter.limit);
1051
+ if (filter?.cursor) params.cursor = filter.cursor;
1052
+ const data = await this.http.get(`/ocapi/v1/members/${memberId}/orders`, { params });
1053
+ return this.parsePaginatedOrders(data);
1054
+ }
1055
+ };
1056
+
1057
+ // src/resources/loyalty.ts
1058
+ var LoyaltyResource = class {
1059
+ constructor(http) {
1060
+ this.http = http;
1061
+ }
1062
+ http;
1063
+ /**
1064
+ * Fetch a loyalty member by their unique ID.
1065
+ *
1066
+ * @param memberId - Vista loyalty member identifier
1067
+ * @throws {TheatricalError} When the API returns an error response
1068
+ */
1069
+ async getMember(memberId) {
1070
+ return this.http.get(`/ocapi/v1/loyalty/members/${memberId}`);
1071
+ }
1072
+ /**
1073
+ * Authenticate a member using their email address and password (cookie-based
1074
+ * session). Returns the authenticated member record on success.
1075
+ *
1076
+ * @param email - Member's registered email
1077
+ * @param password - Account password (transmitted over TLS)
1078
+ */
1079
+ async authenticate(email, password) {
1080
+ return this.http.post("/ocapi/v1/loyalty/authenticate", {
1081
+ body: { email, password }
1082
+ });
1083
+ }
1084
+ /**
1085
+ * Return the current redeemable points balance for a member.
1086
+ *
1087
+ * @param memberId - Vista loyalty member identifier
1088
+ */
1089
+ async getPointsBalance(memberId) {
1090
+ return this.http.get(
1091
+ `/ocapi/v1/loyalty/members/${memberId}/points`
1092
+ );
1093
+ }
1094
+ /**
1095
+ * Retrieve a paginated transaction history for a member.
1096
+ *
1097
+ * @param memberId - Vista loyalty member identifier
1098
+ * @param filter - Optional date range, type, and pagination filters
1099
+ */
1100
+ async getHistory(memberId, filter) {
1101
+ return this.http.get(
1102
+ `/ocapi/v1/loyalty/members/${memberId}/history`,
1103
+ { params: filter }
1104
+ );
1105
+ }
1106
+ /**
1107
+ * List available redemption options for the loyalty program.
1108
+ * Options are filtered to those the member is eligible for based on their
1109
+ * tier and current points balance.
1110
+ *
1111
+ * @param memberId - Vista loyalty member identifier
1112
+ */
1113
+ async listRedemptionOptions(memberId) {
1114
+ return this.http.get(
1115
+ `/ocapi/v1/loyalty/members/${memberId}/redemptions`
1116
+ );
1117
+ }
1118
+ /**
1119
+ * Redeem points against a catalog option for a member.
1120
+ * Deducts points from the member's balance and applies the benefit.
1121
+ *
1122
+ * @param memberId - Vista loyalty member identifier
1123
+ * @param input - Redemption option, optional order linkage, and quantity
1124
+ */
1125
+ async redeemPoints(memberId, input) {
1126
+ return this.http.post(
1127
+ `/ocapi/v1/loyalty/members/${memberId}/redeem`,
1128
+ { body: input }
1129
+ );
1130
+ }
1131
+ };
1132
+
1133
+ // src/resources/subscriptions.ts
1134
+ var SubscriptionsResource = class {
1135
+ constructor(http) {
1136
+ this.http = http;
1137
+ }
1138
+ http;
1139
+ /**
1140
+ * List all available subscription plans for a Vista site.
1141
+ *
1142
+ * Returns only plans that are available for new subscriptions unless
1143
+ * {@link includeUnavailable} is set to true (useful for admin UIs).
1144
+ *
1145
+ * @param siteId - Vista site identifier to scope plans to
1146
+ * @param includeUnavailable - When true, include discontinued plans
1147
+ */
1148
+ async listPlans(siteId, includeUnavailable) {
1149
+ return this.http.get("/ocapi/v1/subscriptions/plans", {
1150
+ params: {
1151
+ ...siteId ? { siteId } : {},
1152
+ ...includeUnavailable ? { includeUnavailable: true } : {}
1153
+ }
1154
+ });
1155
+ }
1156
+ /**
1157
+ * Retrieve the active (or most recent) subscription for a member.
1158
+ *
1159
+ * @param memberId - Vista loyalty member identifier
1160
+ */
1161
+ async getMemberSubscription(memberId) {
1162
+ return this.http.get(`/ocapi/v1/subscriptions/members/${memberId}`);
1163
+ }
1164
+ /**
1165
+ * Get usage statistics for a member's subscription in the current billing period.
1166
+ *
1167
+ * Returns booking counts, remaining allowance, and per-benefit usage.
1168
+ * Use this before allowing a booking to check whether the member has
1169
+ * bookings remaining under their plan.
1170
+ *
1171
+ * @param memberId - Vista loyalty member identifier
1172
+ */
1173
+ async getUsage(memberId) {
1174
+ return this.http.get(
1175
+ `/ocapi/v1/subscriptions/members/${memberId}/usage`
1176
+ );
1177
+ }
1178
+ /**
1179
+ * Check whether a member is eligible to use a specific benefit.
1180
+ *
1181
+ * Returns true if the benefit is included in their plan, is currently active,
1182
+ * and the member has remaining uses for the current period.
1183
+ *
1184
+ * @param memberId - Vista loyalty member identifier
1185
+ * @param benefitId - Benefit ID from the plan's benefits array
1186
+ */
1187
+ async checkBenefitEligibility(memberId, benefitId) {
1188
+ return this.http.get(
1189
+ `/ocapi/v1/subscriptions/members/${memberId}/benefits/${benefitId}/eligibility`
1190
+ );
1191
+ }
1192
+ /**
1193
+ * Suspend a member's subscription temporarily.
1194
+ *
1195
+ * The subscription status transitions to `paused`. Billing is paused for the
1196
+ * suspension period. If {@link SuspendSubscriptionInput.resumeDate} is
1197
+ * provided, the subscription will auto-resume on that date; otherwise it must
1198
+ * be manually resumed.
1199
+ *
1200
+ * @param memberId - Vista loyalty member identifier
1201
+ * @param input - Optional resume date and reason
1202
+ */
1203
+ async suspend(memberId, input) {
1204
+ return this.http.post(
1205
+ `/ocapi/v1/subscriptions/members/${memberId}/suspend`,
1206
+ { body: input ?? {} }
1207
+ );
1208
+ }
1209
+ /**
1210
+ * Cancel a member's subscription.
1211
+ *
1212
+ * By default cancels at the end of the current billing period. Set
1213
+ * {@link CancelSubscriptionInput.immediate} to `true` for an immediate
1214
+ * cancellation with no refund.
1215
+ *
1216
+ * @param memberId - Vista loyalty member identifier
1217
+ * @param input - Whether to cancel immediately and optional reason
1218
+ */
1219
+ async cancel(memberId, input) {
1220
+ return this.http.post(
1221
+ `/ocapi/v1/subscriptions/members/${memberId}/cancel`,
1222
+ { body: input ?? {} }
1223
+ );
1224
+ }
1225
+ };
1226
+
1227
+ // src/resources/pricing.ts
1228
+ var PricingResource = class {
1229
+ constructor(http) {
1230
+ this.http = http;
1231
+ }
1232
+ http;
1233
+ /**
1234
+ * List available ticket types for a session.
1235
+ *
1236
+ * @param sessionId - The Vista session ID.
1237
+ * @param filter - Optional filter params (category, availableOnly).
1238
+ */
1239
+ async ticketTypes(sessionId, filter) {
1240
+ return this.http.get(`/ocapi/v1/sessions/${sessionId}/ticket-types`, {
1241
+ params: { ...filter }
1242
+ });
1243
+ }
1244
+ /**
1245
+ * Calculate the price for a ticket type and quantity.
1246
+ *
1247
+ * Returns an itemised {@link PriceBreakdown} including base price, discounts,
1248
+ * surcharges, and tax. The breakdown reflects the caller's Vista site tax
1249
+ * configuration (inclusive vs exclusive).
1250
+ *
1251
+ * @param sessionId - The Vista session ID.
1252
+ * @param ticketTypeId - Ticket type to price.
1253
+ * @param quantity - Number of tickets (default 1).
1254
+ */
1255
+ async calculate(sessionId, ticketTypeId, quantity = 1) {
1256
+ return this.http.get(`/ocapi/v1/pricing/calculate`, {
1257
+ params: { sessionId, ticketTypeId, quantity }
1258
+ });
1259
+ }
1260
+ /**
1261
+ * Apply coupon codes and resolve loyalty-tier discounts.
1262
+ *
1263
+ * Validates each coupon code, stacks applicable discounts, and returns
1264
+ * an updated {@link PriceBreakdown}. Invalid or expired codes are listed
1265
+ * in the `rejected` array rather than throwing.
1266
+ *
1267
+ * @param input - Session, ticket type, quantity, coupon codes, and optional member ID.
1268
+ */
1269
+ async applyCoupons(input) {
1270
+ return this.http.post(`/ocapi/v1/pricing/apply-coupons`, { body: input });
1271
+ }
1272
+ };
1273
+
1274
+ // src/resources/food-and-beverage.ts
1275
+ var FoodAndBeverageResource = class {
1276
+ constructor(http) {
1277
+ this.http = http;
1278
+ }
1279
+ http;
1280
+ /**
1281
+ * List menu items for a site, with optional filtering.
1282
+ *
1283
+ * Results include dietary tags and customisation options. Use `filter.dietary`
1284
+ * to surface only vegan / gluten-free items. Use `filter.preOrderOnly` when
1285
+ * building a pre-order flow tied to a session.
1286
+ *
1287
+ * @param siteId - The Vista site ID.
1288
+ * @param filter - Optional filter (dietary, category, availability).
1289
+ */
1290
+ async menu(siteId, filter) {
1291
+ return this.http.get(`/ocapi/v1/sites/${siteId}/menu`, {
1292
+ params: { ...filter, dietary: filter?.dietary?.join(",") }
1293
+ });
1294
+ }
1295
+ /**
1296
+ * List available menu categories for a site.
1297
+ *
1298
+ * Categories define the candy-bar section groupings (popcorn, drinks,
1299
+ * combos, snacks). Ordered by `displayOrder` ascending.
1300
+ *
1301
+ * @param siteId - The Vista site ID.
1302
+ */
1303
+ async categories(siteId) {
1304
+ return this.http.get(`/ocapi/v1/sites/${siteId}/menu/categories`);
1305
+ }
1306
+ /**
1307
+ * Fetch full detail for a single menu item.
1308
+ *
1309
+ * Includes customisation options (size, flavour) not always returned in
1310
+ * the full menu list. Use this before presenting an add-to-order form.
1311
+ *
1312
+ * @param siteId - The Vista site ID.
1313
+ * @param itemId - The menu item ID.
1314
+ */
1315
+ async itemDetail(siteId, itemId) {
1316
+ return this.http.get(`/ocapi/v1/sites/${siteId}/menu/items/${itemId}`);
1317
+ }
1318
+ /**
1319
+ * List combo deals available at a site.
1320
+ *
1321
+ * Combos are pre-configured bundles (e.g. "Large Popcorn + Large Drink").
1322
+ * The `savings` field on each {@link ComboOffer} indicates the discount
1323
+ * vs buying items individually — useful for upsell copy.
1324
+ *
1325
+ * @param siteId - The Vista site ID.
1326
+ */
1327
+ async combos(siteId) {
1328
+ return this.http.get(`/ocapi/v1/sites/${siteId}/menu/combos`);
1329
+ }
1330
+ /**
1331
+ * Add F&B items to an existing Vista order.
1332
+ *
1333
+ * Attaches concession items to a booking in progress. When adding
1334
+ * pre-order items tied to a specific session, pass `input.sessionId`
1335
+ * to enable collection-slot logic on the Vista side.
1336
+ *
1337
+ * Returns a confirmation with updated F&B subtotal for display.
1338
+ *
1339
+ * @param input - Order ID, items to add, optional session ID.
1340
+ */
1341
+ async addToOrder(input) {
1342
+ return this.http.post(
1343
+ `/ocapi/v1/orders/${input.orderId}/fnb`,
1344
+ { body: input }
1345
+ );
1346
+ }
1347
+ };
1348
+
1349
+ // src/http/retry.ts
1350
+ var DEFAULT_RETRY_CONFIG = {
1351
+ maxRetries: 3,
1352
+ baseDelay: 1e3,
1353
+ maxDelay: 3e4,
1354
+ jitter: true
1355
+ };
1356
+ function computeBackoffDelay(attempt, config) {
1357
+ const exponential = Math.min(
1358
+ config.baseDelay * Math.pow(2, attempt - 1),
1359
+ config.maxDelay
1360
+ );
1361
+ if (!config.jitter) return exponential;
1362
+ return Math.floor(exponential * (0.5 + Math.random() * 0.5));
1363
+ }
1364
+
1365
+ // src/http/interceptors.ts
1366
+ async function runInterceptors(value, interceptors) {
1367
+ let current = value;
1368
+ for (const interceptor of interceptors) {
1369
+ current = await interceptor(current);
1370
+ }
1371
+ return current;
1372
+ }
1373
+
1374
+ // src/http/client.ts
1375
+ var TheatricalHTTPClient = class {
1376
+ config;
1377
+ constructor(config) {
1378
+ this.config = config;
1379
+ }
1380
+ async get(path, options) {
1381
+ return this.request({ ...options, method: "GET" }, path);
1382
+ }
1383
+ async post(path, options) {
1384
+ return this.request({ ...options, method: "POST" }, path);
1385
+ }
1386
+ async put(path, options) {
1387
+ return this.request({ ...options, method: "PUT" }, path);
1388
+ }
1389
+ async delete(path, options) {
1390
+ return this.request({ ...options, method: "DELETE" }, path);
1391
+ }
1392
+ async request(options, path, attempt = 1) {
1393
+ if (this.config.rateLimiter) {
1394
+ await this.config.rateLimiter.waitForSlot();
1395
+ }
1396
+ const token = await this.config.tokenManager.getToken();
1397
+ const url = this.buildUrl(path, options.params);
1398
+ const requestId = this.generateRequestId();
1399
+ if (this.config.debug) {
1400
+ console.log(`[theatrical] ${options.method} ${url} (${requestId})`);
1401
+ }
1402
+ const serialisedBody = options.body ? JSON.stringify(options.body) : void 0;
1403
+ let requestConfig = {
1404
+ url,
1405
+ method: options.method ?? "GET",
1406
+ headers: {
1407
+ "Authorization": `Bearer ${token}`,
1408
+ "Content-Type": "application/json",
1409
+ "X-Request-ID": requestId,
1410
+ ...options.headers
1411
+ },
1412
+ body: serialisedBody
1413
+ };
1414
+ if (this.config.onRequest?.length) {
1415
+ requestConfig = await runInterceptors(requestConfig, this.config.onRequest);
1416
+ }
1417
+ const controller = new AbortController();
1418
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
1419
+ try {
1420
+ let response = await fetch(requestConfig.url, {
1421
+ method: requestConfig.method,
1422
+ headers: requestConfig.headers,
1423
+ body: requestConfig.body,
1424
+ signal: controller.signal
1425
+ });
1426
+ clearTimeout(timeoutId);
1427
+ if (response.ok) {
1428
+ if (this.config.onResponse?.length) {
1429
+ response = await runInterceptors(response, this.config.onResponse);
1430
+ }
1431
+ return await response.json();
1432
+ }
1433
+ if (response.status === 401) {
1434
+ this.config.tokenManager.invalidate();
1435
+ if (attempt <= 1) {
1436
+ return this.request(options, path, attempt + 1);
1437
+ }
1438
+ }
1439
+ const error = await parseErrorResponse(response, requestConfig.url);
1440
+ if ((error instanceof RateLimitError || error instanceof ServerError) && attempt <= this.config.maxRetries) {
1441
+ const retryConfig = this.config.retry ?? { ...DEFAULT_RETRY_CONFIG, maxRetries: this.config.maxRetries };
1442
+ const delayMs = error instanceof RateLimitError ? error.retryAfter * 1e3 : computeBackoffDelay(attempt, retryConfig);
1443
+ await this.delay(delayMs);
1444
+ return this.request(options, path, attempt + 1);
1445
+ }
1446
+ throw error;
1447
+ } catch (error) {
1448
+ clearTimeout(timeoutId);
1449
+ if (error instanceof TheatricalError) {
1450
+ throw error;
1451
+ }
1452
+ if (error instanceof DOMException && error.name === "AbortError") {
1453
+ throw new TheatricalError("Request timed out", 408, void 0, requestId);
1454
+ }
1455
+ throw new TheatricalError(
1456
+ `Network error: ${error.message}`,
1457
+ 0,
1458
+ void 0,
1459
+ requestId
1460
+ );
1461
+ }
1462
+ }
1463
+ buildUrl(path, params) {
1464
+ const url = new URL(path, this.config.baseUrl);
1465
+ if (params) {
1466
+ Object.entries(params).forEach(([key, value]) => {
1467
+ if (value !== void 0) {
1468
+ url.searchParams.set(key, String(value));
1469
+ }
1470
+ });
1471
+ }
1472
+ return url.toString();
1473
+ }
1474
+ generateRequestId() {
1475
+ return `th_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1476
+ }
1477
+ delay(ms) {
1478
+ return new Promise((resolve) => setTimeout(resolve, ms));
1479
+ }
1480
+ };
1481
+
1482
+ // src/auth/gas-client.ts
1483
+ var GASClient = class {
1484
+ config;
1485
+ constructor(config) {
1486
+ this.config = config;
1487
+ }
1488
+ /**
1489
+ * Request a new JWT token from the Global Authentication Service
1490
+ */
1491
+ async requestToken() {
1492
+ const response = await fetch(`${this.config.authUrl}/oauth/token`, {
1493
+ method: "POST",
1494
+ headers: {
1495
+ "Content-Type": "application/json"
1496
+ },
1497
+ body: JSON.stringify({
1498
+ grant_type: "client_credentials",
1499
+ api_key: this.config.apiKey
1500
+ })
1501
+ });
1502
+ if (!response.ok) {
1503
+ throw new Error(`GAS authentication failed: ${response.status} ${response.statusText}`);
1504
+ }
1505
+ const data = await response.json();
1506
+ return {
1507
+ accessToken: data.access_token,
1508
+ tokenType: data.token_type ?? "Bearer",
1509
+ expiresIn: data.expires_in ?? 3600,
1510
+ issuedAt: Date.now()
1511
+ };
1512
+ }
1513
+ };
1514
+
1515
+ // src/auth/token-manager.ts
1516
+ var TokenManager = class {
1517
+ gasClient;
1518
+ currentToken = null;
1519
+ refreshPromise = null;
1520
+ /** Buffer before expiry to trigger refresh (5 minutes) */
1521
+ expiryBuffer = 5 * 60 * 1e3;
1522
+ constructor(gasClient) {
1523
+ this.gasClient = gasClient;
1524
+ }
1525
+ /**
1526
+ * Get a valid access token, refreshing if necessary.
1527
+ * Deduplicates concurrent refresh requests.
1528
+ */
1529
+ async getToken() {
1530
+ if (this.currentToken && !this.isExpired(this.currentToken)) {
1531
+ return this.currentToken.accessToken;
1532
+ }
1533
+ if (!this.refreshPromise) {
1534
+ this.refreshPromise = this.refresh();
1535
+ }
1536
+ try {
1537
+ const token = await this.refreshPromise;
1538
+ return token.accessToken;
1539
+ } finally {
1540
+ this.refreshPromise = null;
1541
+ }
1542
+ }
1543
+ /**
1544
+ * Force a token refresh regardless of current token state
1545
+ */
1546
+ async refresh() {
1547
+ const token = await this.gasClient.requestToken();
1548
+ this.currentToken = token;
1549
+ return token;
1550
+ }
1551
+ /**
1552
+ * Check if a token is expired or within the expiry buffer
1553
+ */
1554
+ isExpired(token) {
1555
+ const expiresAt = token.issuedAt + token.expiresIn * 1e3;
1556
+ return Date.now() >= expiresAt - this.expiryBuffer;
1557
+ }
1558
+ /**
1559
+ * Clear the current token (e.g., on 401 response)
1560
+ */
1561
+ invalidate() {
1562
+ this.currentToken = null;
1563
+ }
1564
+ };
1565
+
1566
+ // src/http/rate-limiter.ts
1567
+ var DEFAULT_RATE_LIMITER_CONFIG = {
1568
+ maxRequests: 60,
1569
+ windowMs: 6e4
1570
+ };
1571
+ var RateLimiter = class {
1572
+ config;
1573
+ timestamps = [];
1574
+ /**
1575
+ * Creates a new RateLimiter.
1576
+ * @param config - Limits and window size. Defaults to {@link DEFAULT_RATE_LIMITER_CONFIG}.
1577
+ */
1578
+ constructor(config = DEFAULT_RATE_LIMITER_CONFIG) {
1579
+ this.config = config;
1580
+ }
1581
+ /**
1582
+ * Waits until a request slot is available within the current window,
1583
+ * records the request timestamp, and resolves.
1584
+ *
1585
+ * Callers `await` this before making an HTTP request. If the limit has been
1586
+ * reached, the call suspends until the oldest in-window request expires.
1587
+ *
1588
+ * @returns Promise that resolves when it is safe to proceed with a request
1589
+ */
1590
+ async waitForSlot() {
1591
+ for (; ; ) {
1592
+ const now = Date.now();
1593
+ const windowStart = now - this.config.windowMs;
1594
+ while (this.timestamps.length > 0 && this.timestamps[0] < windowStart) {
1595
+ this.timestamps.shift();
1596
+ }
1597
+ if (this.timestamps.length < this.config.maxRequests) {
1598
+ this.timestamps.push(now);
1599
+ return;
1600
+ }
1601
+ const oldestTs = this.timestamps[0];
1602
+ const waitMs = oldestTs + this.config.windowMs - now + 1;
1603
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
1604
+ }
1605
+ }
1606
+ /**
1607
+ * Returns the number of requests recorded within the current rolling window.
1608
+ * Useful for diagnostics and integration tests.
1609
+ */
1610
+ get activeCount() {
1611
+ const windowStart = Date.now() - this.config.windowMs;
1612
+ return this.timestamps.filter((ts) => ts >= windowStart).length;
1613
+ }
1614
+ /**
1615
+ * Clears all recorded timestamps, resetting the limiter to a clean state.
1616
+ * Primarily intended for use in tests.
1617
+ */
1618
+ reset() {
1619
+ this.timestamps.length = 0;
1620
+ }
1621
+ };
1622
+
1623
+ // src/mock/fixtures.ts
1624
+ var DEFAULT_MOCK_RESPONSES = {
1625
+ // Films
1626
+ "/ocapi/v1/films/now-showing": [
1627
+ {
1628
+ id: "film_holdovers_2023",
1629
+ title: "The Holdovers",
1630
+ synopsis: "A cranky history teacher is forced to stay on campus over the holidays with a troubled student.",
1631
+ genres: ["drama", "comedy"],
1632
+ runtime: 133,
1633
+ rating: { classification: "M", description: "Offensive language" },
1634
+ releaseDate: "2026-03-14",
1635
+ posterUrl: "https://image.tmdb.org/t/p/w500/VHSEkBNbAoEGfBmiGGOhEHaGMsD.jpg",
1636
+ cast: [{ name: "Paul Giamatti", role: "Paul Hunham" }, { name: "Dominic Sessa", role: "Angus Tully" }],
1637
+ director: "Alexander Payne",
1638
+ isNowShowing: true,
1639
+ isComingSoon: false
1640
+ },
1641
+ {
1642
+ id: "film_poor_things_2023",
1643
+ title: "Poor Things",
1644
+ synopsis: "The incredible tale about the fantastical evolution of Bella Baxter.",
1645
+ genres: ["comedy", "drama", "sci-fi"],
1646
+ runtime: 141,
1647
+ rating: { classification: "R18", description: "Graphic sexual content, offensive language" },
1648
+ releaseDate: "2026-02-01",
1649
+ posterUrl: "https://image.tmdb.org/t/p/w500/kCGlIMHnOm8JPXIhMLGUMEFbV4B.jpg",
1650
+ cast: [{ name: "Emma Stone", role: "Bella Baxter" }, { name: "Mark Ruffalo", role: "Duncan Wedderburn" }],
1651
+ director: "Yorgos Lanthimos",
1652
+ isNowShowing: true,
1653
+ isComingSoon: false
1654
+ }
1655
+ ],
1656
+ "/ocapi/v1/films/coming-soon": [
1657
+ {
1658
+ id: "film_dune2_2024",
1659
+ title: "Dune: Part Two",
1660
+ synopsis: "Paul Atreides unites with Chani and the Fremen while seeking revenge against the conspirators.",
1661
+ genres: ["sci-fi", "adventure"],
1662
+ runtime: 166,
1663
+ rating: { classification: "M", description: "Violence" },
1664
+ releaseDate: "2026-05-01",
1665
+ posterUrl: "https://image.tmdb.org/t/p/w500/czembW0Rk1Ke7lCJGahbOhdCuhx.jpg",
1666
+ cast: [{ name: "Timoth\xE9e Chalamet", role: "Paul Atreides" }, { name: "Zendaya", role: "Chani" }],
1667
+ director: "Denis Villeneuve",
1668
+ isNowShowing: false,
1669
+ isComingSoon: true
1670
+ }
1671
+ ],
1672
+ "/ocapi/v1/films/:id": {
1673
+ id: "film_holdovers_2023",
1674
+ title: "The Holdovers",
1675
+ synopsis: "A cranky history teacher is forced to stay on campus over the holidays with a troubled student.",
1676
+ genres: ["drama", "comedy"],
1677
+ runtime: 133,
1678
+ rating: { classification: "M", description: "Offensive language" },
1679
+ releaseDate: "2026-03-14",
1680
+ posterUrl: "https://image.tmdb.org/t/p/w500/VHSEkBNbAoEGfBmiGGOhEHaGMsD.jpg",
1681
+ cast: [{ name: "Paul Giamatti", role: "Paul Hunham" }],
1682
+ director: "Alexander Payne",
1683
+ crew: [{ name: "Alexander Payne", department: "Directing", job: "Director" }],
1684
+ ratings: [{ source: "TMDB", score: "7.9", outOf: "10" }],
1685
+ formats: ["2D"],
1686
+ languages: ["en"],
1687
+ isNowShowing: true,
1688
+ isComingSoon: false
1689
+ },
1690
+ // Sessions
1691
+ "/ocapi/v1/sessions": {
1692
+ sessions: [
1693
+ {
1694
+ id: "ses_roxy_holdovers_20260427_1915",
1695
+ filmId: "film_holdovers_2023",
1696
+ filmTitle: "The Holdovers",
1697
+ siteId: "site_roxy_wellington",
1698
+ screenId: "scr_roxy_3",
1699
+ screenName: "Screen 3",
1700
+ startTime: "2026-04-27T19:15:00+12:00",
1701
+ endTime: "2026-04-27T21:28:00+12:00",
1702
+ format: "2D",
1703
+ isBookable: true,
1704
+ isSoldOut: false,
1705
+ seatsAvailable: 74,
1706
+ seatsTotal: 120,
1707
+ priceFrom: 19.5,
1708
+ attributes: {}
1709
+ },
1710
+ {
1711
+ id: "ses_roxy_poor_things_20260427_2000",
1712
+ filmId: "film_poor_things_2023",
1713
+ filmTitle: "Poor Things",
1714
+ siteId: "site_roxy_wellington",
1715
+ screenId: "scr_roxy_2",
1716
+ screenName: "Screen 2 \u2014 IMAX",
1717
+ startTime: "2026-04-27T20:00:00+12:00",
1718
+ endTime: "2026-04-27T22:21:00+12:00",
1719
+ format: "IMAX",
1720
+ isBookable: true,
1721
+ isSoldOut: false,
1722
+ seatsAvailable: 28,
1723
+ seatsTotal: 140,
1724
+ priceFrom: 25,
1725
+ attributes: {}
1726
+ }
1727
+ ],
1728
+ total: 2,
1729
+ hasMore: false
1730
+ },
1731
+ "/ocapi/v1/sessions/:id": {
1732
+ id: "ses_roxy_holdovers_20260427_1915",
1733
+ filmId: "film_holdovers_2023",
1734
+ filmTitle: "The Holdovers",
1735
+ siteId: "site_roxy_wellington",
1736
+ screenId: "scr_roxy_3",
1737
+ screenName: "Screen 3",
1738
+ startTime: "2026-04-27T19:15:00+12:00",
1739
+ endTime: "2026-04-27T21:28:00+12:00",
1740
+ format: "2D",
1741
+ isBookable: true,
1742
+ isSoldOut: false,
1743
+ seatsAvailable: 74,
1744
+ seatsTotal: 120,
1745
+ priceFrom: 19.5,
1746
+ attributes: {}
1747
+ },
1748
+ "/ocapi/v1/sessions/:id/seat-plan": {
1749
+ sessionId: "ses_roxy_holdovers_20260427_1915",
1750
+ screenName: "Screen 3",
1751
+ seats: Array.from(
1752
+ { length: 10 },
1753
+ (_, row) => Array.from({ length: 12 }, (_2, col) => ({
1754
+ id: `seat_${String.fromCharCode(65 + row)}${col + 1}`,
1755
+ row: String.fromCharCode(65 + row),
1756
+ number: col + 1,
1757
+ status: row < 3 && col > 8 ? "taken" : "available",
1758
+ x: col,
1759
+ y: row,
1760
+ type: row < 2 ? "premium" : "standard",
1761
+ isAccessible: false
1762
+ }))
1763
+ ).flat(),
1764
+ rowCount: 10,
1765
+ screenPosition: "top",
1766
+ availableCount: 74,
1767
+ totalCount: 120
1768
+ },
1769
+ // Sites
1770
+ "/ocapi/v1/sites": [
1771
+ {
1772
+ id: "site_roxy_wellington",
1773
+ name: "Roxy Cinema",
1774
+ address: { line1: "5 Park Road", city: "Wellington", postalCode: "6022", country: "NZ" },
1775
+ location: { latitude: -41.319, longitude: 174.8127 },
1776
+ screens: [
1777
+ { id: "scr_roxy_1", name: "Screen 1", seatCount: 80, formats: ["2D"], isAccessible: true },
1778
+ { id: "scr_roxy_2", name: "Screen 2 \u2014 IMAX", seatCount: 140, formats: ["IMAX", "2D"], isAccessible: true },
1779
+ { id: "scr_roxy_3", name: "Screen 3", seatCount: 120, formats: ["2D", "3D"], isAccessible: true }
1780
+ ],
1781
+ config: { bookingLeadTime: 15, maxTicketsPerOrder: 8, loyaltyEnabled: true, fnbEnabled: true },
1782
+ timezone: "Pacific/Auckland",
1783
+ currency: "NZD",
1784
+ isActive: true,
1785
+ amenities: [
1786
+ { id: "bar", label: "Bar" },
1787
+ { id: "wheelchair", label: "Wheelchair Access" },
1788
+ { id: "parking", label: "Free Parking" }
1789
+ ]
1790
+ }
1791
+ ],
1792
+ "/ocapi/v1/sites/:id": {
1793
+ id: "site_roxy_wellington",
1794
+ name: "Roxy Cinema",
1795
+ address: { line1: "5 Park Road", city: "Wellington", postalCode: "6022", country: "NZ" },
1796
+ location: { latitude: -41.319, longitude: 174.8127 },
1797
+ screens: [
1798
+ { id: "scr_roxy_1", name: "Screen 1", seatCount: 80, formats: ["2D"], isAccessible: true }
1799
+ ],
1800
+ config: { bookingLeadTime: 15, maxTicketsPerOrder: 8, loyaltyEnabled: true, fnbEnabled: true },
1801
+ timezone: "Pacific/Auckland",
1802
+ currency: "NZD",
1803
+ isActive: true
1804
+ },
1805
+ // Orders
1806
+ "/ocapi/v1/orders": {
1807
+ id: "ord_mock_001",
1808
+ status: "draft",
1809
+ siteId: "site_roxy_wellington",
1810
+ sessionId: "ses_roxy_holdovers_20260427_1915",
1811
+ tickets: [],
1812
+ items: [],
1813
+ pricing: { subtotal: 0, tax: 0, discounts: 0, total: 0 },
1814
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1815
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1816
+ },
1817
+ // Loyalty
1818
+ "/ocapi/v1/loyalty/members/:id": {
1819
+ id: "mem_hemi_walker_5528",
1820
+ email: "hemi.walker@example.co.nz",
1821
+ firstName: "Hemi",
1822
+ lastName: "Walker",
1823
+ tier: {
1824
+ id: "tier_gold",
1825
+ name: "Gold",
1826
+ level: 3,
1827
+ benefits: ["10% off all tickets", "Free large popcorn monthly", "Priority booking"],
1828
+ pointsThreshold: 5e3
1829
+ },
1830
+ points: 2840,
1831
+ lifetimePoints: 8640,
1832
+ memberSince: "2024-01-15",
1833
+ active: true
1834
+ }
1835
+ };
1836
+
1837
+ // src/mock/mock-http-adapter.ts
1838
+ var MockHTTPAdapter = class {
1839
+ responses;
1840
+ constructor(overrides) {
1841
+ this.responses = { ...DEFAULT_MOCK_RESPONSES, ...overrides };
1842
+ }
1843
+ /** Normalise a concrete path to a pattern key by replacing segments that look like IDs. */
1844
+ matchPattern(path) {
1845
+ const cleanPath = path.split("?")[0];
1846
+ return cleanPath.replace(/\/([a-z_]+-[a-z_\d-]+|\d+)(?=\/|$)/gi, "/:id");
1847
+ }
1848
+ lookup(path) {
1849
+ if (this.responses[path] !== void 0) return this.responses[path];
1850
+ const pattern = this.matchPattern(path);
1851
+ if (this.responses[pattern] !== void 0) return this.responses[pattern];
1852
+ return void 0;
1853
+ }
1854
+ async get(path) {
1855
+ const data = this.lookup(path);
1856
+ if (data === void 0) {
1857
+ throw new NotFoundError(`Mock: no fixture for GET ${path}`, path.split("/").pop() ?? path);
1858
+ }
1859
+ return data;
1860
+ }
1861
+ async post(path, options) {
1862
+ const data = this.lookup(path);
1863
+ if (data !== void 0) return data;
1864
+ if (path.includes("/orders")) {
1865
+ return {
1866
+ id: `ord_mock_${Date.now()}`,
1867
+ status: "draft",
1868
+ tickets: [],
1869
+ items: [],
1870
+ pricing: { subtotal: 0, tax: 0, discounts: 0, total: 0 },
1871
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1872
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1873
+ ...options?.body && typeof options.body === "object" ? options.body : {}
1874
+ };
1875
+ }
1876
+ return {};
1877
+ }
1878
+ async put(path, options) {
1879
+ const data = this.lookup(path);
1880
+ if (data !== void 0) return data;
1881
+ return { ...options?.body && typeof options.body === "object" ? options.body : {} };
1882
+ }
1883
+ async delete(path) {
1884
+ return {};
1885
+ }
1886
+ };
1887
+
1888
+ // src/client.ts
1889
+ var ENVIRONMENT_URLS = {
1890
+ sandbox: "https://api-sandbox.vista.co",
1891
+ staging: "https://api-staging.vista.co",
1892
+ production: "https://api.vista.co"
1893
+ };
1894
+ var _globalInstance;
1895
+ var TheatricalClient = class _TheatricalClient {
1896
+ config;
1897
+ httpClient;
1898
+ tokenManager;
1899
+ // Lazily initialised resource modules
1900
+ _sessions;
1901
+ _sites;
1902
+ _films;
1903
+ _orders;
1904
+ _loyalty;
1905
+ _subscriptions;
1906
+ _pricing;
1907
+ _fnb;
1908
+ /**
1909
+ * Constructs a TheatricalClient with validated configuration.
1910
+ *
1911
+ * Throws `ValidationError` immediately if config is invalid.
1912
+ *
1913
+ * @param config - SDK configuration
1914
+ * @throws {ValidationError} if config fails schema validation
1915
+ */
1916
+ constructor(config) {
1917
+ this.config = validateConfig(config);
1918
+ const baseUrl = this.config.baseUrl ?? ENVIRONMENT_URLS[this.config.environment];
1919
+ const gasClient = new GASClient({
1920
+ apiKey: this.config.apiKey,
1921
+ authUrl: "https://auth.moviexchange.com"
1922
+ });
1923
+ this.tokenManager = new TokenManager(gasClient);
1924
+ this.httpClient = new TheatricalHTTPClient({
1925
+ baseUrl,
1926
+ timeout: this.config.timeout,
1927
+ maxRetries: this.config.maxRetries,
1928
+ tokenManager: this.tokenManager,
1929
+ debug: this.config.debug,
1930
+ rateLimiter: new RateLimiter(DEFAULT_RATE_LIMITER_CONFIG)
1931
+ });
1932
+ }
1933
+ /**
1934
+ * Creates a new TheatricalClient instance.
1935
+ *
1936
+ * Equivalent to `new TheatricalClient(config)` but provided as a static
1937
+ * factory for clarity in configuration-heavy setups.
1938
+ *
1939
+ * @param config - SDK configuration
1940
+ * @returns New TheatricalClient instance
1941
+ * @throws {ValidationError} if config fails schema validation
1942
+ *
1943
+ * @example
1944
+ * ```typescript
1945
+ * const client = TheatricalClient.create({
1946
+ * apiKey: process.env.THEATRICAL_API_KEY!,
1947
+ * environment: 'production',
1948
+ * timeout: 15_000,
1949
+ * });
1950
+ * ```
1951
+ */
1952
+ static create(config) {
1953
+ return new _TheatricalClient(config);
1954
+ }
1955
+ /**
1956
+ * Returns the global singleton TheatricalClient instance.
1957
+ *
1958
+ * Throws if no global instance has been configured via `setGlobal()`.
1959
+ * Useful for applications that initialise the client once at startup and
1960
+ * access it throughout without passing the instance around.
1961
+ *
1962
+ * @returns The global TheatricalClient instance
1963
+ * @throws {Error} if setGlobal() has not been called
1964
+ *
1965
+ * @example
1966
+ * ```typescript
1967
+ * // At startup:
1968
+ * TheatricalClient.setGlobal({ apiKey: 'key', environment: 'production' });
1969
+ *
1970
+ * // Anywhere in your app:
1971
+ * const client = TheatricalClient.global();
1972
+ * const films = await client.films.nowShowing({ siteId: 'roxy-wellington' });
1973
+ * ```
1974
+ */
1975
+ static global() {
1976
+ if (!_globalInstance) {
1977
+ throw new Error(
1978
+ "No global TheatricalClient configured. Call TheatricalClient.setGlobal(config) first."
1979
+ );
1980
+ }
1981
+ return _globalInstance;
1982
+ }
1983
+ /**
1984
+ * Configures the global singleton TheatricalClient instance.
1985
+ *
1986
+ * Replaces any existing global instance. Subsequent calls to
1987
+ * `TheatricalClient.global()` will return this instance.
1988
+ *
1989
+ * @param config - SDK configuration
1990
+ * @throws {ValidationError} if config fails schema validation
1991
+ *
1992
+ * @example
1993
+ * ```typescript
1994
+ * TheatricalClient.setGlobal({
1995
+ * apiKey: process.env.THEATRICAL_API_KEY!,
1996
+ * environment: 'production',
1997
+ * });
1998
+ * ```
1999
+ */
2000
+ static setGlobal(config) {
2001
+ _globalInstance = new _TheatricalClient(config);
2002
+ }
2003
+ /**
2004
+ * Resets the global singleton instance.
2005
+ *
2006
+ * Primarily intended for testing — allows tests to start with a clean slate.
2007
+ *
2008
+ * @example
2009
+ * ```typescript
2010
+ * afterEach(() => {
2011
+ * TheatricalClient.resetGlobal();
2012
+ * });
2013
+ * ```
2014
+ */
2015
+ static resetGlobal() {
2016
+ _globalInstance = void 0;
2017
+ }
2018
+ /**
2019
+ * Creates an offline mock client that returns pre-defined NZ cinema fixture data.
2020
+ *
2021
+ * Ideal for demos, prototypes, and evaluators without a Vista API key.
2022
+ * The mock covers films, sessions, sites, orders, and loyalty endpoints.
2023
+ *
2024
+ * @param fixtureOverrides - Optional URL-keyed fixture map to replace specific responses
2025
+ *
2026
+ * @example
2027
+ * ```typescript
2028
+ * const client = import.meta.env.VITE_THEATRICAL_MOCK
2029
+ * ? TheatricalClient.createMock()
2030
+ * : TheatricalClient.create({ apiKey: process.env.THEATRICAL_API_KEY! });
2031
+ * ```
2032
+ *
2033
+ * @see https://developer.vista.co
2034
+ */
2035
+ static createMock(fixtureOverrides) {
2036
+ const instance = Object.create(_TheatricalClient.prototype);
2037
+ instance.httpClient = new MockHTTPAdapter(fixtureOverrides);
2038
+ instance.config = { apiKey: "mock", environment: "sandbox", timeout: 1e4, maxRetries: 0, debug: false };
2039
+ instance.tokenManager = { getToken: async () => "mock-token", invalidate: () => {
2040
+ } };
2041
+ return instance;
2042
+ }
2043
+ /** Sessions — showtimes, availability, seat maps */
2044
+ get sessions() {
2045
+ if (!this._sessions) {
2046
+ this._sessions = new SessionsResource(this.httpClient);
2047
+ }
2048
+ return this._sessions;
2049
+ }
2050
+ /** Sites — cinema locations, screens, configurations */
2051
+ get sites() {
2052
+ if (!this._sites) {
2053
+ this._sites = new SitesResource(this.httpClient);
2054
+ }
2055
+ return this._sites;
2056
+ }
2057
+ /** Films — now showing, coming soon, search */
2058
+ get films() {
2059
+ if (!this._films) {
2060
+ this._films = new FilmsResource(this.httpClient);
2061
+ }
2062
+ return this._films;
2063
+ }
2064
+ /** Orders — booking lifecycle: create, confirm, cancel */
2065
+ get orders() {
2066
+ if (!this._orders) {
2067
+ this._orders = new OrdersResource(this.httpClient);
2068
+ }
2069
+ return this._orders;
2070
+ }
2071
+ /** Loyalty — member management, points, tiers */
2072
+ get loyalty() {
2073
+ if (!this._loyalty) {
2074
+ this._loyalty = new LoyaltyResource(this.httpClient);
2075
+ }
2076
+ return this._loyalty;
2077
+ }
2078
+ /** Subscriptions — plans, member subscriptions */
2079
+ get subscriptions() {
2080
+ if (!this._subscriptions) {
2081
+ this._subscriptions = new SubscriptionsResource(this.httpClient);
2082
+ }
2083
+ return this._subscriptions;
2084
+ }
2085
+ /** Pricing — ticket types, price calculations, tax handling */
2086
+ get pricing() {
2087
+ if (!this._pricing) {
2088
+ this._pricing = new PricingResource(this.httpClient);
2089
+ }
2090
+ return this._pricing;
2091
+ }
2092
+ /** Food & Beverage — menus, ordering, dietary information */
2093
+ get fnb() {
2094
+ if (!this._fnb) {
2095
+ this._fnb = new FoodAndBeverageResource(this.httpClient);
2096
+ }
2097
+ return this._fnb;
2098
+ }
2099
+ };
2100
+
2101
+ // src/version.ts
2102
+ var SDK_VERSION = "0.1.0";
2103
+ var SDK_USER_AGENT = `theatrical-sdk/${SDK_VERSION} (Node.js)`;
2104
+ // Annotate the CommonJS export names for ESM import in node:
2105
+ 0 && (module.exports = {
2106
+ AuthenticationError,
2107
+ MockHTTPAdapter,
2108
+ NotFoundError,
2109
+ RateLimitError,
2110
+ SDK_USER_AGENT,
2111
+ SDK_VERSION,
2112
+ ServerError,
2113
+ TheatricalClient,
2114
+ TheatricalError,
2115
+ ValidationError
2116
+ });