@peektravel/app-utilities 0.9.2 → 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];
@@ -1216,10 +1457,41 @@ function assertRequiredAnswered(resolved, questions) {
1216
1457
  }
1217
1458
  }
1218
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
+
1219
1490
  // src/internal/peek/bookings/booking-service.ts
1220
1491
  var DEFAULT_PAGE_SIZE2 = 50;
1221
1492
  var DEFAULT_CANCEL_NOTE = "Canceled";
1222
1493
  var DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API";
1494
+ var DEFAULT_CURRENCY = "USD";
1223
1495
  var BOOKING_DB_ID_REGEX = /^b_[a-z0-9]+$/;
1224
1496
  var BOOKING_DISPLAY_ID_REGEX = /^B-[A-Z0-9]+$/;
1225
1497
  var ORDER_DB_ID_REGEX = /^o_[a-z0-9]+$/;
@@ -1232,6 +1504,17 @@ var ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required";
1232
1504
  var ERROR_QUANTITY_INVALID = "quantity must be a positive integer string";
1233
1505
  var ERROR_INVALID_BOOKING_ID = "bookingId is required and must be a valid booking id, e.g. 'b_abc123' or 'B-ABC123'";
1234
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";
1235
1518
  var ERROR_BOOKING_NOT_FOUND = "Booking not found";
1236
1519
  var ERROR_MULTIPLE_BOOKINGS_FOUND = "Expected exactly one booking for the provided bookingId";
1237
1520
  var ERROR_NO_ADDON_OPTION_TO_REMOVE = "No confirmed add-on option matching addonOptionId was found on the booking";
@@ -1244,12 +1527,15 @@ var BookingService = class {
1244
1527
  this.deps = deps;
1245
1528
  this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE2;
1246
1529
  this.fullCustomerAccess = resolveAccessOptions(options.accessOptions).fullCustomerAccess;
1530
+ this.integrator = options.integrator ?? "";
1247
1531
  }
1248
1532
  client;
1249
1533
  deps;
1250
1534
  pageSize;
1251
1535
  /** Whether customer PII is requested and payment operations are allowed. */
1252
1536
  fullCustomerAccess;
1537
+ /** Integrator id scoping `getMetaData` results. */
1538
+ integrator;
1253
1539
  /**
1254
1540
  * Throws a {@link PiiAccessDisabledError} for `operation` when this service was
1255
1541
  * created without `fullCustomerAccess` — used to gate payment and booking-modification
@@ -1337,6 +1623,131 @@ var BookingService = class {
1337
1623
  );
1338
1624
  return fromBookingGuestsResponse(body.data);
1339
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
+ }
1340
1751
  /** Returns the payments on file for a booking, or null when not found. */
1341
1752
  async getPaymentsOnFile(bookingId) {
1342
1753
  this.assertPiiEnabled("getPaymentsOnFile");
@@ -1381,6 +1792,30 @@ ${note}`;
1381
1792
  });
1382
1793
  return this.getById(normalized);
1383
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
+ }
1384
1819
  /** Cancels a booking and returns its id/displayId/status. */
1385
1820
  async cancel(bookingId, notes = DEFAULT_CANCEL_NOTE) {
1386
1821
  assertBookingId(bookingId);
@@ -1736,13 +2171,26 @@ ${note}`;
1736
2171
  */
1737
2172
  async create(input) {
1738
2173
  validateCreateInput(input);
1739
- 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;
1740
2181
  const tickets = input.tickets.flatMap(
1741
- (ticket) => Array.from({ length: ticket.quantity }, () => ({
1742
- resourceOptionId: ticket.resourceOptionId,
1743
- reservationStatus: "CONFIRMED",
1744
- refid: crypto.randomUUID()
1745
- }))
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
+ })
1746
2194
  );
1747
2195
  const bookingQuote = {
1748
2196
  tickets,
@@ -1857,6 +2305,15 @@ ${note}`;
1857
2305
  }
1858
2306
  return result.transactionId;
1859
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
+ }
1860
2317
  /** Finds the parent item id of an add-on by matching its option id. */
1861
2318
  async resolveParentItemId(addonOptionId) {
1862
2319
  const products = await this.deps.productService.getAllProducts();
@@ -1931,6 +2388,16 @@ function assertBookingId(bookingId) {
1931
2388
  throw new Error(ERROR_INVALID_BOOKING_ID);
1932
2389
  }
1933
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
+ }
1934
2401
  function assertOrderId(orderId) {
1935
2402
  if (!(ORDER_DB_ID_REGEX.test(orderId) || ORDER_DISPLAY_ID_REGEX.test(orderId))) {
1936
2403
  throw new Error(ERROR_INVALID_ORDER_ID);
@@ -2197,7 +2664,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
2197
2664
  // src/internal/sdk-headers.ts
2198
2665
  var SDK_HEADER_NAME = "x-peek-sdk";
2199
2666
  var SDK_PLATFORM = "js";
2200
- var SDK_VERSION = "0.9.2";
2667
+ var SDK_VERSION = "0.9.3";
2201
2668
  var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
2202
2669
 
2203
2670
  // src/internal/peek/graphql-client.ts
@@ -3483,7 +3950,7 @@ var CREATE_PROMO_CODE_MUTATION = `
3483
3950
 
3484
3951
  // src/internal/peek/promo-codes/promo-code-service.ts
3485
3952
  var DEFAULT_PAGE_SIZE3 = 50;
3486
- var DEFAULT_CURRENCY = "USD";
3953
+ var DEFAULT_CURRENCY2 = "USD";
3487
3954
  var CURRENCY_PATTERN = /^[A-Z]{3}$/;
3488
3955
  var PromoCodeService = class {
3489
3956
  constructor(client, options = {}) {
@@ -3529,7 +3996,7 @@ var PromoCodeService = class {
3529
3996
  if (Number.isNaN(parseFloat(input.amount))) {
3530
3997
  throw new Error("amount must be a valid number");
3531
3998
  }
3532
- const currency = input.currency || DEFAULT_CURRENCY;
3999
+ const currency = input.currency || DEFAULT_CURRENCY2;
3533
4000
  if (!CURRENCY_PATTERN.test(currency)) {
3534
4001
  throw new Error("currency must be 3 uppercase letters");
3535
4002
  }
@@ -3903,6 +4370,7 @@ var PeekAccessService = class {
3903
4370
  productServiceOptions;
3904
4371
  accessOptions;
3905
4372
  jwtSecret;
4373
+ issuer;
3906
4374
  productService;
3907
4375
  accountUserService;
3908
4376
  resourcePoolService;
@@ -3913,7 +4381,7 @@ var PeekAccessService = class {
3913
4381
  dailyNoteService;
3914
4382
  availabilityService;
3915
4383
  membershipService;
3916
- bookingService;
4384
+ bookingServices = /* @__PURE__ */ new Map();
3917
4385
  reviewService;
3918
4386
  constructor(config) {
3919
4387
  const hasApiUrl = !!config.apiUrl;
@@ -3924,6 +4392,7 @@ var PeekAccessService = class {
3924
4392
  if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
3925
4393
  if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
3926
4394
  this.jwtSecret = config.jwtSecret;
4395
+ this.issuer = config.issuer;
3927
4396
  const logger = config.logger ?? noopLogger;
3928
4397
  const tokens = createTokenManager(config);
3929
4398
  const useRegistrySlug = hasApiUrl || isV2;
@@ -4084,17 +4553,25 @@ var PeekAccessService = class {
4084
4553
  }
4085
4554
  /**
4086
4555
  * Returns the {@link BookingService} for this install, bound to the shared
4087
- * 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.
4088
4562
  */
4089
- getBookingService() {
4090
- if (!this.bookingService) {
4091
- 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(
4092
4568
  this.client,
4093
4569
  { productService: this.getProductService() },
4094
- { accessOptions: this.accessOptions }
4570
+ { accessOptions: this.accessOptions, integrator: resolved }
4095
4571
  );
4572
+ this.bookingServices.set(resolved, service);
4096
4573
  }
4097
- return this.bookingService;
4574
+ return service;
4098
4575
  }
4099
4576
  /**
4100
4577
  * Returns the {@link ReviewService} for this install, bound to the shared