@peektravel/app-utilities 0.9.2 → 0.9.4

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,261 @@ 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
+ const { value, displayValue } = fromValueNode(response.value);
965
+ return {
966
+ id: field?.id || "",
967
+ name: field?.name || "",
968
+ slug: field?.slug || "",
969
+ type: field?.type || "",
970
+ prompt: prompt?.label ?? null,
971
+ promptHint: prompt?.hint ?? null,
972
+ isRequired: Boolean(prompt?.isRequired),
973
+ refid: response.refid || "",
974
+ displayValue,
975
+ value
976
+ };
977
+ }
978
+ function fromValueNode(value) {
979
+ if (!value || !value.__typename) return { value: null, displayValue: "" };
980
+ switch (value.__typename) {
981
+ case "AgeFieldResponseValue": {
982
+ const age = value.age ?? 0;
983
+ return { value: { kind: "age", age }, displayValue: String(age) };
984
+ }
985
+ case "AttachmentFieldResponseValue": {
986
+ const attachmentUrl = value.attachmentUrl ?? "";
987
+ const name = value.name ?? null;
988
+ return {
989
+ value: {
990
+ kind: "attachment",
991
+ attachmentUrl,
992
+ mime: value.mime ?? null,
993
+ name,
994
+ size: value.size ?? null
995
+ },
996
+ displayValue: name || attachmentUrl
997
+ };
998
+ }
999
+ case "BarcodeFieldResponseValue": {
1000
+ const barcodeValue = value.barcodeValue ?? "";
1001
+ return {
1002
+ value: {
1003
+ kind: "barcode",
1004
+ barcodeType: value.barcodeType ?? null,
1005
+ barcodeValue
1006
+ },
1007
+ displayValue: barcodeValue
1008
+ };
1009
+ }
1010
+ case "BooleanFieldResponseValue": {
1011
+ const bool = Boolean(value.boolean);
1012
+ return { value: { kind: "boolean", boolean: bool }, displayValue: yesNo(bool) };
1013
+ }
1014
+ case "CheckboxFieldResponseValue": {
1015
+ const isChecked = Boolean(value.isChecked);
1016
+ return { value: { kind: "checkbox", isChecked }, displayValue: yesNo(isChecked) };
1017
+ }
1018
+ case "CurrencyFieldResponseValue": {
1019
+ const currency = value.currency ?? "";
1020
+ return { value: { kind: "currency", currency }, displayValue: currency };
1021
+ }
1022
+ case "DateFieldResponseValue": {
1023
+ const date = value.date ?? "";
1024
+ return { value: { kind: "date", date }, displayValue: date };
1025
+ }
1026
+ case "DecimalFieldResponseValue": {
1027
+ const decimal = value.decimal ?? null;
1028
+ return { value: { kind: "decimal", decimal }, displayValue: decimal ?? "" };
1029
+ }
1030
+ case "DurationFieldResponseValue": {
1031
+ const amount = value.duration?.amount ?? 0;
1032
+ const unit = value.duration?.unit ?? null;
1033
+ return { value: { kind: "duration", amount, unit }, displayValue: amountUnit(amount, unit) };
1034
+ }
1035
+ case "EmailFieldResponseValue": {
1036
+ const email = value.email ?? null;
1037
+ return { value: { kind: "email", email }, displayValue: email ?? "" };
1038
+ }
1039
+ case "GuestFieldResponseValue": {
1040
+ const name = value.name ?? null;
1041
+ return {
1042
+ value: {
1043
+ kind: "guest",
1044
+ name,
1045
+ email: value.email ?? null,
1046
+ dateOfBirth: value.dateOfBirth ?? null,
1047
+ notes: value.notes ?? null,
1048
+ waiverSigned: value.waiverSigned ?? null
1049
+ },
1050
+ displayValue: name ?? ""
1051
+ };
1052
+ }
1053
+ case "HtmlFieldResponseValue": {
1054
+ const html = value.html ?? "";
1055
+ return { value: { kind: "html", html }, displayValue: escapeHtml(html) };
1056
+ }
1057
+ case "ImageUrlFieldResponseValue": {
1058
+ const imageUrl = value.imageUrl ?? "";
1059
+ return { value: { kind: "imageUrl", imageUrl }, displayValue: imageUrl };
1060
+ }
1061
+ case "IntegerFieldResponseValue": {
1062
+ const integer = value.integer ?? 0;
1063
+ return { value: { kind: "integer", integer }, displayValue: String(integer) };
1064
+ }
1065
+ case "LocationFieldResponseValue": {
1066
+ const streetAddress = value.streetAddress ?? null;
1067
+ const locality = value.locality ?? null;
1068
+ const region = value.region ?? null;
1069
+ const postalCode = value.postalCode ?? null;
1070
+ const country = value.country ?? null;
1071
+ return {
1072
+ value: {
1073
+ kind: "location",
1074
+ country,
1075
+ county: value.county ?? null,
1076
+ locality,
1077
+ postalCode,
1078
+ region,
1079
+ streetAddress
1080
+ },
1081
+ displayValue: [streetAddress, locality, region, postalCode, country].filter((part) => part).join(", ")
1082
+ };
1083
+ }
1084
+ case "LongTextFieldResponseValue": {
1085
+ const longText = value.longText ?? "";
1086
+ return { value: { kind: "longText", longText }, displayValue: longText };
1087
+ }
1088
+ case "MetaFieldResponseValue": {
1089
+ const meta = value.meta ?? null;
1090
+ return { value: { kind: "meta", meta }, displayValue: meta == null ? "" : JSON.stringify(meta) };
1091
+ }
1092
+ case "PercentFieldResponseValue": {
1093
+ const percent = value.percent ?? null;
1094
+ return { value: { kind: "percent", percent }, displayValue: percent == null ? "" : `${percent}%` };
1095
+ }
1096
+ case "PhoneFieldResponseValue": {
1097
+ const phone = value.phone ?? "";
1098
+ return { value: { kind: "phone", phone }, displayValue: phone };
1099
+ }
1100
+ case "ShortTextFieldResponseValue": {
1101
+ const shortText = value.shortText ?? "";
1102
+ return { value: { kind: "shortText", shortText }, displayValue: shortText };
1103
+ }
1104
+ case "TimeFieldResponseValue": {
1105
+ const time = value.time ?? "";
1106
+ return { value: { kind: "time", time }, displayValue: time };
1107
+ }
1108
+ case "UrlFieldResponseValue": {
1109
+ const url = value.url ?? null;
1110
+ return { value: { kind: "url", url }, displayValue: url ?? "" };
1111
+ }
1112
+ case "VolumeFieldResponseValue": {
1113
+ const amount = value.volume?.amount ?? "0";
1114
+ const unit = value.volume?.unit ?? null;
1115
+ return { value: { kind: "volume", amount, unit }, displayValue: amountUnit(amount, unit) };
1116
+ }
1117
+ case "WeightFieldResponseValue": {
1118
+ const amount = value.weight?.amount ?? "0";
1119
+ const unit = value.weight?.unit ?? null;
1120
+ return { value: { kind: "weight", amount, unit }, displayValue: amountUnit(amount, unit) };
1121
+ }
1122
+ default:
1123
+ return { value: null, displayValue: "" };
1124
+ }
1125
+ }
1126
+ function yesNo(value) {
1127
+ return value ? "Yes" : "No";
1128
+ }
1129
+ function amountUnit(amount, unit) {
1130
+ return unit ? `${amount} ${unit.toLowerCase()}` : String(amount);
1131
+ }
1132
+ function escapeHtml(value) {
1133
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1134
+ }
1135
+ function toSetMetaDataResult(node, bookingId) {
1136
+ return {
1137
+ success: node?.__typename === UPSERT_SUCCESS_TYPENAME,
1138
+ bookingId: node?.bookingId || bookingId,
1139
+ message: node?.message || ""
1140
+ };
1141
+ }
1142
+ function filterMetaDataByIntegrator(metaData, integrator) {
1143
+ const prefix = `integrators:${integrator}:`;
1144
+ return metaData.filter((entry) => entry.slug.startsWith(prefix)).map((entry) => ({ ...entry, slug: entry.slug.slice(prefix.length) }));
1145
+ }
1146
+
1147
+ // src/internal/peek/bookings/metadata-queries.ts
1148
+ var GUEST_VALUE_PII_FIELDS = "dateOfBirth\n email\n name\n ";
1149
+ function buildGuestValueFields(fullCustomerAccess) {
1150
+ return `... on GuestFieldResponseValue {
1151
+ ${fullCustomerAccess ? GUEST_VALUE_PII_FIELDS : ""}notes
1152
+ waiverSigned
1153
+ }`;
1154
+ }
1155
+ function buildMetaDataValueSelection(fullCustomerAccess) {
1156
+ return `value {
1157
+ __typename
1158
+ ... on AgeFieldResponseValue { age }
1159
+ ... on AttachmentFieldResponseValue { attachmentUrl mime name size }
1160
+ ... on BarcodeFieldResponseValue { barcodeType barcodeValue }
1161
+ ... on BooleanFieldResponseValue { boolean }
1162
+ ... on CheckboxFieldResponseValue { isChecked }
1163
+ ... on CurrencyFieldResponseValue { currency }
1164
+ ... on DateFieldResponseValue { date }
1165
+ ... on DecimalFieldResponseValue { decimal }
1166
+ ... on DurationFieldResponseValue { duration { amount unit } }
1167
+ ... on EmailFieldResponseValue { email }
1168
+ ${buildGuestValueFields(fullCustomerAccess)}
1169
+ ... on HtmlFieldResponseValue { html }
1170
+ ... on ImageUrlFieldResponseValue { imageUrl }
1171
+ ... on IntegerFieldResponseValue { integer }
1172
+ ... on LocationFieldResponseValue { country county locality postalCode region streetAddress }
1173
+ ... on LongTextFieldResponseValue { longText }
1174
+ ... on MetaFieldResponseValue { meta }
1175
+ ... on PercentFieldResponseValue { percent }
1176
+ ... on PhoneFieldResponseValue { phone }
1177
+ ... on ShortTextFieldResponseValue { shortText }
1178
+ ... on TimeFieldResponseValue { time }
1179
+ ... on UrlFieldResponseValue { url }
1180
+ ... on VolumeFieldResponseValue { volume { amount unit } }
1181
+ ... on WeightFieldResponseValue { weight { amount unit } }
1182
+ }`;
1183
+ }
1184
+ function buildBookingMetaDataQuery(fullCustomerAccess) {
1185
+ return buildSalesQuery(`
1186
+ ... on Booking {
1187
+ displayId
1188
+ id
1189
+ fieldResponses {
1190
+ fieldLocation {
1191
+ field { id name slug type }
1192
+ prompt { label hint isRequired }
1193
+ }
1194
+ refid
1195
+ ${buildMetaDataValueSelection(fullCustomerAccess)}
1196
+ }
1197
+ }
1198
+ `);
1199
+ }
1200
+ var UPSERT_BOOKING_FIELD_RESPONSES_MUTATION = `
1201
+ mutation UpsertBookingFieldResponses($input: UpsertBookingFieldResponsesInput!) {
1202
+ upsertBookingFieldResponses(input: $input) {
1203
+ __typename
1204
+ ... on UpsertBookingFieldResponsesSuccess { bookingId message }
1205
+ ... on BookingNotFoundError { message bookingId }
1206
+ ... on GenericError { message }
1207
+ }
1208
+ }
1209
+ `;
1210
+
960
1211
  // src/internal/peek/bookings/payments-on-file-converter.ts
961
1212
  function fromPaymentsOnFileResponse(response, bookingId) {
962
1213
  const firstEdge = (response?.sales?.edges ?? [])[0];
@@ -1216,10 +1467,41 @@ function assertRequiredAnswered(resolved, questions) {
1216
1467
  }
1217
1468
  }
1218
1469
 
1470
+ // src/internal/peek/bookings/list-price.ts
1471
+ var ERROR_LIST_PRICE_INVALID = "listPrice must be a positive number string, e.g. '100', '1,000', or '10.00'";
1472
+ function parseListPriceToCents(listPrice) {
1473
+ if (typeof listPrice !== "string") {
1474
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1475
+ }
1476
+ const cleaned = listPrice.replace(/,/g, "").trim();
1477
+ if (!/^\d+(\.\d+)?$/.test(cleaned)) {
1478
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1479
+ }
1480
+ const cents = Math.round(Number(cleaned) * 100);
1481
+ if (!Number.isFinite(cents) || cents <= 0) {
1482
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1483
+ }
1484
+ return cents;
1485
+ }
1486
+ function centsToAmount(cents) {
1487
+ return (cents / 100).toFixed(2);
1488
+ }
1489
+ function distributeListPrice(listPrice, ticketCount) {
1490
+ const totalCents = parseListPriceToCents(listPrice);
1491
+ const perTicketCents = Math.floor(totalCents / ticketCount);
1492
+ const amounts = [];
1493
+ for (let i = 0; i < ticketCount - 1; i += 1) {
1494
+ amounts.push(centsToAmount(perTicketCents));
1495
+ }
1496
+ amounts.push(centsToAmount(totalCents - perTicketCents * (ticketCount - 1)));
1497
+ return amounts;
1498
+ }
1499
+
1219
1500
  // src/internal/peek/bookings/booking-service.ts
1220
1501
  var DEFAULT_PAGE_SIZE2 = 50;
1221
1502
  var DEFAULT_CANCEL_NOTE = "Canceled";
1222
1503
  var DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API";
1504
+ var DEFAULT_CURRENCY = "USD";
1223
1505
  var BOOKING_DB_ID_REGEX = /^b_[a-z0-9]+$/;
1224
1506
  var BOOKING_DISPLAY_ID_REGEX = /^B-[A-Z0-9]+$/;
1225
1507
  var ORDER_DB_ID_REGEX = /^o_[a-z0-9]+$/;
@@ -1232,6 +1514,17 @@ var ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required";
1232
1514
  var ERROR_QUANTITY_INVALID = "quantity must be a positive integer string";
1233
1515
  var ERROR_INVALID_BOOKING_ID = "bookingId is required and must be a valid booking id, e.g. 'b_abc123' or 'B-ABC123'";
1234
1516
  var ERROR_INVALID_ORDER_ID = "orderId is required and must be a valid order id, e.g. 'o_abc123' or 'O-ABC123'";
1517
+ var ERROR_CUSTOM_STATUS_REQUIRED = "customStatus is required and must be a non-empty string";
1518
+ var ERROR_GUESTS_REQUIRED = "at least one guest is required";
1519
+ var ERROR_ATTACHMENTS_REQUIRED = "at least one attachment is required";
1520
+ var META_FIELD_GUEST = "guest";
1521
+ var META_FIELD_ATTACHMENT = "attachment";
1522
+ var META_FIELD_MANIFEST_URL = "manifest_url";
1523
+ var META_FIELD_RESERVATION_ID = "reservation_id";
1524
+ var META_FIELD_BOOKING_STATUS = "booking_status";
1525
+ var META_FIELD_ASSIGNED_PRODUCT = "assigned_product";
1526
+ var META_FIELD_ASSIGNED_EMPLOYEE = "assigned_employee";
1527
+ var META_FIELD_INSURANCE_PURCHASED = "insurance_purchased";
1235
1528
  var ERROR_BOOKING_NOT_FOUND = "Booking not found";
1236
1529
  var ERROR_MULTIPLE_BOOKINGS_FOUND = "Expected exactly one booking for the provided bookingId";
1237
1530
  var ERROR_NO_ADDON_OPTION_TO_REMOVE = "No confirmed add-on option matching addonOptionId was found on the booking";
@@ -1244,12 +1537,15 @@ var BookingService = class {
1244
1537
  this.deps = deps;
1245
1538
  this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE2;
1246
1539
  this.fullCustomerAccess = resolveAccessOptions(options.accessOptions).fullCustomerAccess;
1540
+ this.integrator = options.integrator ?? "";
1247
1541
  }
1248
1542
  client;
1249
1543
  deps;
1250
1544
  pageSize;
1251
1545
  /** Whether customer PII is requested and payment operations are allowed. */
1252
1546
  fullCustomerAccess;
1547
+ /** Integrator id scoping `getMetaData` results. */
1548
+ integrator;
1253
1549
  /**
1254
1550
  * Throws a {@link PiiAccessDisabledError} for `operation` when this service was
1255
1551
  * created without `fullCustomerAccess` — used to gate payment and booking-modification
@@ -1337,6 +1633,131 @@ var BookingService = class {
1337
1633
  );
1338
1634
  return fromBookingGuestsResponse(body.data);
1339
1635
  }
1636
+ /**
1637
+ * Returns a booking's custom-field metadata (its `fieldResponses`), scoped to
1638
+ * this service's integrator. Only fields whose slug starts with
1639
+ * `integrators:<integrator>:` are returned, and that prefix is stripped from
1640
+ * each returned `slug`. Returns `[]` when the booking is not found or has no
1641
+ * matching fields.
1642
+ *
1643
+ * @example
1644
+ * ```ts
1645
+ * const meta = await peek.getBookingService().getMetaData("b_abc123");
1646
+ * for (const field of meta) {
1647
+ * if (field.value?.kind === "url") console.log(field.slug, field.value.url);
1648
+ * }
1649
+ * ```
1650
+ */
1651
+ async getMetaData(bookingId) {
1652
+ assertBookingId(bookingId);
1653
+ const body = await this.client.request(
1654
+ SALES_ENDPOINT,
1655
+ buildBookingMetaDataQuery(this.fullCustomerAccess),
1656
+ buildBookingsVariables({
1657
+ pageSize: this.pageSize,
1658
+ after: null,
1659
+ bookingId: normalizeBookingId(bookingId)
1660
+ })
1661
+ );
1662
+ const firstEdge = (body.data?.sales?.edges ?? [])[0];
1663
+ if (!firstEdge) {
1664
+ return [];
1665
+ }
1666
+ return filterMetaDataByIntegrator(fromBookingMetaDataNode(firstEdge.node), this.integrator);
1667
+ }
1668
+ /**
1669
+ * Sets the booking's `guest` metadata field. Each guest requires a `name` and
1670
+ * `email`; `dateOfBirth`/`waiverSigned`/`notes` are optional.
1671
+ */
1672
+ async setMetaDataGuest(bookingId, guests) {
1673
+ if (!Array.isArray(guests) || guests.length === 0) {
1674
+ throw new Error(ERROR_GUESTS_REQUIRED);
1675
+ }
1676
+ const built = guests.map((guest) => {
1677
+ assertNonEmptyString(guest?.name, "guest name");
1678
+ assertNonEmptyString(guest?.email, "guest email");
1679
+ const out = { name: guest.name, email: guest.email };
1680
+ if (guest.dateOfBirth !== void 0) out.dateOfBirth = guest.dateOfBirth;
1681
+ if (guest.waiverSigned !== void 0) out.waiverSigned = guest.waiverSigned;
1682
+ if (guest.notes !== void 0) out.notes = guest.notes;
1683
+ return out;
1684
+ });
1685
+ return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_GUEST, guests: built });
1686
+ }
1687
+ /**
1688
+ * Sets the booking's `attachment` metadata field. Each attachment requires a
1689
+ * `name` and `attachmentUrl`.
1690
+ */
1691
+ async setMetaDataAttachment(bookingId, attachments) {
1692
+ if (!Array.isArray(attachments) || attachments.length === 0) {
1693
+ throw new Error(ERROR_ATTACHMENTS_REQUIRED);
1694
+ }
1695
+ const built = attachments.map((attachment) => {
1696
+ assertNonEmptyString(attachment?.name, "attachment name");
1697
+ assertNonEmptyString(attachment?.attachmentUrl, "attachment attachmentUrl");
1698
+ return { name: attachment.name, attachmentUrl: attachment.attachmentUrl };
1699
+ });
1700
+ return this.upsertFieldResponse(bookingId, {
1701
+ fieldName: META_FIELD_ATTACHMENT,
1702
+ attachments: built
1703
+ });
1704
+ }
1705
+ /** Sets the booking's `manifest_url` metadata field. */
1706
+ async setMetaDataManifestUrl(bookingId, url) {
1707
+ assertNonEmptyString(url, "url");
1708
+ return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_MANIFEST_URL, url });
1709
+ }
1710
+ /** Sets the booking's `reservation_id` metadata field. */
1711
+ async setMetaDataReservationId(bookingId, reservationId) {
1712
+ return this.setShortTextField(bookingId, META_FIELD_RESERVATION_ID, reservationId, "reservationId");
1713
+ }
1714
+ /** Sets the booking's `booking_status` metadata field. */
1715
+ async setMetaDataBookingStatus(bookingId, status) {
1716
+ return this.setShortTextField(bookingId, META_FIELD_BOOKING_STATUS, status, "status");
1717
+ }
1718
+ /** Sets the booking's `assigned_product` metadata field. */
1719
+ async setMetaDataAssignedProduct(bookingId, productName) {
1720
+ return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_PRODUCT, productName, "productName");
1721
+ }
1722
+ /** Sets the booking's `assigned_employee` metadata field. */
1723
+ async setMetaDataAssignedEmployee(bookingId, employeeName) {
1724
+ return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_EMPLOYEE, employeeName, "employeeName");
1725
+ }
1726
+ /** Sets the booking's `insurance_purchased` metadata field. */
1727
+ async setMetaDataInsurancePurchased(bookingId, insurancePurchased) {
1728
+ if (typeof insurancePurchased !== "boolean") {
1729
+ throw new Error("insurancePurchased is required and must be a boolean");
1730
+ }
1731
+ return this.upsertFieldResponse(bookingId, {
1732
+ fieldName: META_FIELD_INSURANCE_PURCHASED,
1733
+ boolean: insurancePurchased
1734
+ });
1735
+ }
1736
+ /** Validates a non-empty `shortText` value and upserts it under `fieldName`. */
1737
+ setShortTextField(bookingId, fieldName, value, argName) {
1738
+ assertNonEmptyString(value, argName);
1739
+ return this.upsertFieldResponse(bookingId, { fieldName, shortText: value });
1740
+ }
1741
+ /**
1742
+ * Validates the booking id and upserts a single field response (scoped to this
1743
+ * service's integrator), mapping the result union into a {@link SetMetaDataResult}.
1744
+ */
1745
+ async upsertFieldResponse(bookingId, fieldResponse) {
1746
+ assertBookingId(bookingId);
1747
+ const normalized = normalizeBookingId(bookingId);
1748
+ const body = await this.client.request(
1749
+ SALES_ENDPOINT,
1750
+ UPSERT_BOOKING_FIELD_RESPONSES_MUTATION,
1751
+ {
1752
+ input: {
1753
+ bookingId: normalized,
1754
+ integrator: this.integrator,
1755
+ fieldResponses: [fieldResponse]
1756
+ }
1757
+ }
1758
+ );
1759
+ return toSetMetaDataResult(body.data?.upsertBookingFieldResponses, normalized);
1760
+ }
1340
1761
  /** Returns the payments on file for a booking, or null when not found. */
1341
1762
  async getPaymentsOnFile(bookingId) {
1342
1763
  this.assertPiiEnabled("getPaymentsOnFile");
@@ -1381,6 +1802,30 @@ ${note}`;
1381
1802
  });
1382
1803
  return this.getById(normalized);
1383
1804
  }
1805
+ /**
1806
+ * Sets a booking's operator-facing custom status. Returns `true` when the
1807
+ * gateway confirms the stored `operatorStatus` equals the requested
1808
+ * `customStatus`, `false` otherwise.
1809
+ *
1810
+ * @example
1811
+ * ```ts
1812
+ * const ok = await peek.getBookingService().updateCustomStatus("b_abc123", "Awaiting deposit");
1813
+ * ```
1814
+ *
1815
+ * @throws {Error} when `bookingId` is not a valid booking id (`b_…`/`B-…`) or
1816
+ * when `customStatus` is not a non-empty string.
1817
+ */
1818
+ async updateCustomStatus(bookingId, customStatus) {
1819
+ assertBookingId(bookingId);
1820
+ assertCustomStatus(customStatus);
1821
+ const normalized = normalizeBookingId(bookingId);
1822
+ const body = await this.client.request(
1823
+ SALES_ENDPOINT,
1824
+ UPDATE_OPERATOR_STATUS_MUTATION,
1825
+ { input: { id: normalized, operatorStatus: customStatus } }
1826
+ );
1827
+ return body.data?.updateOperatorStatusForBooking?.booking?.operatorStatus === customStatus;
1828
+ }
1384
1829
  /** Cancels a booking and returns its id/displayId/status. */
1385
1830
  async cancel(bookingId, notes = DEFAULT_CANCEL_NOTE) {
1386
1831
  assertBookingId(bookingId);
@@ -1736,13 +2181,26 @@ ${note}`;
1736
2181
  */
1737
2182
  async create(input) {
1738
2183
  validateCreateInput(input);
1739
- const questionAnswers = await this.buildQuestionAnswers(input);
2184
+ const ticketCount = input.tickets.reduce((count, ticket) => count + ticket.quantity, 0);
2185
+ const listPriceAmounts = input.listPrice === void 0 ? null : distributeListPrice(input.listPrice, ticketCount);
2186
+ const [questionAnswers, currency] = await Promise.all([
2187
+ this.buildQuestionAnswers(input),
2188
+ listPriceAmounts ? this.resolveActivityCurrency(input.activityId) : Promise.resolve("")
2189
+ ]);
2190
+ let seatIndex = 0;
1740
2191
  const tickets = input.tickets.flatMap(
1741
- (ticket) => Array.from({ length: ticket.quantity }, () => ({
1742
- resourceOptionId: ticket.resourceOptionId,
1743
- reservationStatus: "CONFIRMED",
1744
- refid: crypto.randomUUID()
1745
- }))
2192
+ (ticket) => Array.from({ length: ticket.quantity }, () => {
2193
+ const seat = {
2194
+ resourceOptionId: ticket.resourceOptionId,
2195
+ reservationStatus: "CONFIRMED",
2196
+ refid: crypto.randomUUID()
2197
+ };
2198
+ if (listPriceAmounts) {
2199
+ seat.price = { amount: listPriceAmounts[seatIndex], currency };
2200
+ }
2201
+ seatIndex += 1;
2202
+ return seat;
2203
+ })
1746
2204
  );
1747
2205
  const bookingQuote = {
1748
2206
  tickets,
@@ -1857,6 +2315,15 @@ ${note}`;
1857
2315
  }
1858
2316
  return result.transactionId;
1859
2317
  }
2318
+ /**
2319
+ * Resolves an activity's ISO currency via the product service, falling back to
2320
+ * {@link DEFAULT_CURRENCY} when the activity (or its currency) can't be found.
2321
+ */
2322
+ async resolveActivityCurrency(activityId) {
2323
+ const products = await this.deps.productService.getAllProducts();
2324
+ const match = products.find((product) => product.productId === activityId);
2325
+ return match?.currency || DEFAULT_CURRENCY;
2326
+ }
1860
2327
  /** Finds the parent item id of an add-on by matching its option id. */
1861
2328
  async resolveParentItemId(addonOptionId) {
1862
2329
  const products = await this.deps.productService.getAllProducts();
@@ -1931,6 +2398,16 @@ function assertBookingId(bookingId) {
1931
2398
  throw new Error(ERROR_INVALID_BOOKING_ID);
1932
2399
  }
1933
2400
  }
2401
+ function assertCustomStatus(customStatus) {
2402
+ if (typeof customStatus !== "string" || customStatus.trim().length === 0) {
2403
+ throw new Error(ERROR_CUSTOM_STATUS_REQUIRED);
2404
+ }
2405
+ }
2406
+ function assertNonEmptyString(value, field) {
2407
+ if (typeof value !== "string" || value.trim().length === 0) {
2408
+ throw new Error(`${field} is required and must be a non-empty string`);
2409
+ }
2410
+ }
1934
2411
  function assertOrderId(orderId) {
1935
2412
  if (!(ORDER_DB_ID_REGEX.test(orderId) || ORDER_DISPLAY_ID_REGEX.test(orderId))) {
1936
2413
  throw new Error(ERROR_INVALID_ORDER_ID);
@@ -2197,7 +2674,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
2197
2674
  // src/internal/sdk-headers.ts
2198
2675
  var SDK_HEADER_NAME = "x-peek-sdk";
2199
2676
  var SDK_PLATFORM = "js";
2200
- var SDK_VERSION = "0.9.2";
2677
+ var SDK_VERSION = "0.9.4";
2201
2678
  var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
2202
2679
 
2203
2680
  // src/internal/peek/graphql-client.ts
@@ -3483,7 +3960,7 @@ var CREATE_PROMO_CODE_MUTATION = `
3483
3960
 
3484
3961
  // src/internal/peek/promo-codes/promo-code-service.ts
3485
3962
  var DEFAULT_PAGE_SIZE3 = 50;
3486
- var DEFAULT_CURRENCY = "USD";
3963
+ var DEFAULT_CURRENCY2 = "USD";
3487
3964
  var CURRENCY_PATTERN = /^[A-Z]{3}$/;
3488
3965
  var PromoCodeService = class {
3489
3966
  constructor(client, options = {}) {
@@ -3529,7 +4006,7 @@ var PromoCodeService = class {
3529
4006
  if (Number.isNaN(parseFloat(input.amount))) {
3530
4007
  throw new Error("amount must be a valid number");
3531
4008
  }
3532
- const currency = input.currency || DEFAULT_CURRENCY;
4009
+ const currency = input.currency || DEFAULT_CURRENCY2;
3533
4010
  if (!CURRENCY_PATTERN.test(currency)) {
3534
4011
  throw new Error("currency must be 3 uppercase letters");
3535
4012
  }
@@ -3903,6 +4380,7 @@ var PeekAccessService = class {
3903
4380
  productServiceOptions;
3904
4381
  accessOptions;
3905
4382
  jwtSecret;
4383
+ issuer;
3906
4384
  productService;
3907
4385
  accountUserService;
3908
4386
  resourcePoolService;
@@ -3913,7 +4391,7 @@ var PeekAccessService = class {
3913
4391
  dailyNoteService;
3914
4392
  availabilityService;
3915
4393
  membershipService;
3916
- bookingService;
4394
+ bookingServices = /* @__PURE__ */ new Map();
3917
4395
  reviewService;
3918
4396
  constructor(config) {
3919
4397
  const hasApiUrl = !!config.apiUrl;
@@ -3924,6 +4402,7 @@ var PeekAccessService = class {
3924
4402
  if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
3925
4403
  if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
3926
4404
  this.jwtSecret = config.jwtSecret;
4405
+ this.issuer = config.issuer;
3927
4406
  const logger = config.logger ?? noopLogger;
3928
4407
  const tokens = createTokenManager(config);
3929
4408
  const useRegistrySlug = hasApiUrl || isV2;
@@ -4084,17 +4563,25 @@ var PeekAccessService = class {
4084
4563
  }
4085
4564
  /**
4086
4565
  * Returns the {@link BookingService} for this install, bound to the shared
4087
- * authenticated transport. The instance is created lazily and reused.
4566
+ * authenticated transport. Instances are created lazily and reused per
4567
+ * `integrator`.
4568
+ *
4569
+ * `integrator` scopes `getMetaData` results to a custom integrator id. **Do
4570
+ * not set it** unless Peek engineering created a custom integrator id for this
4571
+ * integration — omit it and the correct default is used.
4088
4572
  */
4089
- getBookingService() {
4090
- if (!this.bookingService) {
4091
- this.bookingService = new BookingService(
4573
+ getBookingService(integrator) {
4574
+ const resolved = integrator ?? this.issuer;
4575
+ let service = this.bookingServices.get(resolved);
4576
+ if (!service) {
4577
+ service = new BookingService(
4092
4578
  this.client,
4093
4579
  { productService: this.getProductService() },
4094
- { accessOptions: this.accessOptions }
4580
+ { accessOptions: this.accessOptions, integrator: resolved }
4095
4581
  );
4582
+ this.bookingServices.set(resolved, service);
4096
4583
  }
4097
- return this.bookingService;
4584
+ return service;
4098
4585
  }
4099
4586
  /**
4100
4587
  * Returns the {@link ReviewService} for this install, bound to the shared