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