@peektravel/app-utilities 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -338,6 +338,20 @@ var SEARCH_BY_ACTIVITY_DATE = "activityDate";
338
338
  function normalizeBookingId(bookingId) {
339
339
  return bookingId.toLowerCase().replace(/-/g, "_");
340
340
  }
341
+ function buildSalesQuery(nodeBody) {
342
+ return `
343
+ query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
344
+ sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
345
+ pageInfo { endCursor hasNextPage }
346
+ edges {
347
+ node {
348
+ ${nodeBody}
349
+ }
350
+ }
351
+ }
352
+ }
353
+ `;
354
+ }
341
355
  var GUEST_IDENTITY_PII_FIELDS = "name country dateOfBirth email isGdpr";
342
356
  var GUEST_CONTACT_PII_FIELDS = `phone
343
357
  postalCode
@@ -431,6 +445,7 @@ function buildBookingQueryFields(fullCustomerAccess) {
431
445
  availabilityTimeId
432
446
  ${fullCustomerAccess ? "bookingPortalUrl" : ""}
433
447
  operatorNotes
448
+ operatorStatus
434
449
  value {
435
450
  total {
436
451
  formatted
@@ -513,67 +528,40 @@ function buildBookingsListingQuery(includeGuests, includePriceBreakdown, fullCus
513
528
  const guestsSection = includeGuests ? buildBookingGuestsFields(fullCustomerAccess) : "";
514
529
  const baseFields = buildBookingQueryFields(fullCustomerAccess);
515
530
  const fields = includePriceBreakdown ? baseFields.replace("value {", `value { ${PRICE_BREAKDOWN_FIELDS}`).replace("ticketQuantities {", `ticketQuantities { ${TICKET_VALUE_FIELDS}`) : baseFields;
516
- return `
517
- query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
518
- sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
519
- pageInfo { endCursor hasNextPage }
520
- edges {
521
- node {
522
- ... on Booking {
523
- ${fields}
524
- ${guestsSection}
525
- }
526
- }
527
- }
528
- }
531
+ return buildSalesQuery(`
532
+ ... on Booking {
533
+ ${fields}
534
+ ${guestsSection}
529
535
  }
530
- `;
536
+ `);
531
537
  }
532
538
  function buildBookingGuestsQuery(fullCustomerAccess) {
533
- return `
534
- query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
535
- sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
536
- pageInfo { endCursor hasNextPage }
537
- edges {
538
- node {
539
- ... on Booking {
540
- displayId
541
- id
542
- ${buildBookingGuestsFields(fullCustomerAccess)}
543
- }
544
- }
545
- }
539
+ return buildSalesQuery(`
540
+ ... on Booking {
541
+ displayId
542
+ id
543
+ ${buildBookingGuestsFields(fullCustomerAccess)}
546
544
  }
547
- }
548
- `;
545
+ `);
549
546
  }
550
- var BOOKING_PAYMENTS_ON_FILE_QUERY = `
551
- query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
552
- sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
553
- pageInfo { endCursor hasNextPage }
554
- edges {
555
- node {
556
- order {
557
- payments {
558
- id
559
- paymentSource { id }
560
- appliedAt
561
- currentAmount { amount currency }
562
- refundableAmount { amount currency }
563
- }
564
- id
565
- displayId
566
- paymentSources { description type id }
567
- }
568
- ... on Booking {
569
- displayId
570
- id
571
- }
572
- }
573
- }
547
+ var BOOKING_PAYMENTS_ON_FILE_QUERY = buildSalesQuery(`
548
+ order {
549
+ payments {
550
+ id
551
+ paymentSource { id }
552
+ appliedAt
553
+ currentAmount { amount currency }
554
+ refundableAmount { amount currency }
574
555
  }
556
+ id
557
+ displayId
558
+ paymentSources { description type id }
575
559
  }
576
- `;
560
+ ... on Booking {
561
+ displayId
562
+ id
563
+ }
564
+ `);
577
565
  var UPDATE_OPERATOR_NOTES_MUTATION = `
578
566
  mutation Account($input: UpdateOperatorNotesForBookingInput!) {
579
567
  updateOperatorNotesForBooking(input: $input) {
@@ -581,6 +569,13 @@ var UPDATE_OPERATOR_NOTES_MUTATION = `
581
569
  }
582
570
  }
583
571
  `;
572
+ var UPDATE_OPERATOR_STATUS_MUTATION = `
573
+ mutation Account($input: UpdateOperatorStatusForBookingInput!) {
574
+ updateOperatorStatusForBooking(input: $input) {
575
+ booking { operatorStatus }
576
+ }
577
+ }
578
+ `;
584
579
  var UPDATE_BOOKING_CHECKIN_MUTATION = `
585
580
  mutation Account($input: UpdateBookingCheckInInput!) {
586
581
  updateBookingCheckIn(input: $input) {
@@ -803,6 +798,7 @@ function fromBookingNode(node, includeGuests = false, includePriceBreakdown = fa
803
798
  availabilityTimeId: data.availabilityTimeId || null,
804
799
  portalUrl: data.bookingPortalUrl || null,
805
800
  notes: data.operatorNotes || "",
801
+ customStatus: data.operatorStatus || null,
806
802
  valueDisplay: data.value?.total?.formatted || "",
807
803
  valueAmount: data.value?.total?.amount || "",
808
804
  outstandingBalanceAmount: data.balance?.total?.amount || "",
@@ -957,6 +953,251 @@ function fromBookingGuestsResponse(response) {
957
953
  return guests;
958
954
  }
959
955
 
956
+ // src/internal/peek/bookings/metadata-converter.ts
957
+ var UPSERT_SUCCESS_TYPENAME = "UpsertBookingFieldResponsesSuccess";
958
+ function fromBookingMetaDataNode(node) {
959
+ return (node?.fieldResponses ?? []).map(fromFieldResponseNode);
960
+ }
961
+ function fromFieldResponseNode(response) {
962
+ const field = response.fieldLocation?.field;
963
+ const prompt = response.fieldLocation?.prompt;
964
+ return {
965
+ id: field?.id || "",
966
+ name: field?.name || "",
967
+ slug: field?.slug || "",
968
+ type: field?.type || "",
969
+ prompt: prompt?.label ?? null,
970
+ promptHint: prompt?.hint ?? null,
971
+ isRequired: Boolean(prompt?.isRequired),
972
+ refid: response.refid || "",
973
+ value: fromValueNode(response.value)
974
+ };
975
+ }
976
+ function fromValueNode(value) {
977
+ if (!value || !value.__typename) return null;
978
+ switch (value.__typename) {
979
+ case "AgeFieldResponseValue": {
980
+ const age = value.age ?? 0;
981
+ return { kind: "age", age, displayValue: String(age) };
982
+ }
983
+ case "AttachmentFieldResponseValue": {
984
+ const attachmentUrl = value.attachmentUrl ?? "";
985
+ const name = value.name ?? null;
986
+ return {
987
+ kind: "attachment",
988
+ attachmentUrl,
989
+ mime: value.mime ?? null,
990
+ name,
991
+ size: value.size ?? null,
992
+ displayValue: name || attachmentUrl
993
+ };
994
+ }
995
+ case "BarcodeFieldResponseValue": {
996
+ const barcodeValue = value.barcodeValue ?? "";
997
+ return {
998
+ kind: "barcode",
999
+ barcodeType: value.barcodeType ?? null,
1000
+ barcodeValue,
1001
+ displayValue: barcodeValue
1002
+ };
1003
+ }
1004
+ case "BooleanFieldResponseValue": {
1005
+ const bool = Boolean(value.boolean);
1006
+ return { kind: "boolean", boolean: bool, displayValue: yesNo(bool) };
1007
+ }
1008
+ case "CheckboxFieldResponseValue": {
1009
+ const isChecked = Boolean(value.isChecked);
1010
+ return { kind: "checkbox", isChecked, displayValue: yesNo(isChecked) };
1011
+ }
1012
+ case "CurrencyFieldResponseValue": {
1013
+ const currency = value.currency ?? "";
1014
+ return { kind: "currency", currency, displayValue: currency };
1015
+ }
1016
+ case "DateFieldResponseValue": {
1017
+ const date = value.date ?? "";
1018
+ return { kind: "date", date, displayValue: date };
1019
+ }
1020
+ case "DecimalFieldResponseValue": {
1021
+ const decimal = value.decimal ?? null;
1022
+ return { kind: "decimal", decimal, displayValue: decimal ?? "" };
1023
+ }
1024
+ case "DurationFieldResponseValue": {
1025
+ const amount = value.duration?.amount ?? 0;
1026
+ const unit = value.duration?.unit ?? null;
1027
+ return { kind: "duration", amount, unit, displayValue: amountUnit(amount, unit) };
1028
+ }
1029
+ case "EmailFieldResponseValue": {
1030
+ const email = value.email ?? null;
1031
+ return { kind: "email", email, displayValue: email ?? "" };
1032
+ }
1033
+ case "GuestFieldResponseValue": {
1034
+ const name = value.name ?? null;
1035
+ return {
1036
+ kind: "guest",
1037
+ name,
1038
+ email: value.email ?? null,
1039
+ dateOfBirth: value.dateOfBirth ?? null,
1040
+ notes: value.notes ?? null,
1041
+ waiverSigned: value.waiverSigned ?? null,
1042
+ displayValue: name ?? ""
1043
+ };
1044
+ }
1045
+ case "HtmlFieldResponseValue": {
1046
+ const html = value.html ?? "";
1047
+ return { kind: "html", html, displayValue: escapeHtml(html) };
1048
+ }
1049
+ case "ImageUrlFieldResponseValue": {
1050
+ const imageUrl = value.imageUrl ?? "";
1051
+ return { kind: "imageUrl", imageUrl, displayValue: imageUrl };
1052
+ }
1053
+ case "IntegerFieldResponseValue": {
1054
+ const integer = value.integer ?? 0;
1055
+ return { kind: "integer", integer, displayValue: String(integer) };
1056
+ }
1057
+ case "LocationFieldResponseValue": {
1058
+ const streetAddress = value.streetAddress ?? null;
1059
+ const locality = value.locality ?? null;
1060
+ const region = value.region ?? null;
1061
+ const postalCode = value.postalCode ?? null;
1062
+ const country = value.country ?? null;
1063
+ return {
1064
+ kind: "location",
1065
+ country,
1066
+ county: value.county ?? null,
1067
+ locality,
1068
+ postalCode,
1069
+ region,
1070
+ streetAddress,
1071
+ displayValue: [streetAddress, locality, region, postalCode, country].filter((part) => part).join(", ")
1072
+ };
1073
+ }
1074
+ case "LongTextFieldResponseValue": {
1075
+ const longText = value.longText ?? "";
1076
+ return { kind: "longText", longText, displayValue: longText };
1077
+ }
1078
+ case "MetaFieldResponseValue": {
1079
+ const meta = value.meta ?? null;
1080
+ return { kind: "meta", meta, displayValue: meta == null ? "" : JSON.stringify(meta) };
1081
+ }
1082
+ case "PercentFieldResponseValue": {
1083
+ const percent = value.percent ?? null;
1084
+ return { kind: "percent", percent, displayValue: percent == null ? "" : `${percent}%` };
1085
+ }
1086
+ case "PhoneFieldResponseValue": {
1087
+ const phone = value.phone ?? "";
1088
+ return { kind: "phone", phone, displayValue: phone };
1089
+ }
1090
+ case "ShortTextFieldResponseValue": {
1091
+ const shortText = value.shortText ?? "";
1092
+ return { kind: "shortText", shortText, displayValue: shortText };
1093
+ }
1094
+ case "TimeFieldResponseValue": {
1095
+ const time = value.time ?? "";
1096
+ return { kind: "time", time, displayValue: time };
1097
+ }
1098
+ case "UrlFieldResponseValue": {
1099
+ const url = value.url ?? null;
1100
+ return { kind: "url", url, displayValue: url ?? "" };
1101
+ }
1102
+ case "VolumeFieldResponseValue": {
1103
+ const amount = value.volume?.amount ?? "0";
1104
+ const unit = value.volume?.unit ?? null;
1105
+ return { kind: "volume", amount, unit, displayValue: amountUnit(amount, unit) };
1106
+ }
1107
+ case "WeightFieldResponseValue": {
1108
+ const amount = value.weight?.amount ?? "0";
1109
+ const unit = value.weight?.unit ?? null;
1110
+ return { kind: "weight", amount, unit, displayValue: amountUnit(amount, unit) };
1111
+ }
1112
+ default:
1113
+ return null;
1114
+ }
1115
+ }
1116
+ function yesNo(value) {
1117
+ return value ? "Yes" : "No";
1118
+ }
1119
+ function amountUnit(amount, unit) {
1120
+ return unit ? `${amount} ${unit.toLowerCase()}` : String(amount);
1121
+ }
1122
+ function escapeHtml(value) {
1123
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1124
+ }
1125
+ function toSetMetaDataResult(node, bookingId) {
1126
+ return {
1127
+ success: node?.__typename === UPSERT_SUCCESS_TYPENAME,
1128
+ bookingId: node?.bookingId || bookingId,
1129
+ message: node?.message || ""
1130
+ };
1131
+ }
1132
+ function filterMetaDataByIntegrator(metaData, integrator) {
1133
+ const prefix = `integrator:${integrator}:`;
1134
+ return metaData.filter((entry) => entry.slug.startsWith(prefix)).map((entry) => ({ ...entry, slug: entry.slug.slice(prefix.length) }));
1135
+ }
1136
+
1137
+ // src/internal/peek/bookings/metadata-queries.ts
1138
+ var GUEST_VALUE_PII_FIELDS = "dateOfBirth\n email\n name\n ";
1139
+ function buildGuestValueFields(fullCustomerAccess) {
1140
+ return `... on GuestFieldResponseValue {
1141
+ ${fullCustomerAccess ? GUEST_VALUE_PII_FIELDS : ""}notes
1142
+ waiverSigned
1143
+ }`;
1144
+ }
1145
+ function buildMetaDataValueSelection(fullCustomerAccess) {
1146
+ return `value {
1147
+ __typename
1148
+ ... on AgeFieldResponseValue { age }
1149
+ ... on AttachmentFieldResponseValue { attachmentUrl mime name size }
1150
+ ... on BarcodeFieldResponseValue { barcodeType barcodeValue }
1151
+ ... on BooleanFieldResponseValue { boolean }
1152
+ ... on CheckboxFieldResponseValue { isChecked }
1153
+ ... on CurrencyFieldResponseValue { currency }
1154
+ ... on DateFieldResponseValue { date }
1155
+ ... on DecimalFieldResponseValue { decimal }
1156
+ ... on DurationFieldResponseValue { duration { amount unit } }
1157
+ ... on EmailFieldResponseValue { email }
1158
+ ${buildGuestValueFields(fullCustomerAccess)}
1159
+ ... on HtmlFieldResponseValue { html }
1160
+ ... on ImageUrlFieldResponseValue { imageUrl }
1161
+ ... on IntegerFieldResponseValue { integer }
1162
+ ... on LocationFieldResponseValue { country county locality postalCode region streetAddress }
1163
+ ... on LongTextFieldResponseValue { longText }
1164
+ ... on MetaFieldResponseValue { meta }
1165
+ ... on PercentFieldResponseValue { percent }
1166
+ ... on PhoneFieldResponseValue { phone }
1167
+ ... on ShortTextFieldResponseValue { shortText }
1168
+ ... on TimeFieldResponseValue { time }
1169
+ ... on UrlFieldResponseValue { url }
1170
+ ... on VolumeFieldResponseValue { volume { amount unit } }
1171
+ ... on WeightFieldResponseValue { weight { amount unit } }
1172
+ }`;
1173
+ }
1174
+ function buildBookingMetaDataQuery(fullCustomerAccess) {
1175
+ return buildSalesQuery(`
1176
+ ... on Booking {
1177
+ displayId
1178
+ id
1179
+ fieldResponses {
1180
+ fieldLocation {
1181
+ field { id name slug type }
1182
+ prompt { label hint isRequired }
1183
+ }
1184
+ refid
1185
+ ${buildMetaDataValueSelection(fullCustomerAccess)}
1186
+ }
1187
+ }
1188
+ `);
1189
+ }
1190
+ var UPSERT_BOOKING_FIELD_RESPONSES_MUTATION = `
1191
+ mutation UpsertBookingFieldResponses($input: UpsertBookingFieldResponsesInput!) {
1192
+ upsertBookingFieldResponses(input: $input) {
1193
+ __typename
1194
+ ... on UpsertBookingFieldResponsesSuccess { bookingId message }
1195
+ ... on BookingNotFoundError { message bookingId }
1196
+ ... on GenericError { message }
1197
+ }
1198
+ }
1199
+ `;
1200
+
960
1201
  // src/internal/peek/bookings/payments-on-file-converter.ts
961
1202
  function fromPaymentsOnFileResponse(response, bookingId) {
962
1203
  const firstEdge = (response?.sales?.edges ?? [])[0];
@@ -1112,6 +1353,9 @@ var QUESTION_TYPE_SELECT_ONE = "SELECT_ONE";
1112
1353
  var QUESTION_TYPE_LOCATION = "LOCATION";
1113
1354
  var QUESTION_ID_REGEX = /^cq_[a-z0-9]+$/;
1114
1355
  var OPTION_ID_REGEX = /^cqao_[a-z0-9]+$/;
1356
+ function isCustomQuestionId(idOrText) {
1357
+ return QUESTION_ID_REGEX.test(idOrText);
1358
+ }
1115
1359
  var CHECKBOX_TRUE = /* @__PURE__ */ new Set(["yes", "true"]);
1116
1360
  var CHECKBOX_VALID = /* @__PURE__ */ new Set(["yes", "no", "true", "false"]);
1117
1361
  var CHECKBOX_TEXT_TRUE = "Yes";
@@ -1129,10 +1373,13 @@ var errMissingRequired = (text, id) => `Custom question "${text}" (${id}) is req
1129
1373
  function normalize(value) {
1130
1374
  return value.toLowerCase().replace(/[^a-z0-9]/g, "");
1131
1375
  }
1132
- function resolveQuestion(idOrText, questions) {
1376
+ function resolveQuestion(idOrText, questions, acceptAllIds) {
1133
1377
  if (QUESTION_ID_REGEX.test(idOrText)) {
1134
1378
  const byId = questions.find((question) => question.id === idOrText);
1135
- if (!byId) throw new Error(errUnknownQuestionId(idOrText));
1379
+ if (!byId) {
1380
+ if (acceptAllIds) return null;
1381
+ throw new Error(errUnknownQuestionId(idOrText));
1382
+ }
1136
1383
  return byId;
1137
1384
  }
1138
1385
  const target = normalize(idOrText);
@@ -1157,8 +1404,11 @@ function resolveOption(value, question) {
1157
1404
  if (matches.length > 1) throw new Error(errAmbiguousOption(value, question.id));
1158
1405
  return matches[0];
1159
1406
  }
1160
- function resolveOne(answer, questions) {
1161
- const question = resolveQuestion(answer.questionIdOrText, questions);
1407
+ function resolveOne(answer, questions, acceptAllIds) {
1408
+ const question = resolveQuestion(answer.questionIdOrText, questions, acceptAllIds);
1409
+ if (!question) {
1410
+ return { questionId: answer.questionIdOrText, questionAnswerText: answer.value };
1411
+ }
1162
1412
  if (question.perGuest) {
1163
1413
  throw new Error(errPerGuestUnsupported(question.id));
1164
1414
  }
@@ -1191,7 +1441,8 @@ function resolveOne(answer, questions) {
1191
1441
  }
1192
1442
  }
1193
1443
  function resolveCustomQuestionAnswers(answers, questions, options) {
1194
- const resolved = answers.map((answer) => resolveOne(answer, questions));
1444
+ const acceptAllIds = options?.acceptAllIds ?? false;
1445
+ const resolved = answers.map((answer) => resolveOne(answer, questions, acceptAllIds));
1195
1446
  if (options?.requireRequired) {
1196
1447
  assertRequiredAnswered(resolved, questions);
1197
1448
  }
@@ -1206,10 +1457,41 @@ function assertRequiredAnswered(resolved, questions) {
1206
1457
  }
1207
1458
  }
1208
1459
 
1460
+ // src/internal/peek/bookings/list-price.ts
1461
+ var ERROR_LIST_PRICE_INVALID = "listPrice must be a positive number string, e.g. '100', '1,000', or '10.00'";
1462
+ function parseListPriceToCents(listPrice) {
1463
+ if (typeof listPrice !== "string") {
1464
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1465
+ }
1466
+ const cleaned = listPrice.replace(/,/g, "").trim();
1467
+ if (!/^\d+(\.\d+)?$/.test(cleaned)) {
1468
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1469
+ }
1470
+ const cents = Math.round(Number(cleaned) * 100);
1471
+ if (!Number.isFinite(cents) || cents <= 0) {
1472
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1473
+ }
1474
+ return cents;
1475
+ }
1476
+ function centsToAmount(cents) {
1477
+ return (cents / 100).toFixed(2);
1478
+ }
1479
+ function distributeListPrice(listPrice, ticketCount) {
1480
+ const totalCents = parseListPriceToCents(listPrice);
1481
+ const perTicketCents = Math.floor(totalCents / ticketCount);
1482
+ const amounts = [];
1483
+ for (let i = 0; i < ticketCount - 1; i += 1) {
1484
+ amounts.push(centsToAmount(perTicketCents));
1485
+ }
1486
+ amounts.push(centsToAmount(totalCents - perTicketCents * (ticketCount - 1)));
1487
+ return amounts;
1488
+ }
1489
+
1209
1490
  // src/internal/peek/bookings/booking-service.ts
1210
1491
  var DEFAULT_PAGE_SIZE2 = 50;
1211
1492
  var DEFAULT_CANCEL_NOTE = "Canceled";
1212
1493
  var DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API";
1494
+ var DEFAULT_CURRENCY = "USD";
1213
1495
  var BOOKING_DB_ID_REGEX = /^b_[a-z0-9]+$/;
1214
1496
  var BOOKING_DISPLAY_ID_REGEX = /^B-[A-Z0-9]+$/;
1215
1497
  var ORDER_DB_ID_REGEX = /^o_[a-z0-9]+$/;
@@ -1222,6 +1504,17 @@ var ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required";
1222
1504
  var ERROR_QUANTITY_INVALID = "quantity must be a positive integer string";
1223
1505
  var ERROR_INVALID_BOOKING_ID = "bookingId is required and must be a valid booking id, e.g. 'b_abc123' or 'B-ABC123'";
1224
1506
  var ERROR_INVALID_ORDER_ID = "orderId is required and must be a valid order id, e.g. 'o_abc123' or 'O-ABC123'";
1507
+ var ERROR_CUSTOM_STATUS_REQUIRED = "customStatus is required and must be a non-empty string";
1508
+ var ERROR_GUESTS_REQUIRED = "at least one guest is required";
1509
+ var ERROR_ATTACHMENTS_REQUIRED = "at least one attachment is required";
1510
+ var META_FIELD_GUEST = "guest";
1511
+ var META_FIELD_ATTACHMENT = "attachment";
1512
+ var META_FIELD_MANIFEST_URL = "manifest_url";
1513
+ var META_FIELD_RESERVATION_ID = "reservation_id";
1514
+ var META_FIELD_BOOKING_STATUS = "booking_status";
1515
+ var META_FIELD_ASSIGNED_PRODUCT = "assigned_product";
1516
+ var META_FIELD_ASSIGNED_EMPLOYEE = "assigned_employee";
1517
+ var META_FIELD_INSURANCE_PURCHASED = "insurance_purchased";
1225
1518
  var ERROR_BOOKING_NOT_FOUND = "Booking not found";
1226
1519
  var ERROR_MULTIPLE_BOOKINGS_FOUND = "Expected exactly one booking for the provided bookingId";
1227
1520
  var ERROR_NO_ADDON_OPTION_TO_REMOVE = "No confirmed add-on option matching addonOptionId was found on the booking";
@@ -1234,12 +1527,15 @@ var BookingService = class {
1234
1527
  this.deps = deps;
1235
1528
  this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE2;
1236
1529
  this.fullCustomerAccess = resolveAccessOptions(options.accessOptions).fullCustomerAccess;
1530
+ this.integrator = options.integrator ?? "";
1237
1531
  }
1238
1532
  client;
1239
1533
  deps;
1240
1534
  pageSize;
1241
1535
  /** Whether customer PII is requested and payment operations are allowed. */
1242
1536
  fullCustomerAccess;
1537
+ /** Integrator id scoping `getMetaData` results. */
1538
+ integrator;
1243
1539
  /**
1244
1540
  * Throws a {@link PiiAccessDisabledError} for `operation` when this service was
1245
1541
  * created without `fullCustomerAccess` — used to gate payment and booking-modification
@@ -1327,6 +1623,131 @@ var BookingService = class {
1327
1623
  );
1328
1624
  return fromBookingGuestsResponse(body.data);
1329
1625
  }
1626
+ /**
1627
+ * Returns a booking's custom-field metadata (its `fieldResponses`), scoped to
1628
+ * this service's integrator. Only fields whose slug starts with
1629
+ * `integrator:<integrator>:` are returned, and that prefix is stripped from
1630
+ * each returned `slug`. Returns `[]` when the booking is not found or has no
1631
+ * matching fields.
1632
+ *
1633
+ * @example
1634
+ * ```ts
1635
+ * const meta = await peek.getBookingService().getMetaData("b_abc123");
1636
+ * for (const field of meta) {
1637
+ * if (field.value?.kind === "url") console.log(field.slug, field.value.url);
1638
+ * }
1639
+ * ```
1640
+ */
1641
+ async getMetaData(bookingId) {
1642
+ assertBookingId(bookingId);
1643
+ const body = await this.client.request(
1644
+ SALES_ENDPOINT,
1645
+ buildBookingMetaDataQuery(this.fullCustomerAccess),
1646
+ buildBookingsVariables({
1647
+ pageSize: this.pageSize,
1648
+ after: null,
1649
+ bookingId: normalizeBookingId(bookingId)
1650
+ })
1651
+ );
1652
+ const firstEdge = (body.data?.sales?.edges ?? [])[0];
1653
+ if (!firstEdge) {
1654
+ return [];
1655
+ }
1656
+ return filterMetaDataByIntegrator(fromBookingMetaDataNode(firstEdge.node), this.integrator);
1657
+ }
1658
+ /**
1659
+ * Sets the booking's `guest` metadata field. Each guest requires a `name` and
1660
+ * `email`; `dateOfBirth`/`waiverSigned`/`notes` are optional.
1661
+ */
1662
+ async setMetaDataGuest(bookingId, guests) {
1663
+ if (!Array.isArray(guests) || guests.length === 0) {
1664
+ throw new Error(ERROR_GUESTS_REQUIRED);
1665
+ }
1666
+ const built = guests.map((guest) => {
1667
+ assertNonEmptyString(guest?.name, "guest name");
1668
+ assertNonEmptyString(guest?.email, "guest email");
1669
+ const out = { name: guest.name, email: guest.email };
1670
+ if (guest.dateOfBirth !== void 0) out.dateOfBirth = guest.dateOfBirth;
1671
+ if (guest.waiverSigned !== void 0) out.waiverSigned = guest.waiverSigned;
1672
+ if (guest.notes !== void 0) out.notes = guest.notes;
1673
+ return out;
1674
+ });
1675
+ return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_GUEST, guests: built });
1676
+ }
1677
+ /**
1678
+ * Sets the booking's `attachment` metadata field. Each attachment requires a
1679
+ * `name` and `attachmentUrl`.
1680
+ */
1681
+ async setMetaDataAttachment(bookingId, attachments) {
1682
+ if (!Array.isArray(attachments) || attachments.length === 0) {
1683
+ throw new Error(ERROR_ATTACHMENTS_REQUIRED);
1684
+ }
1685
+ const built = attachments.map((attachment) => {
1686
+ assertNonEmptyString(attachment?.name, "attachment name");
1687
+ assertNonEmptyString(attachment?.attachmentUrl, "attachment attachmentUrl");
1688
+ return { name: attachment.name, attachmentUrl: attachment.attachmentUrl };
1689
+ });
1690
+ return this.upsertFieldResponse(bookingId, {
1691
+ fieldName: META_FIELD_ATTACHMENT,
1692
+ attachments: built
1693
+ });
1694
+ }
1695
+ /** Sets the booking's `manifest_url` metadata field. */
1696
+ async setMetaDataManifestUrl(bookingId, url) {
1697
+ assertNonEmptyString(url, "url");
1698
+ return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_MANIFEST_URL, url });
1699
+ }
1700
+ /** Sets the booking's `reservation_id` metadata field. */
1701
+ async setMetaDataReservationId(bookingId, reservationId) {
1702
+ return this.setShortTextField(bookingId, META_FIELD_RESERVATION_ID, reservationId, "reservationId");
1703
+ }
1704
+ /** Sets the booking's `booking_status` metadata field. */
1705
+ async setMetaDataBookingStatus(bookingId, status) {
1706
+ return this.setShortTextField(bookingId, META_FIELD_BOOKING_STATUS, status, "status");
1707
+ }
1708
+ /** Sets the booking's `assigned_product` metadata field. */
1709
+ async setMetaDataAssignedProduct(bookingId, productName) {
1710
+ return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_PRODUCT, productName, "productName");
1711
+ }
1712
+ /** Sets the booking's `assigned_employee` metadata field. */
1713
+ async setMetaDataAssignedEmployee(bookingId, employeeName) {
1714
+ return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_EMPLOYEE, employeeName, "employeeName");
1715
+ }
1716
+ /** Sets the booking's `insurance_purchased` metadata field. */
1717
+ async setMetaDataInsurancePurchased(bookingId, insurancePurchased) {
1718
+ if (typeof insurancePurchased !== "boolean") {
1719
+ throw new Error("insurancePurchased is required and must be a boolean");
1720
+ }
1721
+ return this.upsertFieldResponse(bookingId, {
1722
+ fieldName: META_FIELD_INSURANCE_PURCHASED,
1723
+ boolean: insurancePurchased
1724
+ });
1725
+ }
1726
+ /** Validates a non-empty `shortText` value and upserts it under `fieldName`. */
1727
+ setShortTextField(bookingId, fieldName, value, argName) {
1728
+ assertNonEmptyString(value, argName);
1729
+ return this.upsertFieldResponse(bookingId, { fieldName, shortText: value });
1730
+ }
1731
+ /**
1732
+ * Validates the booking id and upserts a single field response (scoped to this
1733
+ * service's integrator), mapping the result union into a {@link SetMetaDataResult}.
1734
+ */
1735
+ async upsertFieldResponse(bookingId, fieldResponse) {
1736
+ assertBookingId(bookingId);
1737
+ const normalized = normalizeBookingId(bookingId);
1738
+ const body = await this.client.request(
1739
+ SALES_ENDPOINT,
1740
+ UPSERT_BOOKING_FIELD_RESPONSES_MUTATION,
1741
+ {
1742
+ input: {
1743
+ bookingId: normalized,
1744
+ integrator: this.integrator,
1745
+ fieldResponses: [fieldResponse]
1746
+ }
1747
+ }
1748
+ );
1749
+ return toSetMetaDataResult(body.data?.upsertBookingFieldResponses, normalized);
1750
+ }
1330
1751
  /** Returns the payments on file for a booking, or null when not found. */
1331
1752
  async getPaymentsOnFile(bookingId) {
1332
1753
  this.assertPiiEnabled("getPaymentsOnFile");
@@ -1371,6 +1792,30 @@ ${note}`;
1371
1792
  });
1372
1793
  return this.getById(normalized);
1373
1794
  }
1795
+ /**
1796
+ * Sets a booking's operator-facing custom status. Returns `true` when the
1797
+ * gateway confirms the stored `operatorStatus` equals the requested
1798
+ * `customStatus`, `false` otherwise.
1799
+ *
1800
+ * @example
1801
+ * ```ts
1802
+ * const ok = await peek.getBookingService().updateCustomStatus("b_abc123", "Awaiting deposit");
1803
+ * ```
1804
+ *
1805
+ * @throws {Error} when `bookingId` is not a valid booking id (`b_…`/`B-…`) or
1806
+ * when `customStatus` is not a non-empty string.
1807
+ */
1808
+ async updateCustomStatus(bookingId, customStatus) {
1809
+ assertBookingId(bookingId);
1810
+ assertCustomStatus(customStatus);
1811
+ const normalized = normalizeBookingId(bookingId);
1812
+ const body = await this.client.request(
1813
+ SALES_ENDPOINT,
1814
+ UPDATE_OPERATOR_STATUS_MUTATION,
1815
+ { input: { id: normalized, operatorStatus: customStatus } }
1816
+ );
1817
+ return body.data?.updateOperatorStatusForBooking?.booking?.operatorStatus === customStatus;
1818
+ }
1374
1819
  /** Cancels a booking and returns its id/displayId/status. */
1375
1820
  async cancel(bookingId, notes = DEFAULT_CANCEL_NOTE) {
1376
1821
  assertBookingId(bookingId);
@@ -1726,13 +2171,26 @@ ${note}`;
1726
2171
  */
1727
2172
  async create(input) {
1728
2173
  validateCreateInput(input);
1729
- const questionAnswers = await this.buildQuestionAnswers(input);
2174
+ const ticketCount = input.tickets.reduce((count, ticket) => count + ticket.quantity, 0);
2175
+ const listPriceAmounts = input.listPrice === void 0 ? null : distributeListPrice(input.listPrice, ticketCount);
2176
+ const [questionAnswers, currency] = await Promise.all([
2177
+ this.buildQuestionAnswers(input),
2178
+ listPriceAmounts ? this.resolveActivityCurrency(input.activityId) : Promise.resolve("")
2179
+ ]);
2180
+ let seatIndex = 0;
1730
2181
  const tickets = input.tickets.flatMap(
1731
- (ticket) => Array.from({ length: ticket.quantity }, () => ({
1732
- resourceOptionId: ticket.resourceOptionId,
1733
- reservationStatus: "CONFIRMED",
1734
- refid: crypto.randomUUID()
1735
- }))
2182
+ (ticket) => Array.from({ length: ticket.quantity }, () => {
2183
+ const seat = {
2184
+ resourceOptionId: ticket.resourceOptionId,
2185
+ reservationStatus: "CONFIRMED",
2186
+ refid: crypto.randomUUID()
2187
+ };
2188
+ if (listPriceAmounts) {
2189
+ seat.price = { amount: listPriceAmounts[seatIndex], currency };
2190
+ }
2191
+ seatIndex += 1;
2192
+ return seat;
2193
+ })
1736
2194
  );
1737
2195
  const bookingQuote = {
1738
2196
  tickets,
@@ -1799,19 +2257,22 @@ ${note}`;
1799
2257
  * Resolves the input's custom-question answers into the quote's
1800
2258
  * `questionAnswers` payload (each tagged with a fresh `refid`), or `null` when
1801
2259
  * none were supplied. Fetches the activity's custom questions to validate
1802
- * against; throws on the first unmatched/ambiguous/invalid answer.
2260
+ * against; throws on the first unmatched/ambiguous/invalid answer. When
2261
+ * `acceptAllCustomQuestionIds` is set the fetch is skipped if every answer is
2262
+ * given by id (a by-name answer still needs the list to resolve).
1803
2263
  */
1804
2264
  async buildQuestionAnswers(input) {
1805
2265
  const answers = input.customQuestionAnswers ?? [];
1806
2266
  const requireRequired = input.requireRequiredQuestions ?? false;
2267
+ const acceptAllIds = input.acceptAllCustomQuestionIds ?? false;
1807
2268
  if (answers.length === 0 && !requireRequired) {
1808
2269
  return null;
1809
2270
  }
1810
- const questions = await this.deps.productService.getCustomQuestions(
1811
- input.activityId
1812
- );
2271
+ const needsQuestions = !acceptAllIds || requireRequired || answers.some((answer) => !isCustomQuestionId(answer.questionIdOrText));
2272
+ const questions = needsQuestions ? await this.deps.productService.getCustomQuestions(input.activityId) : [];
1813
2273
  const resolved = resolveCustomQuestionAnswers(answers, questions, {
1814
- requireRequired
2274
+ requireRequired,
2275
+ acceptAllIds
1815
2276
  });
1816
2277
  if (resolved.length === 0) {
1817
2278
  return null;
@@ -1844,6 +2305,15 @@ ${note}`;
1844
2305
  }
1845
2306
  return result.transactionId;
1846
2307
  }
2308
+ /**
2309
+ * Resolves an activity's ISO currency via the product service, falling back to
2310
+ * {@link DEFAULT_CURRENCY} when the activity (or its currency) can't be found.
2311
+ */
2312
+ async resolveActivityCurrency(activityId) {
2313
+ const products = await this.deps.productService.getAllProducts();
2314
+ const match = products.find((product) => product.productId === activityId);
2315
+ return match?.currency || DEFAULT_CURRENCY;
2316
+ }
1847
2317
  /** Finds the parent item id of an add-on by matching its option id. */
1848
2318
  async resolveParentItemId(addonOptionId) {
1849
2319
  const products = await this.deps.productService.getAllProducts();
@@ -1918,6 +2388,16 @@ function assertBookingId(bookingId) {
1918
2388
  throw new Error(ERROR_INVALID_BOOKING_ID);
1919
2389
  }
1920
2390
  }
2391
+ function assertCustomStatus(customStatus) {
2392
+ if (typeof customStatus !== "string" || customStatus.trim().length === 0) {
2393
+ throw new Error(ERROR_CUSTOM_STATUS_REQUIRED);
2394
+ }
2395
+ }
2396
+ function assertNonEmptyString(value, field) {
2397
+ if (typeof value !== "string" || value.trim().length === 0) {
2398
+ throw new Error(`${field} is required and must be a non-empty string`);
2399
+ }
2400
+ }
1921
2401
  function assertOrderId(orderId) {
1922
2402
  if (!(ORDER_DB_ID_REGEX.test(orderId) || ORDER_DISPLAY_ID_REGEX.test(orderId))) {
1923
2403
  throw new Error(ERROR_INVALID_ORDER_ID);
@@ -2184,7 +2664,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
2184
2664
  // src/internal/sdk-headers.ts
2185
2665
  var SDK_HEADER_NAME = "x-peek-sdk";
2186
2666
  var SDK_PLATFORM = "js";
2187
- var SDK_VERSION = "0.9.1";
2667
+ var SDK_VERSION = "0.9.3";
2188
2668
  var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
2189
2669
 
2190
2670
  // src/internal/peek/graphql-client.ts
@@ -2609,6 +3089,23 @@ function fromReviewNode(node) {
2609
3089
  comment: node.comment ?? null
2610
3090
  };
2611
3091
  }
3092
+ function fromActivityReviewSummaryNode(node) {
3093
+ const meta = node.reviewMeta;
3094
+ return {
3095
+ productId: node.id,
3096
+ productName: node.name,
3097
+ avgRating: meta?.avgRating ?? null,
3098
+ countTotal: meta?.count ?? 0,
3099
+ countOneStar: meta?.oneStar ?? 0,
3100
+ countTwoStar: meta?.twoStar ?? 0,
3101
+ countThreeStar: meta?.threeStar ?? 0,
3102
+ countFourStar: meta?.fourStar ?? 0,
3103
+ countFiveStar: meta?.fiveStar ?? 0
3104
+ };
3105
+ }
3106
+ function fromActivityReviewSummaryNodes(nodes) {
3107
+ return nodes.map(fromActivityReviewSummaryNode);
3108
+ }
2612
3109
 
2613
3110
  // src/internal/peek/reviews/review-cursor.ts
2614
3111
  function encodeCursor(offset, pageSize) {
@@ -2644,6 +3141,23 @@ function buildReviewsQuery(fullCustomerAccess) {
2644
3141
  }
2645
3142
  `;
2646
3143
  }
3144
+ var REVIEW_SUMMARIES_QUERY = `
3145
+ query ReviewSummaries {
3146
+ activities {
3147
+ id
3148
+ name
3149
+ reviewMeta {
3150
+ avgRating
3151
+ count
3152
+ fiveStar
3153
+ fourStar
3154
+ threeStar
3155
+ twoStar
3156
+ oneStar
3157
+ }
3158
+ }
3159
+ }
3160
+ `;
2647
3161
  function buildReviewsVariables(params) {
2648
3162
  return {
2649
3163
  first: params.first,
@@ -2703,6 +3217,29 @@ var ReviewService = class {
2703
3217
  const edges = body.data?.reviews?.edges ?? [];
2704
3218
  return edges.map((edge) => fromReviewNode(edge.node));
2705
3219
  }
3220
+ /**
3221
+ * Returns an aggregate review summary for **every** activity — its average
3222
+ * rating and the count of reviews at each star level. This reads the same
3223
+ * activity list as `getAllProducts`; it carries no per-review detail or PII, so
3224
+ * it is unaffected by `fullCustomerAccess`. An activity with no reviews comes
3225
+ * back with a `null` `avgRating` and zero counts.
3226
+ *
3227
+ * @example
3228
+ * ```ts
3229
+ * const summaries = await peek.getReviewService().getAllReviewSummaries();
3230
+ * for (const s of summaries) {
3231
+ * console.log(s.productName, s.avgRating, s.countTotal);
3232
+ * }
3233
+ * ```
3234
+ */
3235
+ async getAllReviewSummaries() {
3236
+ const body = await this.client.request(
3237
+ SALES_ENDPOINT,
3238
+ REVIEW_SUMMARIES_QUERY,
3239
+ {}
3240
+ );
3241
+ return fromActivityReviewSummaryNodes(body.data?.activities ?? []);
3242
+ }
2706
3243
  validate(productId, reviewCount, reviewOffset) {
2707
3244
  if (!productId) {
2708
3245
  throw new Error(ERROR_PRODUCT_ID_REQUIRED);
@@ -3413,7 +3950,7 @@ var CREATE_PROMO_CODE_MUTATION = `
3413
3950
 
3414
3951
  // src/internal/peek/promo-codes/promo-code-service.ts
3415
3952
  var DEFAULT_PAGE_SIZE3 = 50;
3416
- var DEFAULT_CURRENCY = "USD";
3953
+ var DEFAULT_CURRENCY2 = "USD";
3417
3954
  var CURRENCY_PATTERN = /^[A-Z]{3}$/;
3418
3955
  var PromoCodeService = class {
3419
3956
  constructor(client, options = {}) {
@@ -3459,7 +3996,7 @@ var PromoCodeService = class {
3459
3996
  if (Number.isNaN(parseFloat(input.amount))) {
3460
3997
  throw new Error("amount must be a valid number");
3461
3998
  }
3462
- const currency = input.currency || DEFAULT_CURRENCY;
3999
+ const currency = input.currency || DEFAULT_CURRENCY2;
3463
4000
  if (!CURRENCY_PATTERN.test(currency)) {
3464
4001
  throw new Error("currency must be 3 uppercase letters");
3465
4002
  }
@@ -3833,6 +4370,7 @@ var PeekAccessService = class {
3833
4370
  productServiceOptions;
3834
4371
  accessOptions;
3835
4372
  jwtSecret;
4373
+ issuer;
3836
4374
  productService;
3837
4375
  accountUserService;
3838
4376
  resourcePoolService;
@@ -3843,7 +4381,7 @@ var PeekAccessService = class {
3843
4381
  dailyNoteService;
3844
4382
  availabilityService;
3845
4383
  membershipService;
3846
- bookingService;
4384
+ bookingServices = /* @__PURE__ */ new Map();
3847
4385
  reviewService;
3848
4386
  constructor(config) {
3849
4387
  const hasApiUrl = !!config.apiUrl;
@@ -3854,6 +4392,7 @@ var PeekAccessService = class {
3854
4392
  if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
3855
4393
  if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
3856
4394
  this.jwtSecret = config.jwtSecret;
4395
+ this.issuer = config.issuer;
3857
4396
  const logger = config.logger ?? noopLogger;
3858
4397
  const tokens = createTokenManager(config);
3859
4398
  const useRegistrySlug = hasApiUrl || isV2;
@@ -4014,17 +4553,25 @@ var PeekAccessService = class {
4014
4553
  }
4015
4554
  /**
4016
4555
  * Returns the {@link BookingService} for this install, bound to the shared
4017
- * authenticated transport. The instance is created lazily and reused.
4556
+ * authenticated transport. Instances are created lazily and reused per
4557
+ * `integrator`.
4558
+ *
4559
+ * `integrator` scopes `getMetaData` results to a custom integrator id. **Do
4560
+ * not set it** unless Peek engineering created a custom integrator id for this
4561
+ * integration — omit it and the correct default is used.
4018
4562
  */
4019
- getBookingService() {
4020
- if (!this.bookingService) {
4021
- this.bookingService = new BookingService(
4563
+ getBookingService(integrator) {
4564
+ const resolved = integrator ?? this.issuer;
4565
+ let service = this.bookingServices.get(resolved);
4566
+ if (!service) {
4567
+ service = new BookingService(
4022
4568
  this.client,
4023
4569
  { productService: this.getProductService() },
4024
- { accessOptions: this.accessOptions }
4570
+ { accessOptions: this.accessOptions, integrator: resolved }
4025
4571
  );
4572
+ this.bookingServices.set(resolved, service);
4026
4573
  }
4027
- return this.bookingService;
4574
+ return service;
4028
4575
  }
4029
4576
  /**
4030
4577
  * Returns the {@link ReviewService} for this install, bound to the shared