@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.js CHANGED
@@ -316,6 +316,20 @@ var SEARCH_BY_ACTIVITY_DATE = "activityDate";
316
316
  function normalizeBookingId(bookingId) {
317
317
  return bookingId.toLowerCase().replace(/-/g, "_");
318
318
  }
319
+ function buildSalesQuery(nodeBody) {
320
+ return `
321
+ query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
322
+ sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
323
+ pageInfo { endCursor hasNextPage }
324
+ edges {
325
+ node {
326
+ ${nodeBody}
327
+ }
328
+ }
329
+ }
330
+ }
331
+ `;
332
+ }
319
333
  var GUEST_IDENTITY_PII_FIELDS = "name country dateOfBirth email isGdpr";
320
334
  var GUEST_CONTACT_PII_FIELDS = `phone
321
335
  postalCode
@@ -409,6 +423,7 @@ function buildBookingQueryFields(fullCustomerAccess) {
409
423
  availabilityTimeId
410
424
  ${fullCustomerAccess ? "bookingPortalUrl" : ""}
411
425
  operatorNotes
426
+ operatorStatus
412
427
  value {
413
428
  total {
414
429
  formatted
@@ -491,67 +506,40 @@ function buildBookingsListingQuery(includeGuests, includePriceBreakdown, fullCus
491
506
  const guestsSection = includeGuests ? buildBookingGuestsFields(fullCustomerAccess) : "";
492
507
  const baseFields = buildBookingQueryFields(fullCustomerAccess);
493
508
  const fields = includePriceBreakdown ? baseFields.replace("value {", `value { ${PRICE_BREAKDOWN_FIELDS}`).replace("ticketQuantities {", `ticketQuantities { ${TICKET_VALUE_FIELDS}`) : baseFields;
494
- return `
495
- query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
496
- sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
497
- pageInfo { endCursor hasNextPage }
498
- edges {
499
- node {
500
- ... on Booking {
501
- ${fields}
502
- ${guestsSection}
503
- }
504
- }
505
- }
506
- }
509
+ return buildSalesQuery(`
510
+ ... on Booking {
511
+ ${fields}
512
+ ${guestsSection}
507
513
  }
508
- `;
514
+ `);
509
515
  }
510
516
  function buildBookingGuestsQuery(fullCustomerAccess) {
511
- return `
512
- query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
513
- sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
514
- pageInfo { endCursor hasNextPage }
515
- edges {
516
- node {
517
- ... on Booking {
518
- displayId
519
- id
520
- ${buildBookingGuestsFields(fullCustomerAccess)}
521
- }
522
- }
523
- }
517
+ return buildSalesQuery(`
518
+ ... on Booking {
519
+ displayId
520
+ id
521
+ ${buildBookingGuestsFields(fullCustomerAccess)}
524
522
  }
525
- }
526
- `;
523
+ `);
527
524
  }
528
- var BOOKING_PAYMENTS_ON_FILE_QUERY = `
529
- query Sales($after: String, $first: Int, $filter: SalesFilter!, $orderBy: SalesOrdering) {
530
- sales(after: $after, first: $first, filter: $filter, orderBy: $orderBy) {
531
- pageInfo { endCursor hasNextPage }
532
- edges {
533
- node {
534
- order {
535
- payments {
536
- id
537
- paymentSource { id }
538
- appliedAt
539
- currentAmount { amount currency }
540
- refundableAmount { amount currency }
541
- }
542
- id
543
- displayId
544
- paymentSources { description type id }
545
- }
546
- ... on Booking {
547
- displayId
548
- id
549
- }
550
- }
551
- }
525
+ var BOOKING_PAYMENTS_ON_FILE_QUERY = buildSalesQuery(`
526
+ order {
527
+ payments {
528
+ id
529
+ paymentSource { id }
530
+ appliedAt
531
+ currentAmount { amount currency }
532
+ refundableAmount { amount currency }
552
533
  }
534
+ id
535
+ displayId
536
+ paymentSources { description type id }
553
537
  }
554
- `;
538
+ ... on Booking {
539
+ displayId
540
+ id
541
+ }
542
+ `);
555
543
  var UPDATE_OPERATOR_NOTES_MUTATION = `
556
544
  mutation Account($input: UpdateOperatorNotesForBookingInput!) {
557
545
  updateOperatorNotesForBooking(input: $input) {
@@ -559,6 +547,13 @@ var UPDATE_OPERATOR_NOTES_MUTATION = `
559
547
  }
560
548
  }
561
549
  `;
550
+ var UPDATE_OPERATOR_STATUS_MUTATION = `
551
+ mutation Account($input: UpdateOperatorStatusForBookingInput!) {
552
+ updateOperatorStatusForBooking(input: $input) {
553
+ booking { operatorStatus }
554
+ }
555
+ }
556
+ `;
562
557
  var UPDATE_BOOKING_CHECKIN_MUTATION = `
563
558
  mutation Account($input: UpdateBookingCheckInInput!) {
564
559
  updateBookingCheckIn(input: $input) {
@@ -781,6 +776,7 @@ function fromBookingNode(node, includeGuests = false, includePriceBreakdown = fa
781
776
  availabilityTimeId: data.availabilityTimeId || null,
782
777
  portalUrl: data.bookingPortalUrl || null,
783
778
  notes: data.operatorNotes || "",
779
+ customStatus: data.operatorStatus || null,
784
780
  valueDisplay: data.value?.total?.formatted || "",
785
781
  valueAmount: data.value?.total?.amount || "",
786
782
  outstandingBalanceAmount: data.balance?.total?.amount || "",
@@ -935,6 +931,251 @@ function fromBookingGuestsResponse(response) {
935
931
  return guests;
936
932
  }
937
933
 
934
+ // src/internal/peek/bookings/metadata-converter.ts
935
+ var UPSERT_SUCCESS_TYPENAME = "UpsertBookingFieldResponsesSuccess";
936
+ function fromBookingMetaDataNode(node) {
937
+ return (node?.fieldResponses ?? []).map(fromFieldResponseNode);
938
+ }
939
+ function fromFieldResponseNode(response) {
940
+ const field = response.fieldLocation?.field;
941
+ const prompt = response.fieldLocation?.prompt;
942
+ return {
943
+ id: field?.id || "",
944
+ name: field?.name || "",
945
+ slug: field?.slug || "",
946
+ type: field?.type || "",
947
+ prompt: prompt?.label ?? null,
948
+ promptHint: prompt?.hint ?? null,
949
+ isRequired: Boolean(prompt?.isRequired),
950
+ refid: response.refid || "",
951
+ value: fromValueNode(response.value)
952
+ };
953
+ }
954
+ function fromValueNode(value) {
955
+ if (!value || !value.__typename) return null;
956
+ switch (value.__typename) {
957
+ case "AgeFieldResponseValue": {
958
+ const age = value.age ?? 0;
959
+ return { kind: "age", age, displayValue: String(age) };
960
+ }
961
+ case "AttachmentFieldResponseValue": {
962
+ const attachmentUrl = value.attachmentUrl ?? "";
963
+ const name = value.name ?? null;
964
+ return {
965
+ kind: "attachment",
966
+ attachmentUrl,
967
+ mime: value.mime ?? null,
968
+ name,
969
+ size: value.size ?? null,
970
+ displayValue: name || attachmentUrl
971
+ };
972
+ }
973
+ case "BarcodeFieldResponseValue": {
974
+ const barcodeValue = value.barcodeValue ?? "";
975
+ return {
976
+ kind: "barcode",
977
+ barcodeType: value.barcodeType ?? null,
978
+ barcodeValue,
979
+ displayValue: barcodeValue
980
+ };
981
+ }
982
+ case "BooleanFieldResponseValue": {
983
+ const bool = Boolean(value.boolean);
984
+ return { kind: "boolean", boolean: bool, displayValue: yesNo(bool) };
985
+ }
986
+ case "CheckboxFieldResponseValue": {
987
+ const isChecked = Boolean(value.isChecked);
988
+ return { kind: "checkbox", isChecked, displayValue: yesNo(isChecked) };
989
+ }
990
+ case "CurrencyFieldResponseValue": {
991
+ const currency = value.currency ?? "";
992
+ return { kind: "currency", currency, displayValue: currency };
993
+ }
994
+ case "DateFieldResponseValue": {
995
+ const date = value.date ?? "";
996
+ return { kind: "date", date, displayValue: date };
997
+ }
998
+ case "DecimalFieldResponseValue": {
999
+ const decimal = value.decimal ?? null;
1000
+ return { kind: "decimal", decimal, displayValue: decimal ?? "" };
1001
+ }
1002
+ case "DurationFieldResponseValue": {
1003
+ const amount = value.duration?.amount ?? 0;
1004
+ const unit = value.duration?.unit ?? null;
1005
+ return { kind: "duration", amount, unit, displayValue: amountUnit(amount, unit) };
1006
+ }
1007
+ case "EmailFieldResponseValue": {
1008
+ const email = value.email ?? null;
1009
+ return { kind: "email", email, displayValue: email ?? "" };
1010
+ }
1011
+ case "GuestFieldResponseValue": {
1012
+ const name = value.name ?? null;
1013
+ return {
1014
+ kind: "guest",
1015
+ name,
1016
+ email: value.email ?? null,
1017
+ dateOfBirth: value.dateOfBirth ?? null,
1018
+ notes: value.notes ?? null,
1019
+ waiverSigned: value.waiverSigned ?? null,
1020
+ displayValue: name ?? ""
1021
+ };
1022
+ }
1023
+ case "HtmlFieldResponseValue": {
1024
+ const html = value.html ?? "";
1025
+ return { kind: "html", html, displayValue: escapeHtml(html) };
1026
+ }
1027
+ case "ImageUrlFieldResponseValue": {
1028
+ const imageUrl = value.imageUrl ?? "";
1029
+ return { kind: "imageUrl", imageUrl, displayValue: imageUrl };
1030
+ }
1031
+ case "IntegerFieldResponseValue": {
1032
+ const integer = value.integer ?? 0;
1033
+ return { kind: "integer", integer, displayValue: String(integer) };
1034
+ }
1035
+ case "LocationFieldResponseValue": {
1036
+ const streetAddress = value.streetAddress ?? null;
1037
+ const locality = value.locality ?? null;
1038
+ const region = value.region ?? null;
1039
+ const postalCode = value.postalCode ?? null;
1040
+ const country = value.country ?? null;
1041
+ return {
1042
+ kind: "location",
1043
+ country,
1044
+ county: value.county ?? null,
1045
+ locality,
1046
+ postalCode,
1047
+ region,
1048
+ streetAddress,
1049
+ displayValue: [streetAddress, locality, region, postalCode, country].filter((part) => part).join(", ")
1050
+ };
1051
+ }
1052
+ case "LongTextFieldResponseValue": {
1053
+ const longText = value.longText ?? "";
1054
+ return { kind: "longText", longText, displayValue: longText };
1055
+ }
1056
+ case "MetaFieldResponseValue": {
1057
+ const meta = value.meta ?? null;
1058
+ return { kind: "meta", meta, displayValue: meta == null ? "" : JSON.stringify(meta) };
1059
+ }
1060
+ case "PercentFieldResponseValue": {
1061
+ const percent = value.percent ?? null;
1062
+ return { kind: "percent", percent, displayValue: percent == null ? "" : `${percent}%` };
1063
+ }
1064
+ case "PhoneFieldResponseValue": {
1065
+ const phone = value.phone ?? "";
1066
+ return { kind: "phone", phone, displayValue: phone };
1067
+ }
1068
+ case "ShortTextFieldResponseValue": {
1069
+ const shortText = value.shortText ?? "";
1070
+ return { kind: "shortText", shortText, displayValue: shortText };
1071
+ }
1072
+ case "TimeFieldResponseValue": {
1073
+ const time = value.time ?? "";
1074
+ return { kind: "time", time, displayValue: time };
1075
+ }
1076
+ case "UrlFieldResponseValue": {
1077
+ const url = value.url ?? null;
1078
+ return { kind: "url", url, displayValue: url ?? "" };
1079
+ }
1080
+ case "VolumeFieldResponseValue": {
1081
+ const amount = value.volume?.amount ?? "0";
1082
+ const unit = value.volume?.unit ?? null;
1083
+ return { kind: "volume", amount, unit, displayValue: amountUnit(amount, unit) };
1084
+ }
1085
+ case "WeightFieldResponseValue": {
1086
+ const amount = value.weight?.amount ?? "0";
1087
+ const unit = value.weight?.unit ?? null;
1088
+ return { kind: "weight", amount, unit, displayValue: amountUnit(amount, unit) };
1089
+ }
1090
+ default:
1091
+ return null;
1092
+ }
1093
+ }
1094
+ function yesNo(value) {
1095
+ return value ? "Yes" : "No";
1096
+ }
1097
+ function amountUnit(amount, unit) {
1098
+ return unit ? `${amount} ${unit.toLowerCase()}` : String(amount);
1099
+ }
1100
+ function escapeHtml(value) {
1101
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1102
+ }
1103
+ function toSetMetaDataResult(node, bookingId) {
1104
+ return {
1105
+ success: node?.__typename === UPSERT_SUCCESS_TYPENAME,
1106
+ bookingId: node?.bookingId || bookingId,
1107
+ message: node?.message || ""
1108
+ };
1109
+ }
1110
+ function filterMetaDataByIntegrator(metaData, integrator) {
1111
+ const prefix = `integrator:${integrator}:`;
1112
+ return metaData.filter((entry) => entry.slug.startsWith(prefix)).map((entry) => ({ ...entry, slug: entry.slug.slice(prefix.length) }));
1113
+ }
1114
+
1115
+ // src/internal/peek/bookings/metadata-queries.ts
1116
+ var GUEST_VALUE_PII_FIELDS = "dateOfBirth\n email\n name\n ";
1117
+ function buildGuestValueFields(fullCustomerAccess) {
1118
+ return `... on GuestFieldResponseValue {
1119
+ ${fullCustomerAccess ? GUEST_VALUE_PII_FIELDS : ""}notes
1120
+ waiverSigned
1121
+ }`;
1122
+ }
1123
+ function buildMetaDataValueSelection(fullCustomerAccess) {
1124
+ return `value {
1125
+ __typename
1126
+ ... on AgeFieldResponseValue { age }
1127
+ ... on AttachmentFieldResponseValue { attachmentUrl mime name size }
1128
+ ... on BarcodeFieldResponseValue { barcodeType barcodeValue }
1129
+ ... on BooleanFieldResponseValue { boolean }
1130
+ ... on CheckboxFieldResponseValue { isChecked }
1131
+ ... on CurrencyFieldResponseValue { currency }
1132
+ ... on DateFieldResponseValue { date }
1133
+ ... on DecimalFieldResponseValue { decimal }
1134
+ ... on DurationFieldResponseValue { duration { amount unit } }
1135
+ ... on EmailFieldResponseValue { email }
1136
+ ${buildGuestValueFields(fullCustomerAccess)}
1137
+ ... on HtmlFieldResponseValue { html }
1138
+ ... on ImageUrlFieldResponseValue { imageUrl }
1139
+ ... on IntegerFieldResponseValue { integer }
1140
+ ... on LocationFieldResponseValue { country county locality postalCode region streetAddress }
1141
+ ... on LongTextFieldResponseValue { longText }
1142
+ ... on MetaFieldResponseValue { meta }
1143
+ ... on PercentFieldResponseValue { percent }
1144
+ ... on PhoneFieldResponseValue { phone }
1145
+ ... on ShortTextFieldResponseValue { shortText }
1146
+ ... on TimeFieldResponseValue { time }
1147
+ ... on UrlFieldResponseValue { url }
1148
+ ... on VolumeFieldResponseValue { volume { amount unit } }
1149
+ ... on WeightFieldResponseValue { weight { amount unit } }
1150
+ }`;
1151
+ }
1152
+ function buildBookingMetaDataQuery(fullCustomerAccess) {
1153
+ return buildSalesQuery(`
1154
+ ... on Booking {
1155
+ displayId
1156
+ id
1157
+ fieldResponses {
1158
+ fieldLocation {
1159
+ field { id name slug type }
1160
+ prompt { label hint isRequired }
1161
+ }
1162
+ refid
1163
+ ${buildMetaDataValueSelection(fullCustomerAccess)}
1164
+ }
1165
+ }
1166
+ `);
1167
+ }
1168
+ var UPSERT_BOOKING_FIELD_RESPONSES_MUTATION = `
1169
+ mutation UpsertBookingFieldResponses($input: UpsertBookingFieldResponsesInput!) {
1170
+ upsertBookingFieldResponses(input: $input) {
1171
+ __typename
1172
+ ... on UpsertBookingFieldResponsesSuccess { bookingId message }
1173
+ ... on BookingNotFoundError { message bookingId }
1174
+ ... on GenericError { message }
1175
+ }
1176
+ }
1177
+ `;
1178
+
938
1179
  // src/internal/peek/bookings/payments-on-file-converter.ts
939
1180
  function fromPaymentsOnFileResponse(response, bookingId) {
940
1181
  const firstEdge = (response?.sales?.edges ?? [])[0];
@@ -1194,10 +1435,41 @@ function assertRequiredAnswered(resolved, questions) {
1194
1435
  }
1195
1436
  }
1196
1437
 
1438
+ // src/internal/peek/bookings/list-price.ts
1439
+ var ERROR_LIST_PRICE_INVALID = "listPrice must be a positive number string, e.g. '100', '1,000', or '10.00'";
1440
+ function parseListPriceToCents(listPrice) {
1441
+ if (typeof listPrice !== "string") {
1442
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1443
+ }
1444
+ const cleaned = listPrice.replace(/,/g, "").trim();
1445
+ if (!/^\d+(\.\d+)?$/.test(cleaned)) {
1446
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1447
+ }
1448
+ const cents = Math.round(Number(cleaned) * 100);
1449
+ if (!Number.isFinite(cents) || cents <= 0) {
1450
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1451
+ }
1452
+ return cents;
1453
+ }
1454
+ function centsToAmount(cents) {
1455
+ return (cents / 100).toFixed(2);
1456
+ }
1457
+ function distributeListPrice(listPrice, ticketCount) {
1458
+ const totalCents = parseListPriceToCents(listPrice);
1459
+ const perTicketCents = Math.floor(totalCents / ticketCount);
1460
+ const amounts = [];
1461
+ for (let i = 0; i < ticketCount - 1; i += 1) {
1462
+ amounts.push(centsToAmount(perTicketCents));
1463
+ }
1464
+ amounts.push(centsToAmount(totalCents - perTicketCents * (ticketCount - 1)));
1465
+ return amounts;
1466
+ }
1467
+
1197
1468
  // src/internal/peek/bookings/booking-service.ts
1198
1469
  var DEFAULT_PAGE_SIZE2 = 50;
1199
1470
  var DEFAULT_CANCEL_NOTE = "Canceled";
1200
1471
  var DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API";
1472
+ var DEFAULT_CURRENCY = "USD";
1201
1473
  var BOOKING_DB_ID_REGEX = /^b_[a-z0-9]+$/;
1202
1474
  var BOOKING_DISPLAY_ID_REGEX = /^B-[A-Z0-9]+$/;
1203
1475
  var ORDER_DB_ID_REGEX = /^o_[a-z0-9]+$/;
@@ -1210,6 +1482,17 @@ var ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required";
1210
1482
  var ERROR_QUANTITY_INVALID = "quantity must be a positive integer string";
1211
1483
  var ERROR_INVALID_BOOKING_ID = "bookingId is required and must be a valid booking id, e.g. 'b_abc123' or 'B-ABC123'";
1212
1484
  var ERROR_INVALID_ORDER_ID = "orderId is required and must be a valid order id, e.g. 'o_abc123' or 'O-ABC123'";
1485
+ var ERROR_CUSTOM_STATUS_REQUIRED = "customStatus is required and must be a non-empty string";
1486
+ var ERROR_GUESTS_REQUIRED = "at least one guest is required";
1487
+ var ERROR_ATTACHMENTS_REQUIRED = "at least one attachment is required";
1488
+ var META_FIELD_GUEST = "guest";
1489
+ var META_FIELD_ATTACHMENT = "attachment";
1490
+ var META_FIELD_MANIFEST_URL = "manifest_url";
1491
+ var META_FIELD_RESERVATION_ID = "reservation_id";
1492
+ var META_FIELD_BOOKING_STATUS = "booking_status";
1493
+ var META_FIELD_ASSIGNED_PRODUCT = "assigned_product";
1494
+ var META_FIELD_ASSIGNED_EMPLOYEE = "assigned_employee";
1495
+ var META_FIELD_INSURANCE_PURCHASED = "insurance_purchased";
1213
1496
  var ERROR_BOOKING_NOT_FOUND = "Booking not found";
1214
1497
  var ERROR_MULTIPLE_BOOKINGS_FOUND = "Expected exactly one booking for the provided bookingId";
1215
1498
  var ERROR_NO_ADDON_OPTION_TO_REMOVE = "No confirmed add-on option matching addonOptionId was found on the booking";
@@ -1222,12 +1505,15 @@ var BookingService = class {
1222
1505
  this.deps = deps;
1223
1506
  this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE2;
1224
1507
  this.fullCustomerAccess = resolveAccessOptions(options.accessOptions).fullCustomerAccess;
1508
+ this.integrator = options.integrator ?? "";
1225
1509
  }
1226
1510
  client;
1227
1511
  deps;
1228
1512
  pageSize;
1229
1513
  /** Whether customer PII is requested and payment operations are allowed. */
1230
1514
  fullCustomerAccess;
1515
+ /** Integrator id scoping `getMetaData` results. */
1516
+ integrator;
1231
1517
  /**
1232
1518
  * Throws a {@link PiiAccessDisabledError} for `operation` when this service was
1233
1519
  * created without `fullCustomerAccess` — used to gate payment and booking-modification
@@ -1315,6 +1601,131 @@ var BookingService = class {
1315
1601
  );
1316
1602
  return fromBookingGuestsResponse(body.data);
1317
1603
  }
1604
+ /**
1605
+ * Returns a booking's custom-field metadata (its `fieldResponses`), scoped to
1606
+ * this service's integrator. Only fields whose slug starts with
1607
+ * `integrator:<integrator>:` are returned, and that prefix is stripped from
1608
+ * each returned `slug`. Returns `[]` when the booking is not found or has no
1609
+ * matching fields.
1610
+ *
1611
+ * @example
1612
+ * ```ts
1613
+ * const meta = await peek.getBookingService().getMetaData("b_abc123");
1614
+ * for (const field of meta) {
1615
+ * if (field.value?.kind === "url") console.log(field.slug, field.value.url);
1616
+ * }
1617
+ * ```
1618
+ */
1619
+ async getMetaData(bookingId) {
1620
+ assertBookingId(bookingId);
1621
+ const body = await this.client.request(
1622
+ SALES_ENDPOINT,
1623
+ buildBookingMetaDataQuery(this.fullCustomerAccess),
1624
+ buildBookingsVariables({
1625
+ pageSize: this.pageSize,
1626
+ after: null,
1627
+ bookingId: normalizeBookingId(bookingId)
1628
+ })
1629
+ );
1630
+ const firstEdge = (body.data?.sales?.edges ?? [])[0];
1631
+ if (!firstEdge) {
1632
+ return [];
1633
+ }
1634
+ return filterMetaDataByIntegrator(fromBookingMetaDataNode(firstEdge.node), this.integrator);
1635
+ }
1636
+ /**
1637
+ * Sets the booking's `guest` metadata field. Each guest requires a `name` and
1638
+ * `email`; `dateOfBirth`/`waiverSigned`/`notes` are optional.
1639
+ */
1640
+ async setMetaDataGuest(bookingId, guests) {
1641
+ if (!Array.isArray(guests) || guests.length === 0) {
1642
+ throw new Error(ERROR_GUESTS_REQUIRED);
1643
+ }
1644
+ const built = guests.map((guest) => {
1645
+ assertNonEmptyString(guest?.name, "guest name");
1646
+ assertNonEmptyString(guest?.email, "guest email");
1647
+ const out = { name: guest.name, email: guest.email };
1648
+ if (guest.dateOfBirth !== void 0) out.dateOfBirth = guest.dateOfBirth;
1649
+ if (guest.waiverSigned !== void 0) out.waiverSigned = guest.waiverSigned;
1650
+ if (guest.notes !== void 0) out.notes = guest.notes;
1651
+ return out;
1652
+ });
1653
+ return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_GUEST, guests: built });
1654
+ }
1655
+ /**
1656
+ * Sets the booking's `attachment` metadata field. Each attachment requires a
1657
+ * `name` and `attachmentUrl`.
1658
+ */
1659
+ async setMetaDataAttachment(bookingId, attachments) {
1660
+ if (!Array.isArray(attachments) || attachments.length === 0) {
1661
+ throw new Error(ERROR_ATTACHMENTS_REQUIRED);
1662
+ }
1663
+ const built = attachments.map((attachment) => {
1664
+ assertNonEmptyString(attachment?.name, "attachment name");
1665
+ assertNonEmptyString(attachment?.attachmentUrl, "attachment attachmentUrl");
1666
+ return { name: attachment.name, attachmentUrl: attachment.attachmentUrl };
1667
+ });
1668
+ return this.upsertFieldResponse(bookingId, {
1669
+ fieldName: META_FIELD_ATTACHMENT,
1670
+ attachments: built
1671
+ });
1672
+ }
1673
+ /** Sets the booking's `manifest_url` metadata field. */
1674
+ async setMetaDataManifestUrl(bookingId, url) {
1675
+ assertNonEmptyString(url, "url");
1676
+ return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_MANIFEST_URL, url });
1677
+ }
1678
+ /** Sets the booking's `reservation_id` metadata field. */
1679
+ async setMetaDataReservationId(bookingId, reservationId) {
1680
+ return this.setShortTextField(bookingId, META_FIELD_RESERVATION_ID, reservationId, "reservationId");
1681
+ }
1682
+ /** Sets the booking's `booking_status` metadata field. */
1683
+ async setMetaDataBookingStatus(bookingId, status) {
1684
+ return this.setShortTextField(bookingId, META_FIELD_BOOKING_STATUS, status, "status");
1685
+ }
1686
+ /** Sets the booking's `assigned_product` metadata field. */
1687
+ async setMetaDataAssignedProduct(bookingId, productName) {
1688
+ return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_PRODUCT, productName, "productName");
1689
+ }
1690
+ /** Sets the booking's `assigned_employee` metadata field. */
1691
+ async setMetaDataAssignedEmployee(bookingId, employeeName) {
1692
+ return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_EMPLOYEE, employeeName, "employeeName");
1693
+ }
1694
+ /** Sets the booking's `insurance_purchased` metadata field. */
1695
+ async setMetaDataInsurancePurchased(bookingId, insurancePurchased) {
1696
+ if (typeof insurancePurchased !== "boolean") {
1697
+ throw new Error("insurancePurchased is required and must be a boolean");
1698
+ }
1699
+ return this.upsertFieldResponse(bookingId, {
1700
+ fieldName: META_FIELD_INSURANCE_PURCHASED,
1701
+ boolean: insurancePurchased
1702
+ });
1703
+ }
1704
+ /** Validates a non-empty `shortText` value and upserts it under `fieldName`. */
1705
+ setShortTextField(bookingId, fieldName, value, argName) {
1706
+ assertNonEmptyString(value, argName);
1707
+ return this.upsertFieldResponse(bookingId, { fieldName, shortText: value });
1708
+ }
1709
+ /**
1710
+ * Validates the booking id and upserts a single field response (scoped to this
1711
+ * service's integrator), mapping the result union into a {@link SetMetaDataResult}.
1712
+ */
1713
+ async upsertFieldResponse(bookingId, fieldResponse) {
1714
+ assertBookingId(bookingId);
1715
+ const normalized = normalizeBookingId(bookingId);
1716
+ const body = await this.client.request(
1717
+ SALES_ENDPOINT,
1718
+ UPSERT_BOOKING_FIELD_RESPONSES_MUTATION,
1719
+ {
1720
+ input: {
1721
+ bookingId: normalized,
1722
+ integrator: this.integrator,
1723
+ fieldResponses: [fieldResponse]
1724
+ }
1725
+ }
1726
+ );
1727
+ return toSetMetaDataResult(body.data?.upsertBookingFieldResponses, normalized);
1728
+ }
1318
1729
  /** Returns the payments on file for a booking, or null when not found. */
1319
1730
  async getPaymentsOnFile(bookingId) {
1320
1731
  this.assertPiiEnabled("getPaymentsOnFile");
@@ -1359,6 +1770,30 @@ ${note}`;
1359
1770
  });
1360
1771
  return this.getById(normalized);
1361
1772
  }
1773
+ /**
1774
+ * Sets a booking's operator-facing custom status. Returns `true` when the
1775
+ * gateway confirms the stored `operatorStatus` equals the requested
1776
+ * `customStatus`, `false` otherwise.
1777
+ *
1778
+ * @example
1779
+ * ```ts
1780
+ * const ok = await peek.getBookingService().updateCustomStatus("b_abc123", "Awaiting deposit");
1781
+ * ```
1782
+ *
1783
+ * @throws {Error} when `bookingId` is not a valid booking id (`b_…`/`B-…`) or
1784
+ * when `customStatus` is not a non-empty string.
1785
+ */
1786
+ async updateCustomStatus(bookingId, customStatus) {
1787
+ assertBookingId(bookingId);
1788
+ assertCustomStatus(customStatus);
1789
+ const normalized = normalizeBookingId(bookingId);
1790
+ const body = await this.client.request(
1791
+ SALES_ENDPOINT,
1792
+ UPDATE_OPERATOR_STATUS_MUTATION,
1793
+ { input: { id: normalized, operatorStatus: customStatus } }
1794
+ );
1795
+ return body.data?.updateOperatorStatusForBooking?.booking?.operatorStatus === customStatus;
1796
+ }
1362
1797
  /** Cancels a booking and returns its id/displayId/status. */
1363
1798
  async cancel(bookingId, notes = DEFAULT_CANCEL_NOTE) {
1364
1799
  assertBookingId(bookingId);
@@ -1714,13 +2149,26 @@ ${note}`;
1714
2149
  */
1715
2150
  async create(input) {
1716
2151
  validateCreateInput(input);
1717
- const questionAnswers = await this.buildQuestionAnswers(input);
2152
+ const ticketCount = input.tickets.reduce((count, ticket) => count + ticket.quantity, 0);
2153
+ const listPriceAmounts = input.listPrice === void 0 ? null : distributeListPrice(input.listPrice, ticketCount);
2154
+ const [questionAnswers, currency] = await Promise.all([
2155
+ this.buildQuestionAnswers(input),
2156
+ listPriceAmounts ? this.resolveActivityCurrency(input.activityId) : Promise.resolve("")
2157
+ ]);
2158
+ let seatIndex = 0;
1718
2159
  const tickets = input.tickets.flatMap(
1719
- (ticket) => Array.from({ length: ticket.quantity }, () => ({
1720
- resourceOptionId: ticket.resourceOptionId,
1721
- reservationStatus: "CONFIRMED",
1722
- refid: randomUUID()
1723
- }))
2160
+ (ticket) => Array.from({ length: ticket.quantity }, () => {
2161
+ const seat = {
2162
+ resourceOptionId: ticket.resourceOptionId,
2163
+ reservationStatus: "CONFIRMED",
2164
+ refid: randomUUID()
2165
+ };
2166
+ if (listPriceAmounts) {
2167
+ seat.price = { amount: listPriceAmounts[seatIndex], currency };
2168
+ }
2169
+ seatIndex += 1;
2170
+ return seat;
2171
+ })
1724
2172
  );
1725
2173
  const bookingQuote = {
1726
2174
  tickets,
@@ -1835,6 +2283,15 @@ ${note}`;
1835
2283
  }
1836
2284
  return result.transactionId;
1837
2285
  }
2286
+ /**
2287
+ * Resolves an activity's ISO currency via the product service, falling back to
2288
+ * {@link DEFAULT_CURRENCY} when the activity (or its currency) can't be found.
2289
+ */
2290
+ async resolveActivityCurrency(activityId) {
2291
+ const products = await this.deps.productService.getAllProducts();
2292
+ const match = products.find((product) => product.productId === activityId);
2293
+ return match?.currency || DEFAULT_CURRENCY;
2294
+ }
1838
2295
  /** Finds the parent item id of an add-on by matching its option id. */
1839
2296
  async resolveParentItemId(addonOptionId) {
1840
2297
  const products = await this.deps.productService.getAllProducts();
@@ -1909,6 +2366,16 @@ function assertBookingId(bookingId) {
1909
2366
  throw new Error(ERROR_INVALID_BOOKING_ID);
1910
2367
  }
1911
2368
  }
2369
+ function assertCustomStatus(customStatus) {
2370
+ if (typeof customStatus !== "string" || customStatus.trim().length === 0) {
2371
+ throw new Error(ERROR_CUSTOM_STATUS_REQUIRED);
2372
+ }
2373
+ }
2374
+ function assertNonEmptyString(value, field) {
2375
+ if (typeof value !== "string" || value.trim().length === 0) {
2376
+ throw new Error(`${field} is required and must be a non-empty string`);
2377
+ }
2378
+ }
1912
2379
  function assertOrderId(orderId) {
1913
2380
  if (!(ORDER_DB_ID_REGEX.test(orderId) || ORDER_DISPLAY_ID_REGEX.test(orderId))) {
1914
2381
  throw new Error(ERROR_INVALID_ORDER_ID);
@@ -2175,7 +2642,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
2175
2642
  // src/internal/sdk-headers.ts
2176
2643
  var SDK_HEADER_NAME = "x-peek-sdk";
2177
2644
  var SDK_PLATFORM = "js";
2178
- var SDK_VERSION = "0.9.2";
2645
+ var SDK_VERSION = "0.9.3";
2179
2646
  var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
2180
2647
 
2181
2648
  // src/internal/peek/graphql-client.ts
@@ -3461,7 +3928,7 @@ var CREATE_PROMO_CODE_MUTATION = `
3461
3928
 
3462
3929
  // src/internal/peek/promo-codes/promo-code-service.ts
3463
3930
  var DEFAULT_PAGE_SIZE3 = 50;
3464
- var DEFAULT_CURRENCY = "USD";
3931
+ var DEFAULT_CURRENCY2 = "USD";
3465
3932
  var CURRENCY_PATTERN = /^[A-Z]{3}$/;
3466
3933
  var PromoCodeService = class {
3467
3934
  constructor(client, options = {}) {
@@ -3507,7 +3974,7 @@ var PromoCodeService = class {
3507
3974
  if (Number.isNaN(parseFloat(input.amount))) {
3508
3975
  throw new Error("amount must be a valid number");
3509
3976
  }
3510
- const currency = input.currency || DEFAULT_CURRENCY;
3977
+ const currency = input.currency || DEFAULT_CURRENCY2;
3511
3978
  if (!CURRENCY_PATTERN.test(currency)) {
3512
3979
  throw new Error("currency must be 3 uppercase letters");
3513
3980
  }
@@ -3881,6 +4348,7 @@ var PeekAccessService = class {
3881
4348
  productServiceOptions;
3882
4349
  accessOptions;
3883
4350
  jwtSecret;
4351
+ issuer;
3884
4352
  productService;
3885
4353
  accountUserService;
3886
4354
  resourcePoolService;
@@ -3891,7 +4359,7 @@ var PeekAccessService = class {
3891
4359
  dailyNoteService;
3892
4360
  availabilityService;
3893
4361
  membershipService;
3894
- bookingService;
4362
+ bookingServices = /* @__PURE__ */ new Map();
3895
4363
  reviewService;
3896
4364
  constructor(config) {
3897
4365
  const hasApiUrl = !!config.apiUrl;
@@ -3902,6 +4370,7 @@ var PeekAccessService = class {
3902
4370
  if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
3903
4371
  if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
3904
4372
  this.jwtSecret = config.jwtSecret;
4373
+ this.issuer = config.issuer;
3905
4374
  const logger = config.logger ?? noopLogger;
3906
4375
  const tokens = createTokenManager(config);
3907
4376
  const useRegistrySlug = hasApiUrl || isV2;
@@ -4062,17 +4531,25 @@ var PeekAccessService = class {
4062
4531
  }
4063
4532
  /**
4064
4533
  * Returns the {@link BookingService} for this install, bound to the shared
4065
- * authenticated transport. The instance is created lazily and reused.
4534
+ * authenticated transport. Instances are created lazily and reused per
4535
+ * `integrator`.
4536
+ *
4537
+ * `integrator` scopes `getMetaData` results to a custom integrator id. **Do
4538
+ * not set it** unless Peek engineering created a custom integrator id for this
4539
+ * integration — omit it and the correct default is used.
4066
4540
  */
4067
- getBookingService() {
4068
- if (!this.bookingService) {
4069
- this.bookingService = new BookingService(
4541
+ getBookingService(integrator) {
4542
+ const resolved = integrator ?? this.issuer;
4543
+ let service = this.bookingServices.get(resolved);
4544
+ if (!service) {
4545
+ service = new BookingService(
4070
4546
  this.client,
4071
4547
  { productService: this.getProductService() },
4072
- { accessOptions: this.accessOptions }
4548
+ { accessOptions: this.accessOptions, integrator: resolved }
4073
4549
  );
4550
+ this.bookingServices.set(resolved, service);
4074
4551
  }
4075
- return this.bookingService;
4552
+ return service;
4076
4553
  }
4077
4554
  /**
4078
4555
  * Returns the {@link ReviewService} for this install, bound to the shared