@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/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/index.cjs +2961 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1272 -0
- package/dist/index.d.ts +1272 -0
- package/dist/index.js +2924 -0
- package/dist/index.js.map +1 -0
- package/llms.txt +77 -0
- package/package.json +68 -0
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
|