@peektravel/app-utilities 0.1.1

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,2924 @@
1
+ import { randomUUID } from 'crypto';
2
+ import * as jwt from 'jsonwebtoken';
3
+
4
+ // src/internal/gateway-endpoints.ts
5
+ var SALES_ENDPOINT = "sales";
6
+
7
+ // src/internal/account-users/account-user-converter.ts
8
+ var ACTIVE_STATUS = "ACTIVE";
9
+ function fromAccountUserNode(node) {
10
+ if (!node || node.status !== ACTIVE_STATUS) {
11
+ return null;
12
+ }
13
+ return {
14
+ id: node.id,
15
+ name: node.name,
16
+ email: node.email,
17
+ phone: node.phone,
18
+ assignedActivities: (node.assignedActivities ?? []).map((activity) => ({
19
+ id: activity.id,
20
+ name: activity.name
21
+ }))
22
+ };
23
+ }
24
+ function fromAccountUserNodes(nodes) {
25
+ const users = [];
26
+ for (const node of nodes) {
27
+ const user = fromAccountUserNode(node);
28
+ if (user) {
29
+ users.push(user);
30
+ }
31
+ }
32
+ return users;
33
+ }
34
+
35
+ // src/internal/account-users/account-user-queries.ts
36
+ var USER_QUERY = `
37
+ query Sales($first: Int, $after: String) {
38
+ accountUsers(first: $first, after: $after) {
39
+ pageInfo {
40
+ endCursor
41
+ hasNextPage
42
+ }
43
+ edges {
44
+ cursor
45
+ node {
46
+ id
47
+ name
48
+ email
49
+ phone
50
+ status
51
+ assignedActivities {
52
+ id
53
+ name
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ `;
60
+ var USER_BY_FILTER_QUERY = `
61
+ query Sales($first: Int, $filter: AccountUsersFilter) {
62
+ accountUsers(first: $first, filter: $filter) {
63
+ edges {
64
+ cursor
65
+ node {
66
+ id
67
+ name
68
+ email
69
+ phone
70
+ status
71
+ assignedActivities {
72
+ id
73
+ name
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ `;
80
+
81
+ // src/internal/account-users/account-user-service.ts
82
+ var DEFAULT_PAGE_SIZE = 50;
83
+ var AccountUserService = class {
84
+ constructor(client, options = {}) {
85
+ this.client = client;
86
+ this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
87
+ }
88
+ client;
89
+ pageSize;
90
+ /**
91
+ * Returns all active account users, walking the cursor pagination to the end.
92
+ * Inactive users are omitted.
93
+ */
94
+ async getAll() {
95
+ const users = [];
96
+ let after = null;
97
+ for (; ; ) {
98
+ const body = await this.client.request(SALES_ENDPOINT, USER_QUERY, {
99
+ first: this.pageSize,
100
+ after
101
+ });
102
+ const connection = body.data?.accountUsers;
103
+ const edges = connection?.edges ?? [];
104
+ if (edges.length === 0) {
105
+ break;
106
+ }
107
+ users.push(...fromAccountUserNodes(edges.map((edge) => edge.node)));
108
+ const pageInfo = connection?.pageInfo;
109
+ if (pageInfo?.hasNextPage && pageInfo.endCursor) {
110
+ after = pageInfo.endCursor;
111
+ } else {
112
+ break;
113
+ }
114
+ }
115
+ return users;
116
+ }
117
+ /**
118
+ * Returns a single active account user by id, or `null` when no active user
119
+ * matches.
120
+ */
121
+ async getById(userId) {
122
+ const body = await this.client.request(
123
+ SALES_ENDPOINT,
124
+ USER_BY_FILTER_QUERY,
125
+ { first: this.pageSize, filter: { ids: [userId] } }
126
+ );
127
+ const firstEdge = (body.data?.accountUsers?.edges ?? [])[0];
128
+ if (!firstEdge) {
129
+ return null;
130
+ }
131
+ return fromAccountUserNode(firstEdge.node);
132
+ }
133
+ };
134
+
135
+ // src/internal/availability/availability-queries.ts
136
+ var AVAILABILITY_TIMES_QUERY = `
137
+ query Sales($activityId: ID!, $resourceOptionQuantities: [ResourceOptionQuantityData!]!, $date: Date!) {
138
+ availabilityTimes(activityId: $activityId, resourceOptionQuantities: $resourceOptionQuantities, date: $date) {
139
+ id
140
+ time
141
+ from
142
+ end
143
+ duration {
144
+ name
145
+ length {
146
+ amount
147
+ unit
148
+ }
149
+ }
150
+ status
151
+ availability {
152
+ qty
153
+ taken
154
+ resourceOptionId
155
+ }
156
+ }
157
+ }
158
+ `;
159
+
160
+ // src/internal/availability/availability-service.ts
161
+ var AvailabilityService = class {
162
+ constructor(client) {
163
+ this.client = client;
164
+ }
165
+ client;
166
+ /** Returns the availability times for an activity/date and requested quantities. */
167
+ async getAvailabilityTimes(query) {
168
+ const body = await this.client.request(
169
+ SALES_ENDPOINT,
170
+ AVAILABILITY_TIMES_QUERY,
171
+ {
172
+ activityId: query.activityId,
173
+ resourceOptionQuantities: query.resourceOptionQuantities,
174
+ date: query.date
175
+ }
176
+ );
177
+ return body.data?.availabilityTimes ?? [];
178
+ }
179
+ };
180
+
181
+ // src/models/product.ts
182
+ var ADD_ON_PRODUCT_TYPE = "ADD-ON";
183
+
184
+ // src/internal/bookings/booking-converter.ts
185
+ var UNKNOWN = "unknown";
186
+ var SOURCE_SOURCE_MAP = {
187
+ APP_REGISTRY: "app",
188
+ BOOKING_IMPORTER_FUTURE: "importer",
189
+ BOOKING_IMPORTER_HISTORIC: "importer",
190
+ EXPEDIA: "expedia",
191
+ GROUPON: "groupon",
192
+ GYG: "getyourguide",
193
+ HOOK: "internal",
194
+ INTERNAL_TOOLS: "internal",
195
+ IOS_PP: "ios",
196
+ OCTO: "octo",
197
+ PDC: "peekcom",
198
+ PEEK_PLUS: "peekcom",
199
+ PP: "backend",
200
+ RWG: "rwg",
201
+ SELF_SERVE: "internal",
202
+ SYSTEM: "internal",
203
+ VIATOR: "viator",
204
+ WIDGET: "website",
205
+ YELP: "yelp"
206
+ };
207
+ var SOURCE_DESC_MAP = {
208
+ APP_REGISTRY: "App Store App",
209
+ BOOKING_IMPORTER_FUTURE: "Booking Importer",
210
+ BOOKING_IMPORTER_HISTORIC: "Booking Importer",
211
+ EXPEDIA: "Expedia",
212
+ GROUPON: "Groupon",
213
+ GYG: "GetYourGuide",
214
+ HOOK: "Internal",
215
+ INTERNAL_TOOLS: "Internal",
216
+ IOS_PP: "Peek Pro: iOS App",
217
+ OCTO: "OCTO",
218
+ PDC: "Peek.com",
219
+ PEEK_PLUS: "Peek.com",
220
+ PP: "Peek Pro: Backend",
221
+ RWG: "Reserve with Google",
222
+ SELF_SERVE: "Internal",
223
+ SYSTEM: "Internal",
224
+ VIATOR: "Viator",
225
+ WIDGET: "Website Booking Flow",
226
+ YELP: "Yelp"
227
+ };
228
+ function fromBookingNode(node, includeGuests = false, includePriceBreakdown = false) {
229
+ const data = node ?? {};
230
+ const ticketQuantities = data.ticketQuantities ?? [];
231
+ const app = data.order?.initialQuote?.source?.actor?.app ?? null;
232
+ const customQuestionAnswers = (data.questionAnswers ?? []).map(
233
+ (answer) => {
234
+ const base = {
235
+ question: answer.questionText,
236
+ answer: answer.answer
237
+ };
238
+ const location = answer.questionLocationSnapshot;
239
+ if (location && location.latitude && location.longitude) {
240
+ base.latitude = location.latitude;
241
+ base.longitude = location.longitude;
242
+ }
243
+ return base;
244
+ }
245
+ );
246
+ const customGuestQuestionAnswers = Array.isArray(data.tickets) ? data.tickets.flatMap(
247
+ (ticket) => Array.isArray(ticket?.questionAnswers) ? ticket.questionAnswers.filter((qa) => qa && qa.answer !== void 0 && qa.questionText !== void 0).map((qa) => ({ question: qa.questionText, answer: qa.answer })) : []
248
+ ) : [];
249
+ return {
250
+ bookingId: data.id || "",
251
+ displayId: data.displayId || "",
252
+ source: sourceFromApp(app),
253
+ sourceApp: app || UNKNOWN,
254
+ sourceDescription: sourceDescriptionFromApp(app),
255
+ customerName: data.primaryGuest?.name || "",
256
+ customerEmail: data.primaryGuest?.email || null,
257
+ customerPhone: data.primaryGuest?.phone || null,
258
+ productId: data.activitySnapshot?.id || UNKNOWN,
259
+ productName: data.activitySnapshot?.name || UNKNOWN,
260
+ isRentalProduct: data.activitySnapshot?.type === "RENTAL",
261
+ timeslotId: data.timeSnapshot?.legacyId || null,
262
+ totalTickets: ticketQuantity(ticketQuantities),
263
+ ticketDescription: formatTickets(ticketQuantities),
264
+ tickets: ticketsToTicketArray(ticketQuantities),
265
+ isCanceled: data.reservationStatus === "CANCELED",
266
+ isNoShow: data.fulfillmentStatusOverride?.status === "NO_SHOW",
267
+ isCheckedIn: data.checkinStatus !== "NONE",
268
+ isReturned: data.returnStatus !== "NONE",
269
+ purchasedAt: data.purchasedAt || null,
270
+ purchasedAtUtc: data.purchasedAtUtc || null,
271
+ startsAt: data.startsAt || null,
272
+ startsAtUtc: data.startsAtUtc || null,
273
+ endsAt: data.endsAt || null,
274
+ endsAtUtc: data.endsAtUtc || null,
275
+ durationMin: durationInMin(data.startsAt || null, data.endsAt || null),
276
+ availabilityTimeId: data.availabilityTimeId || null,
277
+ portalUrl: data.bookingPortalUrl || null,
278
+ notes: data.operatorNotes || "",
279
+ valueDisplay: data.value?.total?.formatted || "",
280
+ valueAmount: data.value?.total?.amount || "",
281
+ outstandingBalanceAmount: data.balance?.total?.amount || "",
282
+ outstandingBalanceDisplay: data.balance?.total?.formatted || "",
283
+ promoCodes: data.order?.promoCodes?.map((promo) => promo.code) ?? [],
284
+ tips: (data.tips ?? []).map((tip) => ({
285
+ display: tip.price?.formatted || "",
286
+ amount: tip.price?.amount || ""
287
+ })),
288
+ customQuestionAnswers,
289
+ customGuestQuestionAnswers,
290
+ resources: (data.resourcePoolAssignments ?? []).map((resource) => ({
291
+ quantity: resource.quantity || 0,
292
+ name: resource.resourcePool?.name || "",
293
+ shortName: resource.resourcePool?.shortName || ""
294
+ })),
295
+ resourcePoolAssignments: mapResourcePoolAssignments(data.resourcePoolAssignments),
296
+ resellerId: data.order?.channelSnapshot?.id || null,
297
+ resellerName: resellerNameFromChannelSnapshot(data.order?.channelSnapshot),
298
+ orderId: data.order?.id || "",
299
+ convenienceFee: includePriceBreakdown ? mapPrice(data.value?.convenienceFee) : void 0,
300
+ deposit: includePriceBreakdown ? mapPrice(data.value?.deposit) : void 0,
301
+ discount: includePriceBreakdown ? mapPrice(data.value?.discount) : void 0,
302
+ discountedPrice: includePriceBreakdown ? mapPrice(data.value?.discountedPrice) : void 0,
303
+ fees: includePriceBreakdown ? mapPrice(data.value?.fees) : void 0,
304
+ flatPartnerFee: includePriceBreakdown ? mapPrice(data.value?.flatPartnerFee) : void 0,
305
+ price: includePriceBreakdown ? mapPrice(data.value?.price) : void 0,
306
+ retailPrice: includePriceBreakdown ? mapPrice(data.value?.retailPrice) : void 0,
307
+ taxes: includePriceBreakdown ? mapPrice(data.value?.taxes) : void 0,
308
+ tipsBreakdown: includePriceBreakdown ? mapPrice(data.value?.tips) : void 0,
309
+ guests: includeGuests ? convertGuests(data) : void 0
310
+ };
311
+ }
312
+ function convertGuests(data) {
313
+ const bookingGuestsNodes = Array.isArray(data.bookingGuests) ? data.bookingGuests : [];
314
+ const primaryGuestNode = data.primaryGuest;
315
+ const primaryId = primaryGuestNode?.id;
316
+ const guests = [];
317
+ for (const guestNode of bookingGuestsNodes) {
318
+ guests.push(mapGuestNode(guestNode, primaryId ? guestNode.id === primaryId : false));
319
+ }
320
+ const hasPrimaryInGuests = primaryId ? bookingGuestsNodes.some((guest) => guest.id === primaryId) : false;
321
+ if (!hasPrimaryInGuests && primaryGuestNode?.id) {
322
+ guests.push(mapGuestNode(primaryGuestNode, true));
323
+ }
324
+ return guests;
325
+ }
326
+ function mapGuestNode(guestNode, isPrimary) {
327
+ const fieldResponses = Array.isArray(guestNode.fieldResponses) ? guestNode.fieldResponses : [];
328
+ const metadata = fieldResponses.map((response) => ({
329
+ id: response.id,
330
+ name: response.fieldLocation?.field?.name ?? "",
331
+ value: response.text ?? ""
332
+ }));
333
+ return {
334
+ id: guestNode.id,
335
+ name: guestNode.name ?? null,
336
+ country: guestNode.country ?? null,
337
+ dateOfBirth: guestNode.dateOfBirth ? new Date(guestNode.dateOfBirth) : null,
338
+ phone: guestNode.phone ?? null,
339
+ email: guestNode.email ?? null,
340
+ isGdpr: Boolean(guestNode.isGdpr),
341
+ isParticipant: Boolean(guestNode.isParticipant),
342
+ isPrimary,
343
+ optinSms: Boolean(guestNode.optinSms),
344
+ optinMarketing: Boolean(guestNode.optinMarketing),
345
+ postalCode: guestNode.postalCode ?? null,
346
+ metadata
347
+ };
348
+ }
349
+ function sourceFromApp(app) {
350
+ if (!app) return UNKNOWN;
351
+ return SOURCE_SOURCE_MAP[app] ?? UNKNOWN;
352
+ }
353
+ function sourceDescriptionFromApp(app) {
354
+ if (!app) return UNKNOWN;
355
+ return SOURCE_DESC_MAP[app] ?? UNKNOWN;
356
+ }
357
+ function resellerNameFromChannelSnapshot(channelSnapshot) {
358
+ if (!channelSnapshot) return null;
359
+ let out = channelSnapshot.name ?? "";
360
+ if (channelSnapshot.agent?.name) {
361
+ out += " - " + channelSnapshot.agent.name;
362
+ }
363
+ return out;
364
+ }
365
+ function ticketsToTicketArray(ticketQuantities) {
366
+ if (!ticketQuantities || ticketQuantities.length === 0) return [];
367
+ return ticketQuantities.map((ticket) => ({
368
+ name: ticket.resourceOptionSnapshot?.name || "Unknown",
369
+ quantity: ticket.quantity || 0,
370
+ ticketId: ticket.resourceOptionSnapshot?.id || "unknown"
371
+ }));
372
+ }
373
+ function durationInMin(startsAt, endsAt) {
374
+ if (!startsAt || !endsAt) return 0;
375
+ const duration = new Date(endsAt).getTime() - new Date(startsAt).getTime();
376
+ return Math.floor(duration / 1e3 / 60);
377
+ }
378
+ function ticketQuantity(ticketQuantities) {
379
+ if (!ticketQuantities || ticketQuantities.length === 0) return 0;
380
+ return ticketQuantities.reduce((acc, ticket) => acc + (ticket.quantity || 0), 0);
381
+ }
382
+ function formatTickets(ticketQuantities) {
383
+ if (!ticketQuantities || ticketQuantities.length === 0) return "";
384
+ return ticketQuantities.map((ticket) => `${ticket.quantity || 0}x ${ticket.resourceOptionSnapshot?.name || "Unknown"}`).join(", ");
385
+ }
386
+ function mapResourcePoolAssignments(poolAssignments) {
387
+ if (!poolAssignments || poolAssignments.length === 0) return [];
388
+ return poolAssignments.flatMap(
389
+ (pool) => (pool.resourceAssignments ?? []).map((assignment) => ({
390
+ id: assignment.resource?.id || "",
391
+ name: assignment.resource?.name || ""
392
+ }))
393
+ );
394
+ }
395
+ function mapPrice(priceData) {
396
+ if (!priceData || !priceData.amount && !priceData.formatted) {
397
+ return void 0;
398
+ }
399
+ return {
400
+ amount: priceData.amount || "0",
401
+ display: priceData.formatted || ""
402
+ };
403
+ }
404
+
405
+ // src/internal/bookings/booking-guest-converter.ts
406
+ function fromBookingGuestsResponse(response) {
407
+ const firstEdge = (response?.sales?.edges ?? [])[0];
408
+ if (!firstEdge) {
409
+ return [];
410
+ }
411
+ const bookingNode = firstEdge.node;
412
+ const primaryGuestNode = bookingNode.primaryGuest;
413
+ const bookingGuestsNodes = Array.isArray(bookingNode.bookingGuests) ? bookingNode.bookingGuests : [];
414
+ const primaryId = primaryGuestNode?.id;
415
+ const guests = [];
416
+ for (const guestNode of bookingGuestsNodes) {
417
+ guests.push(mapGuestNode(guestNode, primaryId ? guestNode.id === primaryId : false));
418
+ }
419
+ const hasPrimaryInGuests = primaryId ? bookingGuestsNodes.some((guest) => guest.id === primaryId) : false;
420
+ if (!hasPrimaryInGuests && primaryGuestNode) {
421
+ guests.push(mapGuestNode(primaryGuestNode, true));
422
+ }
423
+ return guests;
424
+ }
425
+
426
+ // src/internal/bookings/payments-on-file-converter.ts
427
+ function fromPaymentsOnFileResponse(response, bookingId) {
428
+ const firstEdge = (response?.sales?.edges ?? [])[0];
429
+ if (!firstEdge) {
430
+ return null;
431
+ }
432
+ const order = firstEdge.node.order;
433
+ const orderId = order?.id ?? "";
434
+ const rawPaymentSources = order?.paymentSources ?? [];
435
+ const rawPayments = order?.payments ?? [];
436
+ const paymentsBySourceId = /* @__PURE__ */ new Map();
437
+ for (const payment of rawPayments) {
438
+ const sourceId = payment.paymentSource?.id;
439
+ if (!sourceId) continue;
440
+ const mapped = {
441
+ id: payment.id,
442
+ paidAt: dateOnly(payment.appliedAt),
443
+ currentAmount: payment.currentAmount,
444
+ refundableAmount: payment.refundableAmount
445
+ };
446
+ const existing = paymentsBySourceId.get(sourceId);
447
+ if (existing) {
448
+ existing.push(mapped);
449
+ } else {
450
+ paymentsBySourceId.set(sourceId, [mapped]);
451
+ }
452
+ }
453
+ const paymentsOnFile = rawPaymentSources.map((source) => {
454
+ const payments = paymentsBySourceId.get(source.id);
455
+ return {
456
+ description: source.description,
457
+ id: source.id,
458
+ type: source.type,
459
+ ...payments ? { payments } : {}
460
+ };
461
+ });
462
+ return { bookingId, orderId, paymentsOnFile };
463
+ }
464
+ function dateOnly(iso) {
465
+ return iso.split("T")[0] ?? iso;
466
+ }
467
+
468
+ // src/internal/bookings/addon-queries.ts
469
+ var RESERVATION_STATUS_CONFIRMED = "CONFIRMED";
470
+ var ADDON_OPTION_STATUS_CANCELED = "CANCELED";
471
+ var SALES_ADDONS_PAGE_SIZE = 100;
472
+ var SALES_ADDONS_QUERY = `
473
+ query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
474
+ sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
475
+ pageInfo { endCursor hasNextPage }
476
+ edges {
477
+ node {
478
+ order { id displayId }
479
+ ... on Booking {
480
+ displayId
481
+ id
482
+ refid
483
+ reservationStatus
484
+ items {
485
+ id
486
+ refid
487
+ value { total { amount currency formatted } }
488
+ reservationStatus
489
+ options {
490
+ refid
491
+ reservationStatus
492
+ price { amount currency formatted }
493
+ itemOptionSnapshot { id name }
494
+ itemSnapshot { id name }
495
+ }
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
501
+ }
502
+ `;
503
+ function buildSalesAddonsVariables(searchString) {
504
+ return {
505
+ first: SALES_ADDONS_PAGE_SIZE,
506
+ after: null,
507
+ orderBy: { direction: "DESC", field: "STARTS_AT" },
508
+ filter: { searchString }
509
+ };
510
+ }
511
+
512
+ // src/internal/bookings/addon-converter.ts
513
+ var ERROR_INCONSISTENT_ADDON_ITEM_ID = "Add-on group contains options with mismatched item IDs";
514
+ function parseSaleNode(node) {
515
+ const bookingId = node.id || "";
516
+ const displayId = node.displayId || "";
517
+ const orderId = node.order?.id || "";
518
+ const bookingQuoteRefid = node.refid || "";
519
+ const bookingQuoteReservationStatus = node.reservationStatus || "";
520
+ const items = Array.isArray(node.items) ? node.items : [];
521
+ return items.map((item) => {
522
+ const options = Array.isArray(item.options) ? item.options : [];
523
+ const addonItemOptions = options.map((opt) => ({
524
+ itemId: opt.itemSnapshot?.id || "",
525
+ optionId: opt.itemOptionSnapshot?.id || "",
526
+ itemName: opt.itemSnapshot?.name || "",
527
+ optionName: opt.itemOptionSnapshot?.name || "",
528
+ optionRefid: opt.refid || "",
529
+ optionReservationStatus: opt.reservationStatus || "",
530
+ itemReservationStatus: item.reservationStatus || "",
531
+ itemRefid: item.refid || ""
532
+ }));
533
+ return {
534
+ bookingId,
535
+ displayId,
536
+ orderId,
537
+ total: item.value?.total ?? null,
538
+ bookingQuoteRefid,
539
+ bookingQuoteReservationStatus,
540
+ addonItemOptions
541
+ };
542
+ });
543
+ }
544
+ function toBookingAddon(item) {
545
+ const options = item.addonItemOptions || [];
546
+ if (options.length === 0) {
547
+ return null;
548
+ }
549
+ const addonId = options[0].itemId;
550
+ const addonName = options[0].itemName;
551
+ if (options.some((opt) => opt.itemId !== addonId)) {
552
+ throw new Error(ERROR_INCONSISTENT_ADDON_ITEM_ID);
553
+ }
554
+ const grouped = /* @__PURE__ */ new Map();
555
+ options.filter((opt) => opt.optionReservationStatus !== ADDON_OPTION_STATUS_CANCELED).forEach((opt) => {
556
+ const existing = grouped.get(opt.optionId);
557
+ if (existing) {
558
+ existing.quantity += 1;
559
+ } else {
560
+ grouped.set(opt.optionId, {
561
+ addonOptionId: opt.optionId,
562
+ addonOptionName: opt.optionName,
563
+ quantity: 1
564
+ });
565
+ }
566
+ });
567
+ const addonOptions = Array.from(grouped.values());
568
+ if (addonOptions.length === 0) {
569
+ return null;
570
+ }
571
+ return { addonId, addonName, total: item.total, addonOptions };
572
+ }
573
+
574
+ // src/internal/bookings/booking-queries.ts
575
+ var SEARCH_BY_PURCHASE_DATE = "purchaseDate";
576
+ var SEARCH_BY_ACTIVITY_DATE = "activityDate";
577
+ function normalizeBookingId(bookingId) {
578
+ return bookingId.toLowerCase().replace(/-/g, "_");
579
+ }
580
+ var guestFields = `
581
+ id
582
+ name
583
+ country
584
+ dateOfBirth
585
+ email
586
+ isGdpr
587
+ isParticipant
588
+ optinSms
589
+ optinMarketing
590
+ phone
591
+ postalCode
592
+ fieldResponses {
593
+ id
594
+ text
595
+ fieldLocation {
596
+ field {
597
+ name
598
+ }
599
+ }
600
+ }
601
+ `;
602
+ var bookingGuestsFields = `
603
+ bookingGuests {
604
+ ${guestFields}
605
+ }
606
+ primaryGuest {
607
+ ${guestFields}
608
+ }
609
+ `;
610
+ var bookingQueryFields = `
611
+ displayId
612
+ id
613
+ primaryGuest {
614
+ name
615
+ email
616
+ phone
617
+ optinMarketing
618
+ optinSms
619
+ isGdpr
620
+ postalCode
621
+ }
622
+ activitySnapshot {
623
+ type
624
+ name
625
+ id
626
+ }
627
+ ticketQuantities {
628
+ quantity
629
+ resourceOptionSnapshot {
630
+ name
631
+ id
632
+ }
633
+ }
634
+ reservationStatus
635
+ checkinStatus
636
+ returnStatus
637
+ fulfillmentStatusOverride {
638
+ status
639
+ }
640
+ timeSnapshot {
641
+ id
642
+ legacyId
643
+ }
644
+ purchasedAt
645
+ purchasedAtUtc
646
+ startsAt
647
+ startsAtUtc
648
+ endsAt
649
+ endsAtUtc
650
+ availabilityTimeId
651
+ bookingPortalUrl
652
+ operatorNotes
653
+ value {
654
+ total {
655
+ formatted
656
+ amount
657
+ }
658
+ }
659
+ balance {
660
+ total {
661
+ amount
662
+ formatted
663
+ }
664
+ }
665
+ tips {
666
+ price {
667
+ amount
668
+ formatted
669
+ }
670
+ }
671
+ order {
672
+ displayId
673
+ id
674
+ promoCodes {
675
+ code
676
+ }
677
+ channelSnapshot {
678
+ id
679
+ name
680
+ agent {
681
+ name
682
+ }
683
+ }
684
+ initialQuote {
685
+ source {
686
+ actor {
687
+ app
688
+ }
689
+ }
690
+ }
691
+ }
692
+ questionAnswers {
693
+ answer
694
+ questionText
695
+ questionLocationSnapshot {
696
+ latitude
697
+ longitude
698
+ }
699
+ }
700
+ tickets {
701
+ questionAnswers {
702
+ answer
703
+ questionText
704
+ }
705
+ }
706
+ resourcePoolAssignments {
707
+ quantity
708
+ resourcePool {
709
+ name
710
+ shortName
711
+ resources {
712
+ name
713
+ }
714
+ }
715
+ resourceAssignments {
716
+ resource {
717
+ id
718
+ name
719
+ }
720
+ }
721
+ }
722
+ `;
723
+ var PRICE_BREAKDOWN_FIELDS = `
724
+ convenienceFee { amount formatted }
725
+ deposit { amount formatted }
726
+ discount { amount formatted }
727
+ discountedPrice { amount formatted }
728
+ fees { amount formatted }
729
+ flatPartnerFee { amount formatted }
730
+ price { amount formatted }
731
+ retailPrice { amount formatted }
732
+ taxes { amount formatted }
733
+ tips { amount formatted }
734
+ `;
735
+ function buildBookingsListingQuery(includeGuests, includePriceBreakdown) {
736
+ const guestsSection = includeGuests ? bookingGuestsFields : "";
737
+ const fields = includePriceBreakdown ? bookingQueryFields.replace("value {", `value { ${PRICE_BREAKDOWN_FIELDS}`) : bookingQueryFields;
738
+ return `
739
+ query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
740
+ sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
741
+ pageInfo { endCursor hasNextPage }
742
+ edges {
743
+ node {
744
+ ... on Booking {
745
+ ${fields}
746
+ ${guestsSection}
747
+ }
748
+ }
749
+ }
750
+ }
751
+ }
752
+ `;
753
+ }
754
+ var BOOKING_GUESTS_QUERY = `
755
+ query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
756
+ sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
757
+ pageInfo { endCursor hasNextPage }
758
+ edges {
759
+ node {
760
+ ... on Booking {
761
+ displayId
762
+ id
763
+ ${bookingGuestsFields}
764
+ }
765
+ }
766
+ }
767
+ }
768
+ }
769
+ `;
770
+ var BOOKING_PAYMENTS_ON_FILE_QUERY = `
771
+ query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
772
+ sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
773
+ pageInfo { endCursor hasNextPage }
774
+ edges {
775
+ node {
776
+ order {
777
+ payments {
778
+ id
779
+ paymentSource { id }
780
+ appliedAt
781
+ currentAmount { amount currency }
782
+ refundableAmount { amount currency }
783
+ }
784
+ id
785
+ displayId
786
+ paymentSources { description type id }
787
+ }
788
+ ... on Booking {
789
+ displayId
790
+ id
791
+ }
792
+ }
793
+ }
794
+ }
795
+ }
796
+ `;
797
+ var UPDATE_OPERATOR_NOTES_MUTATION = `
798
+ mutation Account($input: UpdateOperatorNotesForBookingInput!) {
799
+ updateOperatorNotesForBooking(input: $input) {
800
+ booking { operatorNotes }
801
+ }
802
+ }
803
+ `;
804
+ var UPDATE_BOOKING_CHECKIN_MUTATION = `
805
+ mutation Account($input: UpdateBookingCheckInInput!) {
806
+ updateBookingCheckIn(input: $input) {
807
+ booking { checkinStatus }
808
+ }
809
+ }
810
+ `;
811
+ var CANCEL_BOOKING_MUTATION = `
812
+ mutation Account($input: CancelBookingInput!) {
813
+ cancelBooking(input: $input) {
814
+ booking { id displayId reservationStatus }
815
+ }
816
+ }
817
+ `;
818
+ var APPLY_PAYMENT_TO_ORDER_MUTATION = `
819
+ mutation ApplyPaymentToOrder($input: ApplyPaymentToOrderInput!) {
820
+ applyPaymentToOrder(input: $input) {
821
+ transactionId
822
+ errors { code detail value }
823
+ }
824
+ }
825
+ `;
826
+ var APPLY_REFUND_TO_ORDER_MUTATION = `
827
+ mutation ApplyRefundToOrder($input: ApplyRefundToOrderInput!) {
828
+ applyRefundToOrder(input: $input) {
829
+ transactionId
830
+ errors { code detail value }
831
+ }
832
+ }
833
+ `;
834
+ var CREATE_INVOICE_LINK_MUTATION = `
835
+ mutation CreateInvoiceLink($input: CreateInvoiceLinkInput!) {
836
+ createInvoiceLink(input: $input) {
837
+ invoiceLink { status url }
838
+ errors { code detail value __typename }
839
+ }
840
+ }
841
+ `;
842
+ var CREATE_QUOTE_FROM_ORDER_MUTATION = `
843
+ mutation CreateQuoteFromOrder($input: CreateQuoteFromOrderInput!) {
844
+ createQuoteFromOrder(input: $input) {
845
+ errors { detail value code }
846
+ quote {
847
+ id
848
+ saleQuotes { refid reservationStatus }
849
+ }
850
+ }
851
+ }
852
+ `;
853
+ var UPDATE_QUOTE_V2_MUTATION = `
854
+ mutation UpdateQuoteV2($input: UpdateQuoteV2Input!) {
855
+ updateQuoteV2(input: $input) {
856
+ errors { detail value code }
857
+ quote { id }
858
+ }
859
+ }
860
+ `;
861
+ var AMEND_ORDER_MUTATION = `
862
+ mutation AmendOrder($input: AmendOrderInput!) {
863
+ amendOrder(input: $input) {
864
+ errors { code detail value }
865
+ order { id }
866
+ }
867
+ }
868
+ `;
869
+ var CREATE_QUOTE_V2_MUTATION = `
870
+ mutation CreateQuoteV2($input: CreateQuoteV2Input!) {
871
+ createQuoteV2(input: $input) {
872
+ errors { detail value code }
873
+ quote { id }
874
+ }
875
+ }
876
+ `;
877
+ var CREATE_ORDER_FROM_QUOTE_MUTATION = `
878
+ mutation CreateOrderFromQuote($input: CreateOrderFromQuoteInput!) {
879
+ createOrderFromQuote(input: $input) {
880
+ errors { code detail value }
881
+ order {
882
+ id
883
+ sales {
884
+ id
885
+ displayId
886
+ ... on Booking {
887
+ balance { total { amount currency formatted } }
888
+ }
889
+ }
890
+ }
891
+ }
892
+ }
893
+ `;
894
+ function buildBookingsVariables(params) {
895
+ const filter = {};
896
+ const bookingFilter = {};
897
+ if (params.bookingId) {
898
+ bookingFilter.ids = [normalizeBookingId(params.bookingId)];
899
+ } else if (params.timeslotId) {
900
+ bookingFilter.timeslotRefid = params.timeslotId;
901
+ } else {
902
+ if (params.searchBy === SEARCH_BY_ACTIVITY_DATE) {
903
+ bookingFilter.overlapsRange = `[${params.startDateTime},${params.endDateTime}]`;
904
+ } else if (params.searchBy === SEARCH_BY_PURCHASE_DATE) {
905
+ filter.purchasedAtRangeUtc = `[${params.startDateTime},${params.endDateTime}]`;
906
+ }
907
+ if (params.productId) {
908
+ bookingFilter.activityIds = [params.productId];
909
+ }
910
+ }
911
+ if (params.email) {
912
+ filter.primaryGuestEmail = params.email;
913
+ }
914
+ if (params.searchString && params.searchString.length > 0) {
915
+ filter.searchString = params.searchString;
916
+ }
917
+ if (Object.keys(bookingFilter).length > 0) {
918
+ filter.bookingFilter = bookingFilter;
919
+ }
920
+ return {
921
+ first: params.pageSize,
922
+ after: params.after,
923
+ orderBy: { direction: "ASC", field: "STARTS_AT" },
924
+ filter
925
+ };
926
+ }
927
+
928
+ // src/internal/bookings/booking-service.ts
929
+ var DEFAULT_PAGE_SIZE2 = 50;
930
+ var DEFAULT_CANCEL_NOTE = "Canceled";
931
+ var DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API";
932
+ var BOOKING_ID_PREFIX = "b_";
933
+ var PAYMENT_SOURCE_PREFIX = "ps_";
934
+ var PAYMENT_ID_PREFIX = "pmt_";
935
+ var ALLOWED_PAYMENT_SOURCE_IDS = ["cash/cash", "custom/other", "custom/voucher"];
936
+ var CURRENCY_CODE_REGEX = /^[A-Z]{3}$/;
937
+ var ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required";
938
+ var ERROR_QUANTITY_INVALID = "quantity must be a positive integer string";
939
+ var ERROR_BOOKING_ID_REQUIRED = "bookingId is required";
940
+ var ERROR_BOOKING_NOT_FOUND = "Booking not found";
941
+ var ERROR_MULTIPLE_BOOKINGS_FOUND = "Expected exactly one booking for the provided bookingId";
942
+ var ERROR_NO_ADDON_OPTION_TO_REMOVE = "No confirmed add-on option matching addonOptionId was found on the booking";
943
+ var ERROR_CREATE_QUOTE_FAILED = "Failed to create quote from order";
944
+ var ERROR_UPDATE_QUOTE_FAILED = "Failed to update quote with add-on";
945
+ var ERROR_AMEND_ORDER_FAILED = "Failed to amend order with add-on";
946
+ var BookingService = class {
947
+ constructor(client, deps, options = {}) {
948
+ this.client = client;
949
+ this.deps = deps;
950
+ this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE2;
951
+ }
952
+ client;
953
+ deps;
954
+ pageSize;
955
+ /**
956
+ * Returns a single booking by id, or null when not found. The `bookingId` is
957
+ * normalized internally (lowercased, `-` → `_`), so `B-ABC123` and `b_abc123`
958
+ * resolve to the same booking.
959
+ *
960
+ * @example
961
+ * ```ts
962
+ * const bookings = peek.getBookingService();
963
+ * const booking = await bookings.getById("b_abc123", {
964
+ * includeGuests: true,
965
+ * includePriceBreakdown: true,
966
+ * });
967
+ * if (booking) console.log(booking.displayId, booking.outstandingBalanceAmount);
968
+ * ```
969
+ */
970
+ async getById(bookingId, options = {}) {
971
+ const includeGuests = options.includeGuests ?? false;
972
+ const includePriceBreakdown = options.includePriceBreakdown ?? false;
973
+ const body = await this.client.request(
974
+ SALES_ENDPOINT,
975
+ buildBookingsListingQuery(includeGuests, includePriceBreakdown),
976
+ buildBookingsVariables({
977
+ pageSize: this.pageSize,
978
+ after: null,
979
+ bookingId: normalizeBookingId(bookingId)
980
+ })
981
+ );
982
+ const firstEdge = (body.data?.sales?.edges ?? [])[0];
983
+ if (!firstEdge) {
984
+ return null;
985
+ }
986
+ return fromBookingNode(firstEdge.node, includeGuests, includePriceBreakdown);
987
+ }
988
+ /** Returns all bookings matching a time range (paginated). */
989
+ async searchByTimeRange(input) {
990
+ const includeGuests = input.includeGuests ?? false;
991
+ const includePriceBreakdown = input.includePriceBreakdown ?? false;
992
+ return this.fetchPaginated(
993
+ buildBookingsListingQuery(includeGuests, includePriceBreakdown),
994
+ {
995
+ startDateTime: input.start,
996
+ endDateTime: input.end,
997
+ searchBy: input.searchBy ?? SEARCH_BY_PURCHASE_DATE,
998
+ productId: input.productId,
999
+ email: input.email,
1000
+ searchString: input.searchString
1001
+ },
1002
+ includeGuests,
1003
+ includePriceBreakdown
1004
+ );
1005
+ }
1006
+ /** Returns all bookings on a timeslot (paginated). */
1007
+ async searchByTimeslot(timeslotId, options = {}) {
1008
+ const includeGuests = options.includeGuests ?? false;
1009
+ const includePriceBreakdown = options.includePriceBreakdown ?? false;
1010
+ return this.fetchPaginated(
1011
+ buildBookingsListingQuery(includeGuests, includePriceBreakdown),
1012
+ { timeslotId },
1013
+ includeGuests,
1014
+ includePriceBreakdown
1015
+ );
1016
+ }
1017
+ /** Returns the guests on a booking (primary guest included). */
1018
+ async getGuests(bookingId) {
1019
+ const body = await this.client.request(
1020
+ SALES_ENDPOINT,
1021
+ BOOKING_GUESTS_QUERY,
1022
+ buildBookingsVariables({
1023
+ pageSize: this.pageSize,
1024
+ after: null,
1025
+ bookingId: normalizeBookingId(bookingId)
1026
+ })
1027
+ );
1028
+ return fromBookingGuestsResponse(body.data);
1029
+ }
1030
+ /** Returns the payments on file for a booking, or null when not found. */
1031
+ async getPaymentsOnFile(bookingId) {
1032
+ const normalized = normalizeBookingId(bookingId);
1033
+ const body = await this.client.request(
1034
+ SALES_ENDPOINT,
1035
+ BOOKING_PAYMENTS_ON_FILE_QUERY,
1036
+ buildBookingsVariables({ pageSize: this.pageSize, after: null, bookingId: normalized })
1037
+ );
1038
+ return fromPaymentsOnFileResponse(body.data, normalized);
1039
+ }
1040
+ /**
1041
+ * Appends to (or overwrites) a booking's operator notes. Returns the updated
1042
+ * booking, or null when the booking is not found.
1043
+ */
1044
+ async appendNote(bookingId, note, mode = "append") {
1045
+ const normalized = normalizeBookingId(bookingId);
1046
+ const booking = await this.getById(normalized);
1047
+ if (!booking) {
1048
+ return null;
1049
+ }
1050
+ const newNote = mode === "overwrite" || !booking.notes ? note : `${booking.notes}
1051
+ ${note}`;
1052
+ await this.client.request(SALES_ENDPOINT, UPDATE_OPERATOR_NOTES_MUTATION, {
1053
+ input: { id: normalized, operatorNotes: newNote }
1054
+ });
1055
+ booking.notes = newNote;
1056
+ return booking;
1057
+ }
1058
+ /**
1059
+ * Sets a booking's check-in status. Returns the refreshed booking, or null
1060
+ * when not found.
1061
+ */
1062
+ async setCheckinStatus(bookingId, checkedIn) {
1063
+ const normalized = normalizeBookingId(bookingId);
1064
+ const checkedInAt = checkedIn ? (/* @__PURE__ */ new Date()).toISOString() : null;
1065
+ await this.client.request(SALES_ENDPOINT, UPDATE_BOOKING_CHECKIN_MUTATION, {
1066
+ input: { id: normalized, checkedInAt }
1067
+ });
1068
+ return this.getById(normalized);
1069
+ }
1070
+ /** Cancels a booking and returns its id/displayId/status. */
1071
+ async cancel(bookingId, notes = DEFAULT_CANCEL_NOTE) {
1072
+ const body = await this.client.request(
1073
+ SALES_ENDPOINT,
1074
+ CANCEL_BOOKING_MUTATION,
1075
+ { input: { bookingId: normalizeBookingId(bookingId), notes } }
1076
+ );
1077
+ const booking = body.data?.cancelBooking?.booking;
1078
+ if (!booking) {
1079
+ throw new Error("No booking data returned from cancelBooking");
1080
+ }
1081
+ return {
1082
+ id: booking.id,
1083
+ displayId: booking.displayId,
1084
+ reservationStatus: booking.reservationStatus
1085
+ };
1086
+ }
1087
+ /**
1088
+ * Charges a booking. Validates input, resolves the order + payment source via
1089
+ * payments-on-file, then applies the payment. The `idempotencyKey` is passed
1090
+ * through to Peek.
1091
+ *
1092
+ * @example
1093
+ * ```ts
1094
+ * const result = await peek.getBookingService().makePayment({
1095
+ * bookingId: "b_abc123",
1096
+ * paymentSourceId: "custom/other", // or a "ps_…" source on file
1097
+ * amount: "25.00",
1098
+ * currency: "USD",
1099
+ * idempotencyKey: crypto.randomUUID(),
1100
+ * });
1101
+ * console.log(result.transactionId);
1102
+ * ```
1103
+ *
1104
+ * @throws {Error} when `paymentSourceId` is missing or not a `ps_…` id / one
1105
+ * of `cash/cash`, `custom/other`, `custom/voucher`; when `amount` is not a
1106
+ * valid number; when `currency` is not a 3-letter uppercase code; when
1107
+ * `idempotencyKey` is empty; when `bookingId` does not resolve to a `b_…` id;
1108
+ * when the booking or payment source is not found; or when the charge fails.
1109
+ */
1110
+ async makePayment(input) {
1111
+ const normalized = normalizeBookingId(input.bookingId);
1112
+ this.validatePaymentInput(input, normalized);
1113
+ const onFile = await this.getPaymentsOnFile(normalized);
1114
+ if (!onFile) {
1115
+ throw new Error("Booking not found");
1116
+ }
1117
+ if (!onFile.paymentsOnFile.some((source) => source.id === input.paymentSourceId)) {
1118
+ throw new Error("paymentSourceId not found for this booking");
1119
+ }
1120
+ const customerMessage = input.customerMessage ? `${DEFAULT_CUSTOMER_MESSAGE} (${input.customerMessage})` : DEFAULT_CUSTOMER_MESSAGE;
1121
+ const body = await this.client.request(
1122
+ SALES_ENDPOINT,
1123
+ APPLY_PAYMENT_TO_ORDER_MUTATION,
1124
+ {
1125
+ input: {
1126
+ amount: { amount: input.amount, currency: input.currency },
1127
+ orderId: onFile.orderId,
1128
+ paymentSourceId: input.paymentSourceId,
1129
+ idempotencyKey: input.idempotencyKey,
1130
+ customerMessage,
1131
+ scopedTo: [normalized]
1132
+ }
1133
+ }
1134
+ );
1135
+ const result = body.data?.applyPaymentToOrder;
1136
+ if (result?.errors && result.errors.length > 0) {
1137
+ throw new Error(`Payment failed: ${result.errors[0].detail}`);
1138
+ }
1139
+ return {
1140
+ transactionId: result?.transactionId ?? "",
1141
+ bookingId: normalized,
1142
+ orderId: onFile.orderId,
1143
+ amount: input.amount,
1144
+ currency: input.currency,
1145
+ paymentSourceId: input.paymentSourceId
1146
+ };
1147
+ }
1148
+ /**
1149
+ * Refunds a booking payment. Validates input, resolves the order + payment via
1150
+ * payments-on-file, then applies the refund.
1151
+ */
1152
+ async refund(input) {
1153
+ const normalized = normalizeBookingId(input.bookingId);
1154
+ this.validateRefundInput(input, normalized);
1155
+ const onFile = await this.getPaymentsOnFile(normalized);
1156
+ if (!onFile) {
1157
+ throw new Error("Booking not found");
1158
+ }
1159
+ const paymentExists = onFile.paymentsOnFile.some(
1160
+ (source) => (source.payments ?? []).some((payment) => payment.id === input.paymentId)
1161
+ );
1162
+ if (!paymentExists) {
1163
+ throw new Error("paymentId not found for this booking");
1164
+ }
1165
+ const body = await this.client.request(
1166
+ SALES_ENDPOINT,
1167
+ APPLY_REFUND_TO_ORDER_MUTATION,
1168
+ {
1169
+ input: {
1170
+ amount: { amount: input.amount, currency: input.currency },
1171
+ orderId: onFile.orderId,
1172
+ paymentId: input.paymentId,
1173
+ idempotencyKey: input.idempotencyKey,
1174
+ scopedTo: [normalized]
1175
+ }
1176
+ }
1177
+ );
1178
+ const result = body.data?.applyRefundToOrder;
1179
+ if (result?.errors && result.errors.length > 0) {
1180
+ throw new Error(`Refund failed: ${result.errors[0].detail}`);
1181
+ }
1182
+ return {
1183
+ transactionId: result?.transactionId ?? "",
1184
+ bookingId: normalized,
1185
+ orderId: onFile.orderId,
1186
+ amount: input.amount,
1187
+ currency: input.currency,
1188
+ paymentId: input.paymentId
1189
+ };
1190
+ }
1191
+ /** Creates an invoice link for a booking's order. */
1192
+ async createInvoiceLink(bookingId) {
1193
+ if (!bookingId || bookingId.trim().length === 0) {
1194
+ throw new Error("bookingId is required");
1195
+ }
1196
+ const normalized = normalizeBookingId(bookingId);
1197
+ const booking = await this.getById(normalized);
1198
+ if (!booking || !booking.orderId) {
1199
+ throw new Error("Booking not found");
1200
+ }
1201
+ const orderId = normalizeBookingId(booking.orderId);
1202
+ const body = await this.client.request(
1203
+ SALES_ENDPOINT,
1204
+ CREATE_INVOICE_LINK_MUTATION,
1205
+ { input: { orderId } }
1206
+ );
1207
+ const url = body.data?.createInvoiceLink?.invoiceLink?.url;
1208
+ if (!url) {
1209
+ throw new Error("Failed to create invoice link");
1210
+ }
1211
+ return { bookingId: normalized, orderId, invoiceLink: url };
1212
+ }
1213
+ /** Returns the add-ons on a booking, grouped by add-on item (clean model). */
1214
+ async listAddons(bookingId) {
1215
+ const { items, displayId, orderId, normalizedBookingId } = await this.fetchBookingSale(bookingId);
1216
+ const addons = items.map((item) => toBookingAddon(item)).filter((addon) => addon !== null);
1217
+ return {
1218
+ bookingId: items[0]?.bookingId || normalizedBookingId,
1219
+ displayId: items[0]?.displayId || displayId,
1220
+ orderId: items[0]?.orderId || orderId,
1221
+ addons
1222
+ };
1223
+ }
1224
+ /**
1225
+ * Adds an add-on to a booking via createQuoteFromOrder → updateQuoteV2 →
1226
+ * amendOrder. Lists the booking's add-ons first to derive the order id and
1227
+ * reuse an existing add-on refid; resolves the add-on's parent item via the
1228
+ * product service. Returns the booking's add-ons after the change.
1229
+ *
1230
+ * `quantity` is a **positive-integer string** ("1", "2", …); one add-on
1231
+ * itemOption is created per unit. `addonOptionId` is the add-on's item-option
1232
+ * id (a ticket id on an `ADD_ON_PRODUCT_TYPE` product from
1233
+ * {@link ProductService.getAllProducts}).
1234
+ *
1235
+ * @example
1236
+ * ```ts
1237
+ * const result = await peek.getBookingService().addAddon("b_abc123", {
1238
+ * addonOptionId: "io_helmet",
1239
+ * quantity: "2",
1240
+ * });
1241
+ * console.log(result.updatedBookingAddons.addons);
1242
+ * ```
1243
+ *
1244
+ * @throws {Error} when `addonOptionId` is missing, `quantity` is not a
1245
+ * positive-integer string, the add-on is not found on any product, or any of
1246
+ * the underlying quote/order mutations fail.
1247
+ */
1248
+ async addAddon(bookingId, input) {
1249
+ const addonOptionId = (input?.addonOptionId || input?.addonId || "").trim();
1250
+ if (!addonOptionId) {
1251
+ throw new Error(ERROR_ADDON_OPTION_ID_REQUIRED);
1252
+ }
1253
+ const quantity = parseQuantity(input?.quantity);
1254
+ if (quantity === null) {
1255
+ throw new Error(ERROR_QUANTITY_INVALID);
1256
+ }
1257
+ const [sale, parentItemId] = await Promise.all([
1258
+ this.fetchBookingSale(bookingId),
1259
+ this.resolveParentItemId(addonOptionId)
1260
+ ]);
1261
+ const normalizedOrderId = normalizeBookingId(sale.orderId);
1262
+ const existingItemRefid = sale.items.filter(
1263
+ (item) => item.bookingQuoteReservationStatus !== ADDON_OPTION_STATUS_CANCELED
1264
+ ).flatMap((item) => item.addonItemOptions).find((opt) => opt.itemId === parentItemId)?.itemRefid;
1265
+ const addonRefid = existingItemRefid || randomUUID();
1266
+ const { quoteId, saleQuoteRefid } = await this.createQuoteFromOrderOrThrow(normalizedOrderId);
1267
+ if (!saleQuoteRefid) {
1268
+ throw new Error(ERROR_CREATE_QUOTE_FAILED);
1269
+ }
1270
+ const itemOptions = Array.from({ length: quantity }, () => ({
1271
+ itemOptionId: addonOptionId,
1272
+ reservationStatus: RESERVATION_STATUS_CONFIRMED,
1273
+ refid: randomUUID()
1274
+ }));
1275
+ await this.updateQuoteOrThrow(quoteId, {
1276
+ bookingQuotes: [
1277
+ {
1278
+ refid: saleQuoteRefid,
1279
+ addons: [
1280
+ {
1281
+ itemOptions,
1282
+ itemId: parentItemId,
1283
+ reservationStatus: RESERVATION_STATUS_CONFIRMED,
1284
+ refid: addonRefid
1285
+ }
1286
+ ],
1287
+ reservationStatus: RESERVATION_STATUS_CONFIRMED
1288
+ }
1289
+ ]
1290
+ });
1291
+ await this.amendOrderOrThrow(quoteId, normalizedOrderId);
1292
+ return { updatedBookingAddons: await this.listAddons(bookingId) };
1293
+ }
1294
+ /**
1295
+ * Removes (cancels) add-on options from a booking. Lists the booking's
1296
+ * add-ons first to get the order id and the existing item/option refids, then
1297
+ * issues the same three mutations as {@link addAddon} but with cancellation
1298
+ * variables. Returns the booking's add-ons after the change.
1299
+ */
1300
+ async removeAddon(bookingId, input) {
1301
+ const addonOptionId = (input?.addonOptionId || input?.addonId || "").trim();
1302
+ if (!addonOptionId) {
1303
+ throw new Error(ERROR_ADDON_OPTION_ID_REQUIRED);
1304
+ }
1305
+ const quantity = parseQuantity(input?.quantity);
1306
+ if (quantity === null) {
1307
+ throw new Error(ERROR_QUANTITY_INVALID);
1308
+ }
1309
+ const sale = await this.fetchBookingSale(bookingId);
1310
+ const normalizedOrderId = normalizeBookingId(sale.orderId);
1311
+ const { bookingQuotes, canceledCount } = buildCancellation(
1312
+ sale.items,
1313
+ addonOptionId,
1314
+ quantity
1315
+ );
1316
+ if (canceledCount === 0) {
1317
+ throw new Error(ERROR_NO_ADDON_OPTION_TO_REMOVE);
1318
+ }
1319
+ const { quoteId } = await this.createQuoteFromOrderOrThrow(normalizedOrderId);
1320
+ await this.updateQuoteOrThrow(quoteId, { bookingQuotes });
1321
+ await this.amendOrderOrThrow(quoteId, normalizedOrderId);
1322
+ return { updatedBookingAddons: await this.listAddons(bookingId) };
1323
+ }
1324
+ /**
1325
+ * Runs the sales add-ons query for a booking, validates that exactly one
1326
+ * booking matched, and parses the node into the internal model.
1327
+ */
1328
+ async fetchBookingSale(bookingId) {
1329
+ const searchString = (bookingId || "").trim();
1330
+ if (!searchString) {
1331
+ throw new Error(ERROR_BOOKING_ID_REQUIRED);
1332
+ }
1333
+ const body = await this.client.request(
1334
+ SALES_ENDPOINT,
1335
+ SALES_ADDONS_QUERY,
1336
+ buildSalesAddonsVariables(searchString)
1337
+ );
1338
+ const edges = body.data?.sales?.edges ?? [];
1339
+ if (edges.length === 0) {
1340
+ throw new Error(ERROR_BOOKING_NOT_FOUND);
1341
+ }
1342
+ if (edges.length > 1) {
1343
+ throw new Error(ERROR_MULTIPLE_BOOKINGS_FOUND);
1344
+ }
1345
+ const node = edges[0].node;
1346
+ return {
1347
+ items: parseSaleNode(node),
1348
+ displayId: node.displayId || "",
1349
+ orderId: node.order?.id || "",
1350
+ normalizedBookingId: node.id || ""
1351
+ };
1352
+ }
1353
+ /**
1354
+ * Runs `createQuoteFromOrder` and validates the response, returning the new
1355
+ * quote id and the first sale quote refid (empty string when absent).
1356
+ */
1357
+ async createQuoteFromOrderOrThrow(normalizedOrderId) {
1358
+ const body = await this.client.request(
1359
+ SALES_ENDPOINT,
1360
+ CREATE_QUOTE_FROM_ORDER_MUTATION,
1361
+ { input: { orderId: normalizedOrderId, quoteInput: {} } }
1362
+ );
1363
+ const created = body.data?.createQuoteFromOrder;
1364
+ const quote = created?.quote;
1365
+ if (created?.errors && created.errors.length > 0 || !quote?.id) {
1366
+ throw new Error(ERROR_CREATE_QUOTE_FAILED);
1367
+ }
1368
+ return { quoteId: quote.id, saleQuoteRefid: quote.saleQuotes?.[0]?.refid || "" };
1369
+ }
1370
+ /** Runs `updateQuoteV2` with the given quoteInput and validates the response. */
1371
+ async updateQuoteOrThrow(quoteId, quoteInput) {
1372
+ const body = await this.client.request(
1373
+ SALES_ENDPOINT,
1374
+ UPDATE_QUOTE_V2_MUTATION,
1375
+ { input: { quoteId, quoteInput } }
1376
+ );
1377
+ const updated = body.data?.updateQuoteV2;
1378
+ if (updated?.errors && updated.errors.length > 0 || !updated?.quote) {
1379
+ throw new Error(ERROR_UPDATE_QUOTE_FAILED);
1380
+ }
1381
+ }
1382
+ /** Runs `amendOrder` and validates the response. */
1383
+ async amendOrderOrThrow(quoteId, normalizedOrderId) {
1384
+ const body = await this.client.request(
1385
+ SALES_ENDPOINT,
1386
+ AMEND_ORDER_MUTATION,
1387
+ { input: { quoteId, orderId: normalizedOrderId } }
1388
+ );
1389
+ const amended = body.data?.amendOrder;
1390
+ if (amended?.errors && amended.errors.length > 0 || !amended?.order) {
1391
+ throw new Error(ERROR_AMEND_ORDER_FAILED);
1392
+ }
1393
+ }
1394
+ /**
1395
+ * Creates a booking via createQuoteV2 → createOrderFromQuote, optionally
1396
+ * marking it paid. IDs must be pre-resolved (no free-text matching) — resolve
1397
+ * `activityId` + ticket `resourceOptionId`s from {@link ProductService} and
1398
+ * `availabilityTimeId` from {@link AvailabilityService.getAvailabilityTimes}.
1399
+ *
1400
+ * @example
1401
+ * ```ts
1402
+ * const created = await peek.getBookingService().create({
1403
+ * activityId: "a_kayak_tour",
1404
+ * availabilityTimeId: "at_2026_06_20_0900",
1405
+ * tickets: [{ resourceOptionId: "ro_adult", quantity: 2 }],
1406
+ * guest: { name: "Sam Rivera", email: "sam@example.com" },
1407
+ * markAsPaid: true,
1408
+ * idempotencyKey: crypto.randomUUID(),
1409
+ * });
1410
+ * console.log(created.bookingId, created.displayId, created.balanceFormatted);
1411
+ * ```
1412
+ *
1413
+ * @throws {Error} when `activityId`, `availabilityTimeId`, a ticket
1414
+ * `resourceOptionId`/positive `quantity`, or the guest `name` is missing; when
1415
+ * `markAsPaid` is set without an `idempotencyKey`; or when the quote/order
1416
+ * mutations fail.
1417
+ */
1418
+ async create(input) {
1419
+ validateCreateInput(input);
1420
+ const tickets = input.tickets.flatMap(
1421
+ (ticket) => Array.from({ length: ticket.quantity }, () => ({
1422
+ resourceOptionId: ticket.resourceOptionId,
1423
+ reservationStatus: "CONFIRMED",
1424
+ refid: randomUUID()
1425
+ }))
1426
+ );
1427
+ const bookingQuote = {
1428
+ tickets,
1429
+ activityId: input.activityId,
1430
+ availabilityTimeId: input.availabilityTimeId,
1431
+ refid: randomUUID(),
1432
+ skipCustomerEmail: input.skipCustomerEmail ?? false,
1433
+ guest: {
1434
+ name: input.guest.name,
1435
+ email: input.guest.email ?? null,
1436
+ phone: input.guest.phone ?? null,
1437
+ optinMarketing: input.guest.optinMarketing ?? false,
1438
+ optinSms: input.guest.optinSms ?? false,
1439
+ postalCode: input.guest.postalCode ?? null,
1440
+ country: input.guest.country ?? null
1441
+ }
1442
+ };
1443
+ if (input.operatorNotes) {
1444
+ bookingQuote.operatorNotes = input.operatorNotes;
1445
+ }
1446
+ const quoteInput = { bookingQuotes: [bookingQuote] };
1447
+ if (input.parentOrderId) {
1448
+ quoteInput.source = { clonedFromId: input.parentOrderId };
1449
+ }
1450
+ const quoteBody = await this.client.request(SALES_ENDPOINT, CREATE_QUOTE_V2_MUTATION, {
1451
+ input: { quoteInput }
1452
+ });
1453
+ const quoteResult = quoteBody.data?.createQuoteV2;
1454
+ if (quoteResult?.errors && quoteResult.errors.length > 0 || !quoteResult?.quote?.id) {
1455
+ throw new Error("Failed to create quote");
1456
+ }
1457
+ const quoteId = quoteResult.quote.id;
1458
+ const orderBody = await this.client.request(
1459
+ SALES_ENDPOINT,
1460
+ CREATE_ORDER_FROM_QUOTE_MUTATION,
1461
+ { input: { quoteId } }
1462
+ );
1463
+ const orderResult = orderBody.data?.createOrderFromQuote;
1464
+ if (orderResult?.errors && orderResult.errors.length > 0) {
1465
+ throw new Error("Failed to create order from quote");
1466
+ }
1467
+ const order = orderResult?.order;
1468
+ const sale = order?.sales?.[0];
1469
+ if (!order || !sale) {
1470
+ throw new Error("Order created but no sales found");
1471
+ }
1472
+ const created = {
1473
+ orderId: order.id,
1474
+ bookingId: sale.id,
1475
+ displayId: sale.displayId,
1476
+ balanceAmount: sale.balance?.total?.amount ?? "0.00",
1477
+ balanceCurrency: sale.balance?.total?.currency ?? "USD",
1478
+ balanceFormatted: sale.balance?.total?.formatted ?? "$0.00"
1479
+ };
1480
+ if (input.markAsPaid) {
1481
+ created.transactionId = await this.markCreatedBookingPaid(created, input);
1482
+ }
1483
+ return created;
1484
+ }
1485
+ async markCreatedBookingPaid(booking, input) {
1486
+ const parsedPartial = input.markAsPaidAmount ? parseFloat(input.markAsPaidAmount) : NaN;
1487
+ const amount = !Number.isNaN(parsedPartial) && parsedPartial > 0 ? parsedPartial.toFixed(2) : booking.balanceAmount;
1488
+ const body = await this.client.request(
1489
+ SALES_ENDPOINT,
1490
+ APPLY_PAYMENT_TO_ORDER_MUTATION,
1491
+ {
1492
+ input: {
1493
+ amount: { amount, currency: booking.balanceCurrency },
1494
+ orderId: booking.orderId,
1495
+ paymentSourceId: "custom/other",
1496
+ idempotencyKey: input.idempotencyKey,
1497
+ customerMessage: "Marked as paid",
1498
+ scopedTo: [booking.bookingId]
1499
+ }
1500
+ }
1501
+ );
1502
+ const result = body.data?.applyPaymentToOrder;
1503
+ if (result?.errors && result.errors.length > 0 || !result?.transactionId) {
1504
+ throw new Error("Failed to mark booking as paid");
1505
+ }
1506
+ return result.transactionId;
1507
+ }
1508
+ /** Finds the parent item id of an add-on by matching its option id. */
1509
+ async resolveParentItemId(addonOptionId) {
1510
+ const products = await this.deps.productService.getAllProducts();
1511
+ const matched = products.find(
1512
+ (product) => product.type === ADD_ON_PRODUCT_TYPE && product.tickets.some((ticket) => ticket.id === addonOptionId)
1513
+ );
1514
+ if (!matched) {
1515
+ throw new Error("Add-on not found for the provided addonOptionId");
1516
+ }
1517
+ return matched.productId;
1518
+ }
1519
+ validatePaymentInput(input, normalizedBookingId) {
1520
+ if (!input.paymentSourceId || !input.paymentSourceId.startsWith(PAYMENT_SOURCE_PREFIX) && !ALLOWED_PAYMENT_SOURCE_IDS.includes(input.paymentSourceId)) {
1521
+ throw new Error(
1522
+ "paymentSourceId is required and must start with 'ps_' or be one of 'cash/cash', 'custom/other', 'custom/voucher'"
1523
+ );
1524
+ }
1525
+ assertAmount(input.amount);
1526
+ assertCurrency(input.currency);
1527
+ assertIdempotencyKey(input.idempotencyKey);
1528
+ assertBookingId(normalizedBookingId);
1529
+ }
1530
+ validateRefundInput(input, normalizedBookingId) {
1531
+ if (!input.paymentId || !input.paymentId.startsWith(PAYMENT_ID_PREFIX)) {
1532
+ throw new Error("paymentId is required and must start with 'pmt_'");
1533
+ }
1534
+ assertAmount(input.amount);
1535
+ assertCurrency(input.currency);
1536
+ assertIdempotencyKey(input.idempotencyKey);
1537
+ assertBookingId(normalizedBookingId);
1538
+ }
1539
+ async fetchPaginated(query, baseParams, includeGuests, includePriceBreakdown) {
1540
+ const bookings = [];
1541
+ let after = null;
1542
+ for (; ; ) {
1543
+ const body = await this.client.request(
1544
+ SALES_ENDPOINT,
1545
+ query,
1546
+ buildBookingsVariables({ ...baseParams, pageSize: this.pageSize, after })
1547
+ );
1548
+ const sales = body.data?.sales;
1549
+ for (const edge of sales?.edges ?? []) {
1550
+ bookings.push(fromBookingNode(edge.node, includeGuests, includePriceBreakdown));
1551
+ }
1552
+ const pageInfo = sales?.pageInfo;
1553
+ if (pageInfo?.hasNextPage && pageInfo.endCursor) {
1554
+ after = pageInfo.endCursor;
1555
+ } else {
1556
+ break;
1557
+ }
1558
+ }
1559
+ return bookings;
1560
+ }
1561
+ };
1562
+ function assertAmount(amount) {
1563
+ if (!amount || Number.isNaN(Number(amount))) {
1564
+ throw new Error("amount is required and must be a valid number");
1565
+ }
1566
+ }
1567
+ function assertCurrency(currency) {
1568
+ if (!currency || !CURRENCY_CODE_REGEX.test(currency)) {
1569
+ throw new Error("currency is required and must be a 3-letter uppercase code");
1570
+ }
1571
+ }
1572
+ function assertIdempotencyKey(idempotencyKey) {
1573
+ if (!idempotencyKey) {
1574
+ throw new Error("idempotencyKey is required");
1575
+ }
1576
+ }
1577
+ function assertBookingId(normalizedBookingId) {
1578
+ if (!normalizedBookingId.startsWith(BOOKING_ID_PREFIX)) {
1579
+ throw new Error("bookingId is required and must start with 'b_' or 'B-'");
1580
+ }
1581
+ }
1582
+ function validateCreateInput(input) {
1583
+ if (!input.activityId) throw new Error("activityId is required");
1584
+ if (!input.availabilityTimeId) throw new Error("availabilityTimeId is required");
1585
+ if (!input.tickets || input.tickets.length === 0) {
1586
+ throw new Error("at least one ticket is required");
1587
+ }
1588
+ if (input.tickets.some((ticket) => !ticket.resourceOptionId || !(ticket.quantity > 0))) {
1589
+ throw new Error("each ticket requires a resourceOptionId and a positive quantity");
1590
+ }
1591
+ if (!input.guest || !input.guest.name) {
1592
+ throw new Error("guest name is required");
1593
+ }
1594
+ if (input.markAsPaid && !input.idempotencyKey) {
1595
+ throw new Error("idempotencyKey is required when markAsPaid is set");
1596
+ }
1597
+ }
1598
+ function parseQuantity(value) {
1599
+ if (typeof value !== "string" && typeof value !== "number") {
1600
+ return null;
1601
+ }
1602
+ const str = String(value).trim();
1603
+ if (!/^\d+$/.test(str)) {
1604
+ return null;
1605
+ }
1606
+ const num = parseInt(str, 10);
1607
+ if (!Number.isFinite(num) || num <= 0) {
1608
+ return null;
1609
+ }
1610
+ return num;
1611
+ }
1612
+ function buildCancellation(items, addonOptionId, quantity) {
1613
+ let budget = quantity;
1614
+ const byBookingQuote = /* @__PURE__ */ new Map();
1615
+ for (const item of items) {
1616
+ if (budget <= 0) break;
1617
+ for (const opt of item.addonItemOptions) {
1618
+ if (budget <= 0) break;
1619
+ if (opt.itemReservationStatus !== RESERVATION_STATUS_CONFIRMED || opt.optionReservationStatus !== RESERVATION_STATUS_CONFIRMED || opt.optionId !== addonOptionId) {
1620
+ continue;
1621
+ }
1622
+ let itemMap = byBookingQuote.get(item.bookingQuoteRefid);
1623
+ if (!itemMap) {
1624
+ itemMap = /* @__PURE__ */ new Map();
1625
+ byBookingQuote.set(item.bookingQuoteRefid, itemMap);
1626
+ }
1627
+ let entry = itemMap.get(opt.itemRefid);
1628
+ if (!entry) {
1629
+ entry = { item, canceledOptionRefids: [] };
1630
+ itemMap.set(opt.itemRefid, entry);
1631
+ }
1632
+ entry.canceledOptionRefids.push(opt.optionRefid);
1633
+ budget -= 1;
1634
+ }
1635
+ }
1636
+ const canceledCount = quantity - budget;
1637
+ const bookingQuotes = Array.from(byBookingQuote.entries()).map(
1638
+ ([bookingQuoteRefid, itemMap]) => ({
1639
+ refid: bookingQuoteRefid,
1640
+ addons: Array.from(itemMap.entries()).map(
1641
+ ([itemRefid, { item, canceledOptionRefids }]) => {
1642
+ const canceledSet = new Set(canceledOptionRefids);
1643
+ const itemOptions = canceledOptionRefids.map((refid) => ({
1644
+ reservationStatus: ADDON_OPTION_STATUS_CANCELED,
1645
+ refid
1646
+ }));
1647
+ const allCanceled = item.addonItemOptions.every(
1648
+ (o) => o.optionReservationStatus === ADDON_OPTION_STATUS_CANCELED || canceledSet.has(o.optionRefid)
1649
+ );
1650
+ const addon = { itemOptions, refid: itemRefid };
1651
+ if (allCanceled) {
1652
+ addon.reservationStatus = ADDON_OPTION_STATUS_CANCELED;
1653
+ }
1654
+ return addon;
1655
+ }
1656
+ )
1657
+ })
1658
+ );
1659
+ return { bookingQuotes, canceledCount };
1660
+ }
1661
+
1662
+ // src/internal/daily-notes/daily-note-converter.ts
1663
+ function toDailyNote(response) {
1664
+ const union = response?.dailyNote;
1665
+ if (!union) {
1666
+ return null;
1667
+ }
1668
+ if ("dailyNote" in union) {
1669
+ return cleanNote(union.dailyNote);
1670
+ }
1671
+ return null;
1672
+ }
1673
+ function fromUpsertResponse(response) {
1674
+ return cleanNote(response?.upsertDailyNote?.dailyNote);
1675
+ }
1676
+ function cleanNote(note) {
1677
+ if (!note || typeof note.note !== "string") {
1678
+ return null;
1679
+ }
1680
+ return { note: note.note };
1681
+ }
1682
+
1683
+ // src/internal/daily-notes/daily-note-queries.ts
1684
+ var GLOBAL_NOTE_TYPE = "DASHBOARD";
1685
+ var DAILY_NOTE_TODAY_QUERY = `
1686
+ query dailyNote($type: GlobalNoteType!) {
1687
+ dailyNote(type: $type) {
1688
+ ... on DailyNoteSuccess {
1689
+ dailyNote {
1690
+ note
1691
+ }
1692
+ }
1693
+ ... on NotFoundError {
1694
+ message
1695
+ }
1696
+ }
1697
+ }
1698
+ `;
1699
+ var UPSERT_DAILY_NOTE_MUTATION = `
1700
+ mutation Account($input: UpsertDailyNoteInput!) {
1701
+ upsertDailyNote(input: $input) {
1702
+ ... on UpsertDailyNoteSuccess {
1703
+ dailyNote {
1704
+ note
1705
+ }
1706
+ }
1707
+ }
1708
+ }
1709
+ `;
1710
+ function buildUpsertDailyNoteVariables(note) {
1711
+ return { input: { type: GLOBAL_NOTE_TYPE, note } };
1712
+ }
1713
+
1714
+ // src/internal/daily-notes/daily-note-service.ts
1715
+ var DailyNoteService = class {
1716
+ constructor(client) {
1717
+ this.client = client;
1718
+ }
1719
+ client;
1720
+ /** Returns today's daily note, or null when none is set. */
1721
+ async getToday() {
1722
+ const body = await this.client.request(SALES_ENDPOINT, DAILY_NOTE_TODAY_QUERY, {
1723
+ type: GLOBAL_NOTE_TYPE
1724
+ });
1725
+ return toDailyNote(body.data);
1726
+ }
1727
+ /** Upserts the daily note and returns the saved note, or null. */
1728
+ async update(note) {
1729
+ const body = await this.client.request(
1730
+ SALES_ENDPOINT,
1731
+ UPSERT_DAILY_NOTE_MUTATION,
1732
+ buildUpsertDailyNoteVariables(note)
1733
+ );
1734
+ return fromUpsertResponse(body.data);
1735
+ }
1736
+ };
1737
+
1738
+ // src/errors.ts
1739
+ var AdminAccountRequiredError = class extends Error {
1740
+ /** The HTTP status that triggered this error. */
1741
+ statusCode = 418;
1742
+ constructor(message = "Admin account required") {
1743
+ super(message);
1744
+ this.name = "AdminAccountRequiredError";
1745
+ }
1746
+ };
1747
+ var RateLimitError = class extends Error {
1748
+ /** The HTTP status that triggered this error. */
1749
+ statusCode = 429;
1750
+ constructor(message = "Rate limit exceeded") {
1751
+ super(message);
1752
+ this.name = "RateLimitError";
1753
+ }
1754
+ };
1755
+ var PeekGraphQLError = class extends Error {
1756
+ /** The raw `errors` array returned by the GraphQL endpoint. */
1757
+ graphqlErrors;
1758
+ constructor(graphqlErrors, message = "GraphQL request failed") {
1759
+ super(message);
1760
+ this.name = "PeekGraphQLError";
1761
+ this.graphqlErrors = graphqlErrors;
1762
+ }
1763
+ };
1764
+
1765
+ // src/internal/graphql-client.ts
1766
+ var RATE_LIMIT_STATUS = 429;
1767
+ var ADMIN_ACCOUNT_REQUIRED_STATUS = 418;
1768
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1769
+ var GraphQLClient = class {
1770
+ constructor(options) {
1771
+ this.options = options;
1772
+ }
1773
+ options;
1774
+ /**
1775
+ * Executes a GraphQL query against the named endpoint and returns the raw
1776
+ * response body. Retries on HTTP 429 per the configured backoff.
1777
+ */
1778
+ async request(endpointName, query, variables) {
1779
+ const { retryDelaysMs, logger, fetchFn } = this.options;
1780
+ const url = this.endpoint(endpointName);
1781
+ const collapsedQuery = query.replace(/\s+/g, " ").trim();
1782
+ logger.info("Making GraphQL request", { url, endpointName });
1783
+ for (let attempt = 0; attempt <= retryDelaysMs.length; attempt++) {
1784
+ const response = await fetchFn(url, {
1785
+ method: "POST",
1786
+ headers: this.buildHeaders(),
1787
+ body: JSON.stringify({ query: collapsedQuery, variables })
1788
+ });
1789
+ if (response.status === ADMIN_ACCOUNT_REQUIRED_STATUS) {
1790
+ logger.warn(`Admin account required for ${endpointName} (HTTP 418)`, { url });
1791
+ throw new AdminAccountRequiredError();
1792
+ }
1793
+ if (response.status === RATE_LIMIT_STATUS) {
1794
+ const delay = retryDelaysMs[attempt];
1795
+ if (delay !== void 0) {
1796
+ logger.warn(
1797
+ `Rate limited on ${endpointName}, retrying in ${delay}ms (attempt ${attempt + 1}/${retryDelaysMs.length})`
1798
+ );
1799
+ await sleep(delay);
1800
+ continue;
1801
+ }
1802
+ logger.error(`Rate limit exceeded for ${endpointName}`, { url });
1803
+ throw new RateLimitError();
1804
+ }
1805
+ const body = await response.json();
1806
+ if (body.errors) {
1807
+ logger.error(`GraphQL errors for ${endpointName}`, {
1808
+ url,
1809
+ graphqlErrors: JSON.stringify(body.errors)
1810
+ });
1811
+ throw new PeekGraphQLError(body.errors);
1812
+ }
1813
+ if (!response.ok) {
1814
+ logger.error(`GraphQL request failed with HTTP ${response.status}`, { url });
1815
+ throw new Error(`GraphQL request failed with HTTP ${response.status}`);
1816
+ }
1817
+ return body;
1818
+ }
1819
+ throw new RateLimitError();
1820
+ }
1821
+ endpoint(endpointName) {
1822
+ return `${this.options.baseUrl}/${this.options.appId}/${endpointName}`;
1823
+ }
1824
+ buildHeaders() {
1825
+ return {
1826
+ "X-Peek-Auth": `Bearer ${this.options.getToken()}`,
1827
+ "pk-api-key": this.options.gatewayKey,
1828
+ "Content-Type": "application/json"
1829
+ };
1830
+ }
1831
+ };
1832
+
1833
+ // src/internal/memberships/membership-converter.ts
1834
+ function fromMembershipsResponse(response) {
1835
+ return (response?.memberships ?? []).flatMap(
1836
+ (membership) => (membership.membershipVariants ?? []).map((variant) => ({
1837
+ id: membership.id,
1838
+ membershipVariantId: variant.id,
1839
+ description: variant.description ?? null,
1840
+ externalName: variant.externalName,
1841
+ imageUrl: variant.imageUrl ?? null,
1842
+ internalName: variant.internalName,
1843
+ displayName: membership.name
1844
+ }))
1845
+ );
1846
+ }
1847
+
1848
+ // src/internal/memberships/membership-queries.ts
1849
+ var MEMBERSHIPS_QUERY = `
1850
+ query Sales {
1851
+ memberships {
1852
+ name
1853
+ id
1854
+ membershipVariants {
1855
+ id
1856
+ description
1857
+ imageUrl
1858
+ externalName
1859
+ internalName
1860
+ }
1861
+ }
1862
+ }
1863
+ `;
1864
+ var CREATE_QUOTE_V2_MUTATION2 = `
1865
+ mutation CreateQuoteV2($input: CreateQuoteV2Input!) {
1866
+ createQuoteV2(input: $input) {
1867
+ errors { detail value code }
1868
+ quote { id }
1869
+ }
1870
+ }
1871
+ `;
1872
+ var CREATE_MEMBERSHIP_ORDER_FROM_QUOTE_MUTATION = `
1873
+ mutation CreateOrderFromQuote($input: CreateOrderFromQuoteInput!) {
1874
+ createOrderFromQuote(input: $input) {
1875
+ errors { code detail value }
1876
+ order {
1877
+ id
1878
+ sales {
1879
+ id
1880
+ displayId
1881
+ ... on SoldMembership {
1882
+ id
1883
+ displayId
1884
+ primaryMember { name id }
1885
+ balance { total { amount currency formatted } }
1886
+ }
1887
+ }
1888
+ }
1889
+ }
1890
+ }
1891
+ `;
1892
+
1893
+ // src/internal/memberships/membership-service.ts
1894
+ var QUOTE_STATUS_CONFIRMED = "CONFIRMED";
1895
+ var DEFAULT_BALANCE_AMOUNT = "0.00";
1896
+ var DEFAULT_BALANCE_CURRENCY = "USD";
1897
+ var DEFAULT_BALANCE_FORMATTED = "$0.00";
1898
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1899
+ var COUNTRY_REGEX = /^[A-Z]{2}$/i;
1900
+ var PHONE_REGEX = /^[0-9+\s()\-.]+$/;
1901
+ var MembershipService = class {
1902
+ constructor(client) {
1903
+ this.client = client;
1904
+ }
1905
+ client;
1906
+ /** Returns all membership variants, flattened into {@link Membership} records. */
1907
+ async getAll() {
1908
+ const body = await this.client.request(SALES_ENDPOINT, MEMBERSHIPS_QUERY, {});
1909
+ return fromMembershipsResponse(body.data);
1910
+ }
1911
+ /**
1912
+ * Purchases a membership via the two-step quote → order flow. Throws on
1913
+ * invalid input or when Peek returns errors.
1914
+ */
1915
+ async purchase(input) {
1916
+ validatePurchaseInput(input);
1917
+ const quoteBody = await this.client.request(
1918
+ SALES_ENDPOINT,
1919
+ CREATE_QUOTE_V2_MUTATION2,
1920
+ {
1921
+ input: {
1922
+ quoteInput: {
1923
+ membershipQuotes: [
1924
+ {
1925
+ member: buildMember(input),
1926
+ membershipVariantId: input.membershipVariantId,
1927
+ refid: randomUUID(),
1928
+ status: QUOTE_STATUS_CONFIRMED
1929
+ }
1930
+ ]
1931
+ }
1932
+ }
1933
+ }
1934
+ );
1935
+ const quoteResult = quoteBody.data?.createQuoteV2;
1936
+ if (quoteResult?.errors && quoteResult.errors.length > 0) {
1937
+ throw new Error(`Failed to create membership quote: ${quoteResult.errors[0].detail}`);
1938
+ }
1939
+ const quoteId = quoteResult?.quote?.id;
1940
+ if (!quoteId) {
1941
+ throw new Error("Membership quote created but missing quote id");
1942
+ }
1943
+ const orderBody = await this.client.request(
1944
+ SALES_ENDPOINT,
1945
+ CREATE_MEMBERSHIP_ORDER_FROM_QUOTE_MUTATION,
1946
+ { input: { quoteId } }
1947
+ );
1948
+ const orderResult = orderBody.data?.createOrderFromQuote;
1949
+ if (orderResult?.errors && orderResult.errors.length > 0) {
1950
+ throw new Error(`Failed to create membership order: ${orderResult.errors[0].detail}`);
1951
+ }
1952
+ const order = orderResult?.order;
1953
+ const sale = order?.sales?.[0];
1954
+ if (!order || !sale) {
1955
+ throw new Error("Membership order created but no sales found");
1956
+ }
1957
+ return {
1958
+ orderId: order.id,
1959
+ membershipId: sale.id,
1960
+ displayId: sale.displayId,
1961
+ primaryMemberId: sale.primaryMember?.id ?? null,
1962
+ primaryMemberName: sale.primaryMember?.name ?? null,
1963
+ balanceAmount: sale.balance?.total?.amount ?? DEFAULT_BALANCE_AMOUNT,
1964
+ balanceCurrency: sale.balance?.total?.currency ?? DEFAULT_BALANCE_CURRENCY,
1965
+ balanceFormatted: sale.balance?.total?.formatted ?? DEFAULT_BALANCE_FORMATTED
1966
+ };
1967
+ }
1968
+ };
1969
+ function buildMember(input) {
1970
+ const member = { email: input.email };
1971
+ if (input.country?.trim()) member.country = input.country;
1972
+ if (input.formattedAddress?.trim()) member.formattedAddress = input.formattedAddress;
1973
+ if (input.membershipCode?.trim()) member.membershipCode = input.membershipCode;
1974
+ if (input.name?.trim()) member.name = input.name;
1975
+ if (input.phone?.trim()) member.phone = input.phone;
1976
+ return member;
1977
+ }
1978
+ function validatePurchaseInput(input) {
1979
+ const errors = [];
1980
+ if (!input.membershipVariantId || input.membershipVariantId.trim().length === 0) {
1981
+ errors.push("membershipVariantId is required");
1982
+ }
1983
+ if (!input.email || input.email.trim().length === 0) {
1984
+ errors.push("email is required");
1985
+ } else if (!EMAIL_REGEX.test(input.email)) {
1986
+ errors.push("email is invalid");
1987
+ }
1988
+ if (input.country && !COUNTRY_REGEX.test(input.country)) {
1989
+ errors.push("country is invalid");
1990
+ }
1991
+ if (input.phone && !PHONE_REGEX.test(input.phone)) {
1992
+ errors.push("phone is invalid");
1993
+ }
1994
+ if (errors.length > 0) {
1995
+ throw new Error(errors.join(", "));
1996
+ }
1997
+ }
1998
+
1999
+ // src/internal/resellers/channel-converter.ts
2000
+ function fromChannelsResponse(response) {
2001
+ return (response?.channels ?? []).map(fromChannelNode);
2002
+ }
2003
+ function fromChannelNode(node) {
2004
+ return {
2005
+ id: node.id,
2006
+ name: node.name,
2007
+ notes: node.notes,
2008
+ pricingModel: node.pricingModel,
2009
+ state: node.state,
2010
+ type: node.type,
2011
+ agents: (node.agents?.edges ?? []).map((edge) => ({
2012
+ email: edge.node.email,
2013
+ name: edge.node.name,
2014
+ internalNotes: edge.node.internalNotes,
2015
+ phone: edge.node.phone
2016
+ }))
2017
+ };
2018
+ }
2019
+
2020
+ // src/internal/resellers/channel-queries.ts
2021
+ var CHANNELS_QUERY = `
2022
+ query Sales($first: Int) {
2023
+ channels {
2024
+ id
2025
+ agents(first: $first) {
2026
+ edges {
2027
+ node {
2028
+ email
2029
+ name
2030
+ internalNotes
2031
+ phone
2032
+ }
2033
+ }
2034
+ }
2035
+ name
2036
+ notes
2037
+ state
2038
+ type
2039
+ pricingModel
2040
+ }
2041
+ }
2042
+ `;
2043
+
2044
+ // src/internal/resellers/reseller-service.ts
2045
+ var DEFAULT_AGENTS_PER_CHANNEL = 10;
2046
+ var ResellerService = class {
2047
+ constructor(client) {
2048
+ this.client = client;
2049
+ }
2050
+ client;
2051
+ /**
2052
+ * Returns all reseller channels, each with up to `agentsPerChannel` agents
2053
+ * (default 10).
2054
+ */
2055
+ async getAllChannels(agentsPerChannel = DEFAULT_AGENTS_PER_CHANNEL) {
2056
+ const body = await this.client.request(SALES_ENDPOINT, CHANNELS_QUERY, {
2057
+ first: agentsPerChannel
2058
+ });
2059
+ return fromChannelsResponse(body.data);
2060
+ }
2061
+ };
2062
+
2063
+ // src/internal/resource-pools/resource-pool-converter.ts
2064
+ function fromResourcePoolsResponse(response) {
2065
+ return (response?.resourcePools ?? []).map(fromResourcePoolNode);
2066
+ }
2067
+ function fromResourcePoolNode(pool) {
2068
+ return {
2069
+ id: pool.id || "",
2070
+ name: pool.name || "",
2071
+ imageUrl: pool.imageUrl ?? null,
2072
+ mode: pool.mode || "",
2073
+ colorHex: pool.colorHex ?? null,
2074
+ quantity: pool.quantity ?? null,
2075
+ category: pool.category || "",
2076
+ capacity: pool.capacity ?? null,
2077
+ resourceTrackingMode: pool.resourceTrackingMode ?? null,
2078
+ accountUser: fromAccountUser(pool.accountUser)
2079
+ };
2080
+ }
2081
+ function fromAccountUser(accountUser) {
2082
+ if (!accountUser) {
2083
+ return null;
2084
+ }
2085
+ return {
2086
+ id: accountUser.id || "",
2087
+ name: accountUser.name || ""
2088
+ };
2089
+ }
2090
+
2091
+ // src/internal/resource-pools/resource-pool-queries.ts
2092
+ var RESOURCE_POOLS_QUERY = `
2093
+ query Sales($filter: ResourcePoolsFilter) {
2094
+ resourcePools(filter: $filter) {
2095
+ id
2096
+ name
2097
+ imageUrl
2098
+ mode
2099
+ colorHex
2100
+ quantity
2101
+ category
2102
+ capacity
2103
+ resourceTrackingMode
2104
+ accountUser {
2105
+ id
2106
+ name
2107
+ }
2108
+ }
2109
+ }
2110
+ `;
2111
+
2112
+ // src/internal/resource-pools/resource-pool-service.ts
2113
+ var DEFAULT_MODE = "ALL";
2114
+ var ResourcePoolService = class {
2115
+ constructor(client) {
2116
+ this.client = client;
2117
+ }
2118
+ client;
2119
+ /**
2120
+ * Returns all resource pools for the given mode filter (defaults to `"ALL"`).
2121
+ */
2122
+ async getAll(mode = DEFAULT_MODE) {
2123
+ const body = await this.client.request(
2124
+ SALES_ENDPOINT,
2125
+ RESOURCE_POOLS_QUERY,
2126
+ { filter: { mode } }
2127
+ );
2128
+ return fromResourcePoolsResponse(body.data);
2129
+ }
2130
+ };
2131
+
2132
+ // src/internal/timeslots/timeslot-converter.ts
2133
+ function fromTimeslotNodes(nodes, productId) {
2134
+ if (!Array.isArray(nodes) || nodes.length === 0) {
2135
+ return [];
2136
+ }
2137
+ return nodes.map((node) => fromTimeslotNode(node, productId));
2138
+ }
2139
+ function fromTimeslotNode(node, productId) {
2140
+ if (!node) {
2141
+ return {
2142
+ id: "",
2143
+ productId,
2144
+ totalCapacity: 0,
2145
+ availableCapacity: 0,
2146
+ maxPartySize: 0,
2147
+ bookingCount: 0,
2148
+ checkedInCount: 0,
2149
+ status: "",
2150
+ notes: null,
2151
+ durationMin: 0,
2152
+ date: "",
2153
+ startTime: null,
2154
+ assignedResources: []
2155
+ };
2156
+ }
2157
+ return {
2158
+ id: node.id || "",
2159
+ productId,
2160
+ totalCapacity: node.totalCapacity ?? 0,
2161
+ availableCapacity: node.availableSpots ?? 0,
2162
+ maxPartySize: node.maxPartySize ?? 0,
2163
+ bookingCount: node.bookingCount ?? 0,
2164
+ checkedInCount: node.checkedInCount ?? 0,
2165
+ status: node.status || "",
2166
+ notes: node.manifestNotes ?? null,
2167
+ durationMin: node.minuteLength ?? 0,
2168
+ date: node.date || "",
2169
+ startTime: node.start ?? null,
2170
+ assignedResources: mapAssignedResources(node.resourceAllocations)
2171
+ };
2172
+ }
2173
+ function mapAssignedResources(allocations) {
2174
+ if (!Array.isArray(allocations) || allocations.length === 0) {
2175
+ return [];
2176
+ }
2177
+ return allocations.map((allocation) => {
2178
+ const pool = allocation.resourcePool;
2179
+ return {
2180
+ id: pool?.id || "",
2181
+ name: pool?.name || "",
2182
+ capacity: pool?.capacity ?? 0,
2183
+ category: pool?.category || "",
2184
+ quantity: allocation.quantity ?? 0,
2185
+ accountUserId: pool?.accountUser?.id ?? null
2186
+ };
2187
+ });
2188
+ }
2189
+
2190
+ // src/internal/timeslots/guide-matcher.ts
2191
+ function matchGuideToResourcePool(guideId, guideResourcePools, accountUsers) {
2192
+ const directIdMatch = guideResourcePools.find((pool) => pool.id === guideId);
2193
+ if (directIdMatch) {
2194
+ return directIdMatch.id;
2195
+ }
2196
+ const accountUserMatch = guideResourcePools.find(
2197
+ (pool) => pool.accountUser?.id === guideId
2198
+ );
2199
+ if (accountUserMatch) {
2200
+ return accountUserMatch.id;
2201
+ }
2202
+ const nameMatch = guideResourcePools.find((pool) => pool.name === guideId);
2203
+ if (nameMatch) {
2204
+ return nameMatch.id;
2205
+ }
2206
+ const matchedUser = accountUsers.find((user) => user.id === guideId);
2207
+ if (matchedUser) {
2208
+ const userIdMatch = guideResourcePools.find(
2209
+ (pool) => pool.accountUser?.id === matchedUser.id
2210
+ );
2211
+ if (userIdMatch) {
2212
+ return userIdMatch.id;
2213
+ }
2214
+ const userNameMatch = guideResourcePools.find(
2215
+ (pool) => pool.name === matchedUser.name
2216
+ );
2217
+ if (userNameMatch) {
2218
+ return userNameMatch.id;
2219
+ }
2220
+ }
2221
+ return null;
2222
+ }
2223
+
2224
+ // src/internal/timeslots/resource-allocation-queries.ts
2225
+ var RESOURCE_ALLOCATION_BULK_REQUEST_MUTATION = `
2226
+ mutation ResourceAllocationBulkRequest($input: ResourceAllocationBulkRequestInput!) {
2227
+ resourceAllocationBulkRequest(input: $input) {
2228
+ __typename
2229
+ ... on ResourceAllocationRequest {
2230
+ id
2231
+ }
2232
+ ... on GenericError {
2233
+ __typename
2234
+ message
2235
+ }
2236
+ }
2237
+ }
2238
+ `;
2239
+ function buildResourceAllocationVariables(timeslotIds, resourcePoolIds, status) {
2240
+ return { input: { timeslotIds, resourcePoolIds, status } };
2241
+ }
2242
+
2243
+ // src/internal/timeslots/timeslot-queries.ts
2244
+ var TIMESLOTS_QUERY = `
2245
+ query Sales($params: TimeslotsFilter!) {
2246
+ timeslots(filter: $params) {
2247
+ ... on TimeslotsSuccess {
2248
+ timeslots {
2249
+ id
2250
+ bookingCount
2251
+ availableSpots
2252
+ maxPartySize
2253
+ totalCapacity
2254
+ checkedInCount
2255
+ manifestNotes
2256
+ minuteLength
2257
+ status
2258
+ date
2259
+ resourceAllocations {
2260
+ quantity
2261
+ resourcePool {
2262
+ id
2263
+ name
2264
+ category
2265
+ capacity
2266
+ accountUser {
2267
+ id
2268
+ }
2269
+ }
2270
+ }
2271
+ ... on VariableTimeslot {
2272
+ start
2273
+ }
2274
+ ... on FixedTimeslot {
2275
+ start
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+ }
2281
+ `;
2282
+ var TIMESLOT_BY_ID_QUERY = `
2283
+ query Sales($id: ID!) {
2284
+ timeslot(id: $id) {
2285
+ ... on ActivityTimeslotSuccess {
2286
+ timeslot {
2287
+ id
2288
+ bookingCount
2289
+ availableSpots
2290
+ maxPartySize
2291
+ totalCapacity
2292
+ checkedInCount
2293
+ manifestNotes
2294
+ minuteLength
2295
+ status
2296
+ date
2297
+ resourceAllocations {
2298
+ quantity
2299
+ resourcePool {
2300
+ id
2301
+ name
2302
+ category
2303
+ capacity
2304
+ accountUser {
2305
+ id
2306
+ }
2307
+ }
2308
+ }
2309
+ ... on VariableTimeslot {
2310
+ start
2311
+ }
2312
+ ... on FixedTimeslot {
2313
+ start
2314
+ }
2315
+ }
2316
+ }
2317
+ }
2318
+ }
2319
+ `;
2320
+ var UPDATE_TIMESLOT_MUTATION = `
2321
+ mutation Account($input: UpdateTimeslotInput!) {
2322
+ updateTimeslot(input: $input) {
2323
+ ... on UpdateTimeslotSuccess {
2324
+ timeslot {
2325
+ id
2326
+ manifestNotes
2327
+ status
2328
+ }
2329
+ }
2330
+ }
2331
+ }
2332
+ `;
2333
+ function buildTimeslotVariables(productId, date, filter) {
2334
+ const params = {
2335
+ activityIds: [productId],
2336
+ dateRange: `[${date},${date}]`
2337
+ };
2338
+ if (filter === "withBookings") {
2339
+ params.hasBookings = true;
2340
+ } else if (filter === "withoutBookings") {
2341
+ params.hasBookings = false;
2342
+ }
2343
+ return { params };
2344
+ }
2345
+
2346
+ // src/internal/timeslots/timeslot-service.ts
2347
+ var GUIDE_CATEGORY = "guide";
2348
+ var ERROR_MISSING_TIMESLOTS_OR_GUIDES = "At least one timeslot and one guide are required";
2349
+ var ERROR_INVALID_ACTION = "Invalid action. Must be either assign or unassign";
2350
+ var ERROR_GUIDE_NOT_FOUND = "Guide not found";
2351
+ var TimeslotService = class {
2352
+ constructor(client, deps) {
2353
+ this.client = client;
2354
+ this.deps = deps;
2355
+ }
2356
+ client;
2357
+ deps;
2358
+ /** Returns the timeslots for an activity on a given date. */
2359
+ async getForDay(productId, date, filter = "all") {
2360
+ const normalizedDate = normalizeDate(date);
2361
+ const body = await this.client.request(
2362
+ SALES_ENDPOINT,
2363
+ TIMESLOTS_QUERY,
2364
+ buildTimeslotVariables(productId, normalizedDate, filter)
2365
+ );
2366
+ return fromTimeslotNodes(body.data?.timeslots?.timeslots ?? [], productId);
2367
+ }
2368
+ /** Returns a single timeslot by id, or null when not found. */
2369
+ async getById(timeslotId) {
2370
+ const body = await this.client.request(
2371
+ SALES_ENDPOINT,
2372
+ TIMESLOT_BY_ID_QUERY,
2373
+ { id: timeslotId }
2374
+ );
2375
+ const node = body.data?.timeslot?.timeslot;
2376
+ if (!node) {
2377
+ return null;
2378
+ }
2379
+ return fromTimeslotNode(node, extractProductId(timeslotId));
2380
+ }
2381
+ /** Sets the timeslot's status (e.g. open/closed). */
2382
+ async setAvailability(timeslotId, status) {
2383
+ return this.updateTimeslot({ id: timeslotId, status });
2384
+ }
2385
+ /** Sets the timeslot's manifest notes. */
2386
+ async setNotes(timeslotId, manifestNotes) {
2387
+ return this.updateTimeslot({ id: timeslotId, manifestNotes });
2388
+ }
2389
+ /**
2390
+ * Assigns or unassigns guides across timeslots. Guides are resolved to
2391
+ * resource pools (by pool id, account-user id, or name) before the bulk
2392
+ * allocation request is issued.
2393
+ *
2394
+ * @example
2395
+ * ```ts
2396
+ * await peek.getTimeslotService().assignGuide({
2397
+ * timeslotIds: ["ts_2026_06_20_0900"],
2398
+ * guideIds: ["Alex Guide"], // pool id, account-user id, or name
2399
+ * action: "assign",
2400
+ * });
2401
+ * ```
2402
+ */
2403
+ async assignGuide(assignment) {
2404
+ const { timeslotIds, guideIds, action } = assignment;
2405
+ if (!timeslotIds?.length || !guideIds?.length) {
2406
+ throw new Error(ERROR_MISSING_TIMESLOTS_OR_GUIDES);
2407
+ }
2408
+ if (action !== "assign" && action !== "unassign") {
2409
+ throw new Error(ERROR_INVALID_ACTION);
2410
+ }
2411
+ const [allPools, accountUsers] = await Promise.all([
2412
+ this.deps.resourcePoolService.getAll(),
2413
+ this.deps.accountUserService.getAll()
2414
+ ]);
2415
+ const guidePools = allPools.filter((pool) => pool.category === GUIDE_CATEGORY);
2416
+ const resourcePoolIds = guideIds.map((guideId) => {
2417
+ const matched = matchGuideToResourcePool(guideId, guidePools, accountUsers);
2418
+ if (!matched) {
2419
+ throw new Error(`${ERROR_GUIDE_NOT_FOUND}: ${guideId}`);
2420
+ }
2421
+ return matched;
2422
+ });
2423
+ const status = action === "assign" ? "ACTIVE" : "REMOVAL";
2424
+ const body = await this.client.request(
2425
+ SALES_ENDPOINT,
2426
+ RESOURCE_ALLOCATION_BULK_REQUEST_MUTATION,
2427
+ buildResourceAllocationVariables(timeslotIds, resourcePoolIds, status)
2428
+ );
2429
+ const result = body.data?.resourceAllocationBulkRequest;
2430
+ if (result?.__typename === "ResourceAllocationRequest") {
2431
+ return {
2432
+ status: "success",
2433
+ resourceAllocationRequestId: result.id,
2434
+ errors: null
2435
+ };
2436
+ }
2437
+ return {
2438
+ status: "error",
2439
+ resourceAllocationRequestId: null,
2440
+ errors: [{ message: result?.message ?? "Unknown error" }]
2441
+ };
2442
+ }
2443
+ async updateTimeslot(input) {
2444
+ const body = await this.client.request(
2445
+ SALES_ENDPOINT,
2446
+ UPDATE_TIMESLOT_MUTATION,
2447
+ { input }
2448
+ );
2449
+ const timeslot = body.data?.updateTimeslot?.timeslot;
2450
+ return {
2451
+ manifestNotes: timeslot?.manifestNotes ?? null,
2452
+ status: timeslot?.status ?? null
2453
+ };
2454
+ }
2455
+ };
2456
+ function extractProductId(timeslotId) {
2457
+ return timeslotId.split("|")[0] ?? "";
2458
+ }
2459
+ function normalizeDate(date) {
2460
+ return date.split("T")[0] ?? date;
2461
+ }
2462
+
2463
+ // src/internal/products/product-converter.ts
2464
+ var ADD_ON_COLOR = "#FFFFFF";
2465
+ function fromActivities(activities) {
2466
+ return activities.map(fromActivity);
2467
+ }
2468
+ function fromActivity(activity) {
2469
+ return {
2470
+ // Prefer the primary GraphQL ID for stable product identity.
2471
+ productId: activity.id || activity.legacyId || "",
2472
+ name: activity.name,
2473
+ type: activity.type,
2474
+ color: activity.colorHex || "",
2475
+ tickets: (activity.resourceOptions ?? []).map((option) => ({
2476
+ id: option.id,
2477
+ name: option.name
2478
+ }))
2479
+ };
2480
+ }
2481
+ function fromItemOptionNodes(nodes) {
2482
+ const grouped = /* @__PURE__ */ new Map();
2483
+ for (const node of nodes) {
2484
+ const itemId = node.item?.id;
2485
+ if (!itemId) continue;
2486
+ let product = grouped.get(itemId);
2487
+ if (!product) {
2488
+ product = {
2489
+ productId: itemId,
2490
+ name: node.item.name,
2491
+ type: ADD_ON_PRODUCT_TYPE,
2492
+ color: ADD_ON_COLOR,
2493
+ tickets: []
2494
+ };
2495
+ grouped.set(itemId, product);
2496
+ }
2497
+ product.tickets.push({ id: node.id, name: node.name });
2498
+ }
2499
+ return Array.from(grouped.values());
2500
+ }
2501
+
2502
+ // src/internal/products/product-queries.ts
2503
+ var PRODUCTS_QUERY = `
2504
+ query Sales {
2505
+ activities {
2506
+ name
2507
+ legacyId
2508
+ id
2509
+ deletedAt
2510
+ type
2511
+ colorHex
2512
+ resourceOptions {
2513
+ id
2514
+ name
2515
+ }
2516
+ }
2517
+ }
2518
+ `;
2519
+ var ITEM_OPTIONS_QUERY = `
2520
+ query GetItemOptions($first: Int, $after: String) {
2521
+ itemOptions(first: $first, after: $after) {
2522
+ edges {
2523
+ cursor
2524
+ node {
2525
+ id
2526
+ name
2527
+ description
2528
+ item {
2529
+ id
2530
+ name
2531
+ }
2532
+ }
2533
+ }
2534
+ pageInfo {
2535
+ hasNextPage
2536
+ endCursor
2537
+ }
2538
+ }
2539
+ }
2540
+ `;
2541
+
2542
+ // src/internal/products/product-service.ts
2543
+ var DEFAULT_ITEM_OPTIONS_PAGE_SIZE = 50;
2544
+ var ProductService = class {
2545
+ constructor(client, options = {}) {
2546
+ this.client = client;
2547
+ this.itemOptionsPageSize = options.itemOptionsPageSize ?? DEFAULT_ITEM_OPTIONS_PAGE_SIZE;
2548
+ }
2549
+ client;
2550
+ itemOptionsPageSize;
2551
+ /**
2552
+ * Returns every product as a single flat list: activities plus add-ons (the
2553
+ * latter tagged with the add-on type). Add-ons are gathered across all
2554
+ * cursor-paginated pages.
2555
+ *
2556
+ * @example Split activities from add-ons
2557
+ * ```ts
2558
+ * import { ADD_ON_PRODUCT_TYPE } from "@peektravel/app-utilities";
2559
+ *
2560
+ * const products = await peek.getProductService().getAllProducts();
2561
+ * const activities = products.filter((p) => p.type !== ADD_ON_PRODUCT_TYPE);
2562
+ * const addons = products.filter((p) => p.type === ADD_ON_PRODUCT_TYPE);
2563
+ * ```
2564
+ */
2565
+ async getAllProducts() {
2566
+ const [activities, itemOptionNodes] = await Promise.all([
2567
+ this.fetchActivities(),
2568
+ this.fetchAllItemOptionNodes()
2569
+ ]);
2570
+ return [...fromActivities(activities), ...fromItemOptionNodes(itemOptionNodes)];
2571
+ }
2572
+ async fetchActivities() {
2573
+ const body = await this.client.request(
2574
+ SALES_ENDPOINT,
2575
+ PRODUCTS_QUERY,
2576
+ {}
2577
+ );
2578
+ return body.data?.activities ?? [];
2579
+ }
2580
+ async fetchAllItemOptionNodes() {
2581
+ const all = [];
2582
+ let after = null;
2583
+ for (; ; ) {
2584
+ const body = await this.client.request(
2585
+ SALES_ENDPOINT,
2586
+ ITEM_OPTIONS_QUERY,
2587
+ { first: this.itemOptionsPageSize, after }
2588
+ );
2589
+ const connection = body.data?.itemOptions;
2590
+ for (const edge of connection?.edges ?? []) {
2591
+ all.push(edge.node);
2592
+ }
2593
+ const pageInfo = connection?.pageInfo;
2594
+ if (pageInfo?.hasNextPage && pageInfo.endCursor) {
2595
+ after = pageInfo.endCursor;
2596
+ } else {
2597
+ break;
2598
+ }
2599
+ }
2600
+ return all;
2601
+ }
2602
+ };
2603
+
2604
+ // src/internal/promo-codes/promo-code-queries.ts
2605
+ var PROMO_CODES_QUERY = `
2606
+ query Sales($first: Int, $after: String) {
2607
+ promoCodes(first: $first, after: $after) {
2608
+ edges {
2609
+ node {
2610
+ id
2611
+ name
2612
+ percentAmount
2613
+ perTicketDiscount
2614
+ redemptionCode
2615
+ fixedAmount {
2616
+ amount
2617
+ currency
2618
+ formatted
2619
+ }
2620
+ }
2621
+ }
2622
+ pageInfo {
2623
+ hasNextPage
2624
+ endCursor
2625
+ }
2626
+ }
2627
+ }
2628
+ `;
2629
+ var CREATE_PROMO_CODE_MUTATION = `
2630
+ mutation Sales($input: CreatePromoCodeInput!) {
2631
+ createPromoCode(input: $input) {
2632
+ ... on InvalidDataError {
2633
+ message
2634
+ }
2635
+ ... on PromoCode {
2636
+ id
2637
+ name
2638
+ }
2639
+ }
2640
+ }
2641
+ `;
2642
+
2643
+ // src/internal/promo-codes/promo-code-service.ts
2644
+ var DEFAULT_PAGE_SIZE3 = 50;
2645
+ var DEFAULT_CURRENCY = "USD";
2646
+ var CURRENCY_PATTERN = /^[A-Z]{3}$/;
2647
+ var PromoCodeService = class {
2648
+ constructor(client, options = {}) {
2649
+ this.client = client;
2650
+ this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE3;
2651
+ }
2652
+ client;
2653
+ pageSize;
2654
+ /** Returns all promo codes, walking the cursor pagination to the end. */
2655
+ async getAll() {
2656
+ const all = [];
2657
+ let after = null;
2658
+ for (; ; ) {
2659
+ const body = await this.client.request(SALES_ENDPOINT, PROMO_CODES_QUERY, {
2660
+ first: this.pageSize,
2661
+ after
2662
+ });
2663
+ const connection = body.data?.promoCodes;
2664
+ for (const edge of connection?.edges ?? []) {
2665
+ all.push(edge.node);
2666
+ }
2667
+ const pageInfo = connection?.pageInfo;
2668
+ if (pageInfo?.hasNextPage && pageInfo.endCursor) {
2669
+ after = pageInfo.endCursor;
2670
+ } else {
2671
+ break;
2672
+ }
2673
+ }
2674
+ return all;
2675
+ }
2676
+ /**
2677
+ * Creates a promo code. Throws on invalid input or when Peek returns an
2678
+ * InvalidDataError.
2679
+ */
2680
+ async create(input) {
2681
+ if (!input.name) throw new Error("name is required");
2682
+ if (!input.code) throw new Error("code is required");
2683
+ if (!input.amount) throw new Error("amount is required");
2684
+ if (!input.discountType) throw new Error("discountType is required");
2685
+ if (input.discountType !== "percent" && input.discountType !== "fixed") {
2686
+ throw new Error("discountType must be either 'percent' or 'fixed'");
2687
+ }
2688
+ if (Number.isNaN(parseFloat(input.amount))) {
2689
+ throw new Error("amount must be a valid number");
2690
+ }
2691
+ const currency = input.currency || DEFAULT_CURRENCY;
2692
+ if (!CURRENCY_PATTERN.test(currency)) {
2693
+ throw new Error("currency must be 3 uppercase letters");
2694
+ }
2695
+ const variables = {
2696
+ input: {
2697
+ code: input.code,
2698
+ name: input.name,
2699
+ maxRedeemCount: input.maxRedemptions ? parseInt(String(input.maxRedemptions), 10) : void 0
2700
+ }
2701
+ };
2702
+ if (input.discountType === "fixed") {
2703
+ variables.input.fixedAmount = { amount: input.amount, currency };
2704
+ } else {
2705
+ variables.input.percentAmount = input.amount;
2706
+ }
2707
+ const body = await this.client.request(
2708
+ SALES_ENDPOINT,
2709
+ CREATE_PROMO_CODE_MUTATION,
2710
+ variables
2711
+ );
2712
+ const result = body.data?.createPromoCode;
2713
+ if (result && "message" in result) {
2714
+ throw new Error(result.message);
2715
+ }
2716
+ if (!result) {
2717
+ throw new Error("createPromoCode returned no data");
2718
+ }
2719
+ return { id: result.id, name: result.name };
2720
+ }
2721
+ };
2722
+ var TokenManager = class {
2723
+ constructor(options) {
2724
+ this.options = options;
2725
+ }
2726
+ options;
2727
+ cached;
2728
+ /**
2729
+ * Returns a valid bearer token, reusing the cached one until it is within
2730
+ * `leewaySeconds` of expiring, at which point a fresh token is minted.
2731
+ */
2732
+ getToken() {
2733
+ const now = Date.now();
2734
+ if (this.cached && now < this.cached.expiresAtMs) {
2735
+ return this.cached.token;
2736
+ }
2737
+ const { secret, issuer, installId, ttlSeconds, leewaySeconds } = this.options;
2738
+ const token = jwt.sign({}, secret, {
2739
+ expiresIn: ttlSeconds,
2740
+ issuer,
2741
+ subject: installId
2742
+ });
2743
+ this.cached = {
2744
+ token,
2745
+ expiresAtMs: now + (ttlSeconds - leewaySeconds) * 1e3
2746
+ };
2747
+ return token;
2748
+ }
2749
+ };
2750
+
2751
+ // src/logger.ts
2752
+ var noopLogger = {
2753
+ info() {
2754
+ },
2755
+ warn() {
2756
+ },
2757
+ error() {
2758
+ }
2759
+ };
2760
+
2761
+ // src/peek-access-service.ts
2762
+ var DEFAULT_BASE_URL = "https://apps.peekapis.com/backoffice-gql";
2763
+ var DEFAULT_TOKEN_TTL_SECONDS = 3600;
2764
+ var DEFAULT_TOKEN_REFRESH_LEEWAY_SECONDS = 60;
2765
+ var DEFAULT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
2766
+ var PeekAccessService = class {
2767
+ client;
2768
+ productServiceOptions;
2769
+ productService;
2770
+ accountUserService;
2771
+ resourcePoolService;
2772
+ timeslotService;
2773
+ resellerService;
2774
+ promoCodeService;
2775
+ dailyNoteService;
2776
+ availabilityService;
2777
+ membershipService;
2778
+ bookingService;
2779
+ constructor(config) {
2780
+ requireNonEmpty(config.installId, "installId");
2781
+ requireNonEmpty(config.jwtSecret, "jwtSecret");
2782
+ requireNonEmpty(config.issuer, "issuer");
2783
+ requireNonEmpty(config.appId, "appId");
2784
+ requireNonEmpty(config.gatewayKey, "gatewayKey");
2785
+ const logger = config.logger ?? noopLogger;
2786
+ const tokens = new TokenManager({
2787
+ secret: config.jwtSecret,
2788
+ issuer: config.issuer,
2789
+ installId: config.installId,
2790
+ ttlSeconds: config.tokenTtlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS,
2791
+ leewaySeconds: config.tokenRefreshLeewaySeconds ?? DEFAULT_TOKEN_REFRESH_LEEWAY_SECONDS
2792
+ });
2793
+ this.client = new GraphQLClient({
2794
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
2795
+ appId: config.appId,
2796
+ gatewayKey: config.gatewayKey,
2797
+ getToken: () => tokens.getToken(),
2798
+ retryDelaysMs: config.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
2799
+ logger,
2800
+ fetchFn: config.fetch ?? globalThis.fetch
2801
+ });
2802
+ this.productServiceOptions = {
2803
+ itemOptionsPageSize: config.itemOptionsPageSize
2804
+ };
2805
+ }
2806
+ /**
2807
+ * Returns the {@link ProductService} for this install, bound to the shared
2808
+ * authenticated transport. The instance is created lazily and reused.
2809
+ */
2810
+ getProductService() {
2811
+ if (!this.productService) {
2812
+ this.productService = new ProductService(
2813
+ this.client,
2814
+ this.productServiceOptions
2815
+ );
2816
+ }
2817
+ return this.productService;
2818
+ }
2819
+ /**
2820
+ * Returns the {@link AccountUserService} for this install, bound to the shared
2821
+ * authenticated transport. The instance is created lazily and reused.
2822
+ */
2823
+ getAccountUserService() {
2824
+ if (!this.accountUserService) {
2825
+ this.accountUserService = new AccountUserService(this.client);
2826
+ }
2827
+ return this.accountUserService;
2828
+ }
2829
+ /**
2830
+ * Returns the {@link ResourcePoolService} for this install, bound to the
2831
+ * shared authenticated transport. The instance is created lazily and reused.
2832
+ */
2833
+ getResourcePoolService() {
2834
+ if (!this.resourcePoolService) {
2835
+ this.resourcePoolService = new ResourcePoolService(this.client);
2836
+ }
2837
+ return this.resourcePoolService;
2838
+ }
2839
+ /**
2840
+ * Returns the {@link TimeslotService} for this install, bound to the shared
2841
+ * authenticated transport. The instance is created lazily and reused; its
2842
+ * guide assignment composes the resource-pool and account-user services.
2843
+ */
2844
+ getTimeslotService() {
2845
+ if (!this.timeslotService) {
2846
+ this.timeslotService = new TimeslotService(this.client, {
2847
+ resourcePoolService: this.getResourcePoolService(),
2848
+ accountUserService: this.getAccountUserService()
2849
+ });
2850
+ }
2851
+ return this.timeslotService;
2852
+ }
2853
+ /**
2854
+ * Returns the {@link ResellerService} for this install, bound to the shared
2855
+ * authenticated transport. The instance is created lazily and reused.
2856
+ */
2857
+ getResellerService() {
2858
+ if (!this.resellerService) {
2859
+ this.resellerService = new ResellerService(this.client);
2860
+ }
2861
+ return this.resellerService;
2862
+ }
2863
+ /**
2864
+ * Returns the {@link PromoCodeService} for this install, bound to the shared
2865
+ * authenticated transport. The instance is created lazily and reused.
2866
+ */
2867
+ getPromoCodeService() {
2868
+ if (!this.promoCodeService) {
2869
+ this.promoCodeService = new PromoCodeService(this.client);
2870
+ }
2871
+ return this.promoCodeService;
2872
+ }
2873
+ /**
2874
+ * Returns the {@link DailyNoteService} for this install, bound to the shared
2875
+ * authenticated transport. The instance is created lazily and reused.
2876
+ */
2877
+ getDailyNoteService() {
2878
+ if (!this.dailyNoteService) {
2879
+ this.dailyNoteService = new DailyNoteService(this.client);
2880
+ }
2881
+ return this.dailyNoteService;
2882
+ }
2883
+ /**
2884
+ * Returns the {@link AvailabilityService} for this install, bound to the
2885
+ * shared authenticated transport. The instance is created lazily and reused.
2886
+ */
2887
+ getAvailabilityService() {
2888
+ if (!this.availabilityService) {
2889
+ this.availabilityService = new AvailabilityService(this.client);
2890
+ }
2891
+ return this.availabilityService;
2892
+ }
2893
+ /**
2894
+ * Returns the {@link MembershipService} for this install, bound to the shared
2895
+ * authenticated transport. The instance is created lazily and reused.
2896
+ */
2897
+ getMembershipService() {
2898
+ if (!this.membershipService) {
2899
+ this.membershipService = new MembershipService(this.client);
2900
+ }
2901
+ return this.membershipService;
2902
+ }
2903
+ /**
2904
+ * Returns the {@link BookingService} for this install, bound to the shared
2905
+ * authenticated transport. The instance is created lazily and reused.
2906
+ */
2907
+ getBookingService() {
2908
+ if (!this.bookingService) {
2909
+ this.bookingService = new BookingService(this.client, {
2910
+ productService: this.getProductService()
2911
+ });
2912
+ }
2913
+ return this.bookingService;
2914
+ }
2915
+ };
2916
+ function requireNonEmpty(value, name) {
2917
+ if (!value) {
2918
+ throw new Error(`PeekAccessService: "${name}" is required`);
2919
+ }
2920
+ }
2921
+
2922
+ export { ADD_ON_PRODUCT_TYPE, AccountUserService, AdminAccountRequiredError, AvailabilityService, BookingService, DailyNoteService, MembershipService, PeekAccessService, PeekGraphQLError, ProductService, PromoCodeService, RateLimitError, ResellerService, ResourcePoolService, TimeslotService, noopLogger };
2923
+ //# sourceMappingURL=index.js.map
2924
+ //# sourceMappingURL=index.js.map