@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.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,261 @@ 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
+ const { value, displayValue } = fromValueNode(response.value);
943
+ return {
944
+ id: field?.id || "",
945
+ name: field?.name || "",
946
+ slug: field?.slug || "",
947
+ type: field?.type || "",
948
+ prompt: prompt?.label ?? null,
949
+ promptHint: prompt?.hint ?? null,
950
+ isRequired: Boolean(prompt?.isRequired),
951
+ refid: response.refid || "",
952
+ displayValue,
953
+ value
954
+ };
955
+ }
956
+ function fromValueNode(value) {
957
+ if (!value || !value.__typename) return { value: null, displayValue: "" };
958
+ switch (value.__typename) {
959
+ case "AgeFieldResponseValue": {
960
+ const age = value.age ?? 0;
961
+ return { value: { kind: "age", age }, displayValue: String(age) };
962
+ }
963
+ case "AttachmentFieldResponseValue": {
964
+ const attachmentUrl = value.attachmentUrl ?? "";
965
+ const name = value.name ?? null;
966
+ return {
967
+ value: {
968
+ kind: "attachment",
969
+ attachmentUrl,
970
+ mime: value.mime ?? null,
971
+ name,
972
+ size: value.size ?? null
973
+ },
974
+ displayValue: name || attachmentUrl
975
+ };
976
+ }
977
+ case "BarcodeFieldResponseValue": {
978
+ const barcodeValue = value.barcodeValue ?? "";
979
+ return {
980
+ value: {
981
+ kind: "barcode",
982
+ barcodeType: value.barcodeType ?? null,
983
+ barcodeValue
984
+ },
985
+ displayValue: barcodeValue
986
+ };
987
+ }
988
+ case "BooleanFieldResponseValue": {
989
+ const bool = Boolean(value.boolean);
990
+ return { value: { kind: "boolean", boolean: bool }, displayValue: yesNo(bool) };
991
+ }
992
+ case "CheckboxFieldResponseValue": {
993
+ const isChecked = Boolean(value.isChecked);
994
+ return { value: { kind: "checkbox", isChecked }, displayValue: yesNo(isChecked) };
995
+ }
996
+ case "CurrencyFieldResponseValue": {
997
+ const currency = value.currency ?? "";
998
+ return { value: { kind: "currency", currency }, displayValue: currency };
999
+ }
1000
+ case "DateFieldResponseValue": {
1001
+ const date = value.date ?? "";
1002
+ return { value: { kind: "date", date }, displayValue: date };
1003
+ }
1004
+ case "DecimalFieldResponseValue": {
1005
+ const decimal = value.decimal ?? null;
1006
+ return { value: { kind: "decimal", decimal }, displayValue: decimal ?? "" };
1007
+ }
1008
+ case "DurationFieldResponseValue": {
1009
+ const amount = value.duration?.amount ?? 0;
1010
+ const unit = value.duration?.unit ?? null;
1011
+ return { value: { kind: "duration", amount, unit }, displayValue: amountUnit(amount, unit) };
1012
+ }
1013
+ case "EmailFieldResponseValue": {
1014
+ const email = value.email ?? null;
1015
+ return { value: { kind: "email", email }, displayValue: email ?? "" };
1016
+ }
1017
+ case "GuestFieldResponseValue": {
1018
+ const name = value.name ?? null;
1019
+ return {
1020
+ value: {
1021
+ kind: "guest",
1022
+ name,
1023
+ email: value.email ?? null,
1024
+ dateOfBirth: value.dateOfBirth ?? null,
1025
+ notes: value.notes ?? null,
1026
+ waiverSigned: value.waiverSigned ?? null
1027
+ },
1028
+ displayValue: name ?? ""
1029
+ };
1030
+ }
1031
+ case "HtmlFieldResponseValue": {
1032
+ const html = value.html ?? "";
1033
+ return { value: { kind: "html", html }, displayValue: escapeHtml(html) };
1034
+ }
1035
+ case "ImageUrlFieldResponseValue": {
1036
+ const imageUrl = value.imageUrl ?? "";
1037
+ return { value: { kind: "imageUrl", imageUrl }, displayValue: imageUrl };
1038
+ }
1039
+ case "IntegerFieldResponseValue": {
1040
+ const integer = value.integer ?? 0;
1041
+ return { value: { kind: "integer", integer }, displayValue: String(integer) };
1042
+ }
1043
+ case "LocationFieldResponseValue": {
1044
+ const streetAddress = value.streetAddress ?? null;
1045
+ const locality = value.locality ?? null;
1046
+ const region = value.region ?? null;
1047
+ const postalCode = value.postalCode ?? null;
1048
+ const country = value.country ?? null;
1049
+ return {
1050
+ value: {
1051
+ kind: "location",
1052
+ country,
1053
+ county: value.county ?? null,
1054
+ locality,
1055
+ postalCode,
1056
+ region,
1057
+ streetAddress
1058
+ },
1059
+ displayValue: [streetAddress, locality, region, postalCode, country].filter((part) => part).join(", ")
1060
+ };
1061
+ }
1062
+ case "LongTextFieldResponseValue": {
1063
+ const longText = value.longText ?? "";
1064
+ return { value: { kind: "longText", longText }, displayValue: longText };
1065
+ }
1066
+ case "MetaFieldResponseValue": {
1067
+ const meta = value.meta ?? null;
1068
+ return { value: { kind: "meta", meta }, displayValue: meta == null ? "" : JSON.stringify(meta) };
1069
+ }
1070
+ case "PercentFieldResponseValue": {
1071
+ const percent = value.percent ?? null;
1072
+ return { value: { kind: "percent", percent }, displayValue: percent == null ? "" : `${percent}%` };
1073
+ }
1074
+ case "PhoneFieldResponseValue": {
1075
+ const phone = value.phone ?? "";
1076
+ return { value: { kind: "phone", phone }, displayValue: phone };
1077
+ }
1078
+ case "ShortTextFieldResponseValue": {
1079
+ const shortText = value.shortText ?? "";
1080
+ return { value: { kind: "shortText", shortText }, displayValue: shortText };
1081
+ }
1082
+ case "TimeFieldResponseValue": {
1083
+ const time = value.time ?? "";
1084
+ return { value: { kind: "time", time }, displayValue: time };
1085
+ }
1086
+ case "UrlFieldResponseValue": {
1087
+ const url = value.url ?? null;
1088
+ return { value: { kind: "url", url }, displayValue: url ?? "" };
1089
+ }
1090
+ case "VolumeFieldResponseValue": {
1091
+ const amount = value.volume?.amount ?? "0";
1092
+ const unit = value.volume?.unit ?? null;
1093
+ return { value: { kind: "volume", amount, unit }, displayValue: amountUnit(amount, unit) };
1094
+ }
1095
+ case "WeightFieldResponseValue": {
1096
+ const amount = value.weight?.amount ?? "0";
1097
+ const unit = value.weight?.unit ?? null;
1098
+ return { value: { kind: "weight", amount, unit }, displayValue: amountUnit(amount, unit) };
1099
+ }
1100
+ default:
1101
+ return { value: null, displayValue: "" };
1102
+ }
1103
+ }
1104
+ function yesNo(value) {
1105
+ return value ? "Yes" : "No";
1106
+ }
1107
+ function amountUnit(amount, unit) {
1108
+ return unit ? `${amount} ${unit.toLowerCase()}` : String(amount);
1109
+ }
1110
+ function escapeHtml(value) {
1111
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1112
+ }
1113
+ function toSetMetaDataResult(node, bookingId) {
1114
+ return {
1115
+ success: node?.__typename === UPSERT_SUCCESS_TYPENAME,
1116
+ bookingId: node?.bookingId || bookingId,
1117
+ message: node?.message || ""
1118
+ };
1119
+ }
1120
+ function filterMetaDataByIntegrator(metaData, integrator) {
1121
+ const prefix = `integrators:${integrator}:`;
1122
+ return metaData.filter((entry) => entry.slug.startsWith(prefix)).map((entry) => ({ ...entry, slug: entry.slug.slice(prefix.length) }));
1123
+ }
1124
+
1125
+ // src/internal/peek/bookings/metadata-queries.ts
1126
+ var GUEST_VALUE_PII_FIELDS = "dateOfBirth\n email\n name\n ";
1127
+ function buildGuestValueFields(fullCustomerAccess) {
1128
+ return `... on GuestFieldResponseValue {
1129
+ ${fullCustomerAccess ? GUEST_VALUE_PII_FIELDS : ""}notes
1130
+ waiverSigned
1131
+ }`;
1132
+ }
1133
+ function buildMetaDataValueSelection(fullCustomerAccess) {
1134
+ return `value {
1135
+ __typename
1136
+ ... on AgeFieldResponseValue { age }
1137
+ ... on AttachmentFieldResponseValue { attachmentUrl mime name size }
1138
+ ... on BarcodeFieldResponseValue { barcodeType barcodeValue }
1139
+ ... on BooleanFieldResponseValue { boolean }
1140
+ ... on CheckboxFieldResponseValue { isChecked }
1141
+ ... on CurrencyFieldResponseValue { currency }
1142
+ ... on DateFieldResponseValue { date }
1143
+ ... on DecimalFieldResponseValue { decimal }
1144
+ ... on DurationFieldResponseValue { duration { amount unit } }
1145
+ ... on EmailFieldResponseValue { email }
1146
+ ${buildGuestValueFields(fullCustomerAccess)}
1147
+ ... on HtmlFieldResponseValue { html }
1148
+ ... on ImageUrlFieldResponseValue { imageUrl }
1149
+ ... on IntegerFieldResponseValue { integer }
1150
+ ... on LocationFieldResponseValue { country county locality postalCode region streetAddress }
1151
+ ... on LongTextFieldResponseValue { longText }
1152
+ ... on MetaFieldResponseValue { meta }
1153
+ ... on PercentFieldResponseValue { percent }
1154
+ ... on PhoneFieldResponseValue { phone }
1155
+ ... on ShortTextFieldResponseValue { shortText }
1156
+ ... on TimeFieldResponseValue { time }
1157
+ ... on UrlFieldResponseValue { url }
1158
+ ... on VolumeFieldResponseValue { volume { amount unit } }
1159
+ ... on WeightFieldResponseValue { weight { amount unit } }
1160
+ }`;
1161
+ }
1162
+ function buildBookingMetaDataQuery(fullCustomerAccess) {
1163
+ return buildSalesQuery(`
1164
+ ... on Booking {
1165
+ displayId
1166
+ id
1167
+ fieldResponses {
1168
+ fieldLocation {
1169
+ field { id name slug type }
1170
+ prompt { label hint isRequired }
1171
+ }
1172
+ refid
1173
+ ${buildMetaDataValueSelection(fullCustomerAccess)}
1174
+ }
1175
+ }
1176
+ `);
1177
+ }
1178
+ var UPSERT_BOOKING_FIELD_RESPONSES_MUTATION = `
1179
+ mutation UpsertBookingFieldResponses($input: UpsertBookingFieldResponsesInput!) {
1180
+ upsertBookingFieldResponses(input: $input) {
1181
+ __typename
1182
+ ... on UpsertBookingFieldResponsesSuccess { bookingId message }
1183
+ ... on BookingNotFoundError { message bookingId }
1184
+ ... on GenericError { message }
1185
+ }
1186
+ }
1187
+ `;
1188
+
938
1189
  // src/internal/peek/bookings/payments-on-file-converter.ts
939
1190
  function fromPaymentsOnFileResponse(response, bookingId) {
940
1191
  const firstEdge = (response?.sales?.edges ?? [])[0];
@@ -1194,10 +1445,41 @@ function assertRequiredAnswered(resolved, questions) {
1194
1445
  }
1195
1446
  }
1196
1447
 
1448
+ // src/internal/peek/bookings/list-price.ts
1449
+ var ERROR_LIST_PRICE_INVALID = "listPrice must be a positive number string, e.g. '100', '1,000', or '10.00'";
1450
+ function parseListPriceToCents(listPrice) {
1451
+ if (typeof listPrice !== "string") {
1452
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1453
+ }
1454
+ const cleaned = listPrice.replace(/,/g, "").trim();
1455
+ if (!/^\d+(\.\d+)?$/.test(cleaned)) {
1456
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1457
+ }
1458
+ const cents = Math.round(Number(cleaned) * 100);
1459
+ if (!Number.isFinite(cents) || cents <= 0) {
1460
+ throw new Error(ERROR_LIST_PRICE_INVALID);
1461
+ }
1462
+ return cents;
1463
+ }
1464
+ function centsToAmount(cents) {
1465
+ return (cents / 100).toFixed(2);
1466
+ }
1467
+ function distributeListPrice(listPrice, ticketCount) {
1468
+ const totalCents = parseListPriceToCents(listPrice);
1469
+ const perTicketCents = Math.floor(totalCents / ticketCount);
1470
+ const amounts = [];
1471
+ for (let i = 0; i < ticketCount - 1; i += 1) {
1472
+ amounts.push(centsToAmount(perTicketCents));
1473
+ }
1474
+ amounts.push(centsToAmount(totalCents - perTicketCents * (ticketCount - 1)));
1475
+ return amounts;
1476
+ }
1477
+
1197
1478
  // src/internal/peek/bookings/booking-service.ts
1198
1479
  var DEFAULT_PAGE_SIZE2 = 50;
1199
1480
  var DEFAULT_CANCEL_NOTE = "Canceled";
1200
1481
  var DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API";
1482
+ var DEFAULT_CURRENCY = "USD";
1201
1483
  var BOOKING_DB_ID_REGEX = /^b_[a-z0-9]+$/;
1202
1484
  var BOOKING_DISPLAY_ID_REGEX = /^B-[A-Z0-9]+$/;
1203
1485
  var ORDER_DB_ID_REGEX = /^o_[a-z0-9]+$/;
@@ -1210,6 +1492,17 @@ var ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required";
1210
1492
  var ERROR_QUANTITY_INVALID = "quantity must be a positive integer string";
1211
1493
  var ERROR_INVALID_BOOKING_ID = "bookingId is required and must be a valid booking id, e.g. 'b_abc123' or 'B-ABC123'";
1212
1494
  var ERROR_INVALID_ORDER_ID = "orderId is required and must be a valid order id, e.g. 'o_abc123' or 'O-ABC123'";
1495
+ var ERROR_CUSTOM_STATUS_REQUIRED = "customStatus is required and must be a non-empty string";
1496
+ var ERROR_GUESTS_REQUIRED = "at least one guest is required";
1497
+ var ERROR_ATTACHMENTS_REQUIRED = "at least one attachment is required";
1498
+ var META_FIELD_GUEST = "guest";
1499
+ var META_FIELD_ATTACHMENT = "attachment";
1500
+ var META_FIELD_MANIFEST_URL = "manifest_url";
1501
+ var META_FIELD_RESERVATION_ID = "reservation_id";
1502
+ var META_FIELD_BOOKING_STATUS = "booking_status";
1503
+ var META_FIELD_ASSIGNED_PRODUCT = "assigned_product";
1504
+ var META_FIELD_ASSIGNED_EMPLOYEE = "assigned_employee";
1505
+ var META_FIELD_INSURANCE_PURCHASED = "insurance_purchased";
1213
1506
  var ERROR_BOOKING_NOT_FOUND = "Booking not found";
1214
1507
  var ERROR_MULTIPLE_BOOKINGS_FOUND = "Expected exactly one booking for the provided bookingId";
1215
1508
  var ERROR_NO_ADDON_OPTION_TO_REMOVE = "No confirmed add-on option matching addonOptionId was found on the booking";
@@ -1222,12 +1515,15 @@ var BookingService = class {
1222
1515
  this.deps = deps;
1223
1516
  this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE2;
1224
1517
  this.fullCustomerAccess = resolveAccessOptions(options.accessOptions).fullCustomerAccess;
1518
+ this.integrator = options.integrator ?? "";
1225
1519
  }
1226
1520
  client;
1227
1521
  deps;
1228
1522
  pageSize;
1229
1523
  /** Whether customer PII is requested and payment operations are allowed. */
1230
1524
  fullCustomerAccess;
1525
+ /** Integrator id scoping `getMetaData` results. */
1526
+ integrator;
1231
1527
  /**
1232
1528
  * Throws a {@link PiiAccessDisabledError} for `operation` when this service was
1233
1529
  * created without `fullCustomerAccess` — used to gate payment and booking-modification
@@ -1315,6 +1611,131 @@ var BookingService = class {
1315
1611
  );
1316
1612
  return fromBookingGuestsResponse(body.data);
1317
1613
  }
1614
+ /**
1615
+ * Returns a booking's custom-field metadata (its `fieldResponses`), scoped to
1616
+ * this service's integrator. Only fields whose slug starts with
1617
+ * `integrators:<integrator>:` are returned, and that prefix is stripped from
1618
+ * each returned `slug`. Returns `[]` when the booking is not found or has no
1619
+ * matching fields.
1620
+ *
1621
+ * @example
1622
+ * ```ts
1623
+ * const meta = await peek.getBookingService().getMetaData("b_abc123");
1624
+ * for (const field of meta) {
1625
+ * if (field.value?.kind === "url") console.log(field.slug, field.value.url);
1626
+ * }
1627
+ * ```
1628
+ */
1629
+ async getMetaData(bookingId) {
1630
+ assertBookingId(bookingId);
1631
+ const body = await this.client.request(
1632
+ SALES_ENDPOINT,
1633
+ buildBookingMetaDataQuery(this.fullCustomerAccess),
1634
+ buildBookingsVariables({
1635
+ pageSize: this.pageSize,
1636
+ after: null,
1637
+ bookingId: normalizeBookingId(bookingId)
1638
+ })
1639
+ );
1640
+ const firstEdge = (body.data?.sales?.edges ?? [])[0];
1641
+ if (!firstEdge) {
1642
+ return [];
1643
+ }
1644
+ return filterMetaDataByIntegrator(fromBookingMetaDataNode(firstEdge.node), this.integrator);
1645
+ }
1646
+ /**
1647
+ * Sets the booking's `guest` metadata field. Each guest requires a `name` and
1648
+ * `email`; `dateOfBirth`/`waiverSigned`/`notes` are optional.
1649
+ */
1650
+ async setMetaDataGuest(bookingId, guests) {
1651
+ if (!Array.isArray(guests) || guests.length === 0) {
1652
+ throw new Error(ERROR_GUESTS_REQUIRED);
1653
+ }
1654
+ const built = guests.map((guest) => {
1655
+ assertNonEmptyString(guest?.name, "guest name");
1656
+ assertNonEmptyString(guest?.email, "guest email");
1657
+ const out = { name: guest.name, email: guest.email };
1658
+ if (guest.dateOfBirth !== void 0) out.dateOfBirth = guest.dateOfBirth;
1659
+ if (guest.waiverSigned !== void 0) out.waiverSigned = guest.waiverSigned;
1660
+ if (guest.notes !== void 0) out.notes = guest.notes;
1661
+ return out;
1662
+ });
1663
+ return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_GUEST, guests: built });
1664
+ }
1665
+ /**
1666
+ * Sets the booking's `attachment` metadata field. Each attachment requires a
1667
+ * `name` and `attachmentUrl`.
1668
+ */
1669
+ async setMetaDataAttachment(bookingId, attachments) {
1670
+ if (!Array.isArray(attachments) || attachments.length === 0) {
1671
+ throw new Error(ERROR_ATTACHMENTS_REQUIRED);
1672
+ }
1673
+ const built = attachments.map((attachment) => {
1674
+ assertNonEmptyString(attachment?.name, "attachment name");
1675
+ assertNonEmptyString(attachment?.attachmentUrl, "attachment attachmentUrl");
1676
+ return { name: attachment.name, attachmentUrl: attachment.attachmentUrl };
1677
+ });
1678
+ return this.upsertFieldResponse(bookingId, {
1679
+ fieldName: META_FIELD_ATTACHMENT,
1680
+ attachments: built
1681
+ });
1682
+ }
1683
+ /** Sets the booking's `manifest_url` metadata field. */
1684
+ async setMetaDataManifestUrl(bookingId, url) {
1685
+ assertNonEmptyString(url, "url");
1686
+ return this.upsertFieldResponse(bookingId, { fieldName: META_FIELD_MANIFEST_URL, url });
1687
+ }
1688
+ /** Sets the booking's `reservation_id` metadata field. */
1689
+ async setMetaDataReservationId(bookingId, reservationId) {
1690
+ return this.setShortTextField(bookingId, META_FIELD_RESERVATION_ID, reservationId, "reservationId");
1691
+ }
1692
+ /** Sets the booking's `booking_status` metadata field. */
1693
+ async setMetaDataBookingStatus(bookingId, status) {
1694
+ return this.setShortTextField(bookingId, META_FIELD_BOOKING_STATUS, status, "status");
1695
+ }
1696
+ /** Sets the booking's `assigned_product` metadata field. */
1697
+ async setMetaDataAssignedProduct(bookingId, productName) {
1698
+ return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_PRODUCT, productName, "productName");
1699
+ }
1700
+ /** Sets the booking's `assigned_employee` metadata field. */
1701
+ async setMetaDataAssignedEmployee(bookingId, employeeName) {
1702
+ return this.setShortTextField(bookingId, META_FIELD_ASSIGNED_EMPLOYEE, employeeName, "employeeName");
1703
+ }
1704
+ /** Sets the booking's `insurance_purchased` metadata field. */
1705
+ async setMetaDataInsurancePurchased(bookingId, insurancePurchased) {
1706
+ if (typeof insurancePurchased !== "boolean") {
1707
+ throw new Error("insurancePurchased is required and must be a boolean");
1708
+ }
1709
+ return this.upsertFieldResponse(bookingId, {
1710
+ fieldName: META_FIELD_INSURANCE_PURCHASED,
1711
+ boolean: insurancePurchased
1712
+ });
1713
+ }
1714
+ /** Validates a non-empty `shortText` value and upserts it under `fieldName`. */
1715
+ setShortTextField(bookingId, fieldName, value, argName) {
1716
+ assertNonEmptyString(value, argName);
1717
+ return this.upsertFieldResponse(bookingId, { fieldName, shortText: value });
1718
+ }
1719
+ /**
1720
+ * Validates the booking id and upserts a single field response (scoped to this
1721
+ * service's integrator), mapping the result union into a {@link SetMetaDataResult}.
1722
+ */
1723
+ async upsertFieldResponse(bookingId, fieldResponse) {
1724
+ assertBookingId(bookingId);
1725
+ const normalized = normalizeBookingId(bookingId);
1726
+ const body = await this.client.request(
1727
+ SALES_ENDPOINT,
1728
+ UPSERT_BOOKING_FIELD_RESPONSES_MUTATION,
1729
+ {
1730
+ input: {
1731
+ bookingId: normalized,
1732
+ integrator: this.integrator,
1733
+ fieldResponses: [fieldResponse]
1734
+ }
1735
+ }
1736
+ );
1737
+ return toSetMetaDataResult(body.data?.upsertBookingFieldResponses, normalized);
1738
+ }
1318
1739
  /** Returns the payments on file for a booking, or null when not found. */
1319
1740
  async getPaymentsOnFile(bookingId) {
1320
1741
  this.assertPiiEnabled("getPaymentsOnFile");
@@ -1359,6 +1780,30 @@ ${note}`;
1359
1780
  });
1360
1781
  return this.getById(normalized);
1361
1782
  }
1783
+ /**
1784
+ * Sets a booking's operator-facing custom status. Returns `true` when the
1785
+ * gateway confirms the stored `operatorStatus` equals the requested
1786
+ * `customStatus`, `false` otherwise.
1787
+ *
1788
+ * @example
1789
+ * ```ts
1790
+ * const ok = await peek.getBookingService().updateCustomStatus("b_abc123", "Awaiting deposit");
1791
+ * ```
1792
+ *
1793
+ * @throws {Error} when `bookingId` is not a valid booking id (`b_…`/`B-…`) or
1794
+ * when `customStatus` is not a non-empty string.
1795
+ */
1796
+ async updateCustomStatus(bookingId, customStatus) {
1797
+ assertBookingId(bookingId);
1798
+ assertCustomStatus(customStatus);
1799
+ const normalized = normalizeBookingId(bookingId);
1800
+ const body = await this.client.request(
1801
+ SALES_ENDPOINT,
1802
+ UPDATE_OPERATOR_STATUS_MUTATION,
1803
+ { input: { id: normalized, operatorStatus: customStatus } }
1804
+ );
1805
+ return body.data?.updateOperatorStatusForBooking?.booking?.operatorStatus === customStatus;
1806
+ }
1362
1807
  /** Cancels a booking and returns its id/displayId/status. */
1363
1808
  async cancel(bookingId, notes = DEFAULT_CANCEL_NOTE) {
1364
1809
  assertBookingId(bookingId);
@@ -1714,13 +2159,26 @@ ${note}`;
1714
2159
  */
1715
2160
  async create(input) {
1716
2161
  validateCreateInput(input);
1717
- const questionAnswers = await this.buildQuestionAnswers(input);
2162
+ const ticketCount = input.tickets.reduce((count, ticket) => count + ticket.quantity, 0);
2163
+ const listPriceAmounts = input.listPrice === void 0 ? null : distributeListPrice(input.listPrice, ticketCount);
2164
+ const [questionAnswers, currency] = await Promise.all([
2165
+ this.buildQuestionAnswers(input),
2166
+ listPriceAmounts ? this.resolveActivityCurrency(input.activityId) : Promise.resolve("")
2167
+ ]);
2168
+ let seatIndex = 0;
1718
2169
  const tickets = input.tickets.flatMap(
1719
- (ticket) => Array.from({ length: ticket.quantity }, () => ({
1720
- resourceOptionId: ticket.resourceOptionId,
1721
- reservationStatus: "CONFIRMED",
1722
- refid: randomUUID()
1723
- }))
2170
+ (ticket) => Array.from({ length: ticket.quantity }, () => {
2171
+ const seat = {
2172
+ resourceOptionId: ticket.resourceOptionId,
2173
+ reservationStatus: "CONFIRMED",
2174
+ refid: randomUUID()
2175
+ };
2176
+ if (listPriceAmounts) {
2177
+ seat.price = { amount: listPriceAmounts[seatIndex], currency };
2178
+ }
2179
+ seatIndex += 1;
2180
+ return seat;
2181
+ })
1724
2182
  );
1725
2183
  const bookingQuote = {
1726
2184
  tickets,
@@ -1835,6 +2293,15 @@ ${note}`;
1835
2293
  }
1836
2294
  return result.transactionId;
1837
2295
  }
2296
+ /**
2297
+ * Resolves an activity's ISO currency via the product service, falling back to
2298
+ * {@link DEFAULT_CURRENCY} when the activity (or its currency) can't be found.
2299
+ */
2300
+ async resolveActivityCurrency(activityId) {
2301
+ const products = await this.deps.productService.getAllProducts();
2302
+ const match = products.find((product) => product.productId === activityId);
2303
+ return match?.currency || DEFAULT_CURRENCY;
2304
+ }
1838
2305
  /** Finds the parent item id of an add-on by matching its option id. */
1839
2306
  async resolveParentItemId(addonOptionId) {
1840
2307
  const products = await this.deps.productService.getAllProducts();
@@ -1909,6 +2376,16 @@ function assertBookingId(bookingId) {
1909
2376
  throw new Error(ERROR_INVALID_BOOKING_ID);
1910
2377
  }
1911
2378
  }
2379
+ function assertCustomStatus(customStatus) {
2380
+ if (typeof customStatus !== "string" || customStatus.trim().length === 0) {
2381
+ throw new Error(ERROR_CUSTOM_STATUS_REQUIRED);
2382
+ }
2383
+ }
2384
+ function assertNonEmptyString(value, field) {
2385
+ if (typeof value !== "string" || value.trim().length === 0) {
2386
+ throw new Error(`${field} is required and must be a non-empty string`);
2387
+ }
2388
+ }
1912
2389
  function assertOrderId(orderId) {
1913
2390
  if (!(ORDER_DB_ID_REGEX.test(orderId) || ORDER_DISPLAY_ID_REGEX.test(orderId))) {
1914
2391
  throw new Error(ERROR_INVALID_ORDER_ID);
@@ -2175,7 +2652,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
2175
2652
  // src/internal/sdk-headers.ts
2176
2653
  var SDK_HEADER_NAME = "x-peek-sdk";
2177
2654
  var SDK_PLATFORM = "js";
2178
- var SDK_VERSION = "0.9.2";
2655
+ var SDK_VERSION = "0.9.4";
2179
2656
  var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
2180
2657
 
2181
2658
  // src/internal/peek/graphql-client.ts
@@ -3461,7 +3938,7 @@ var CREATE_PROMO_CODE_MUTATION = `
3461
3938
 
3462
3939
  // src/internal/peek/promo-codes/promo-code-service.ts
3463
3940
  var DEFAULT_PAGE_SIZE3 = 50;
3464
- var DEFAULT_CURRENCY = "USD";
3941
+ var DEFAULT_CURRENCY2 = "USD";
3465
3942
  var CURRENCY_PATTERN = /^[A-Z]{3}$/;
3466
3943
  var PromoCodeService = class {
3467
3944
  constructor(client, options = {}) {
@@ -3507,7 +3984,7 @@ var PromoCodeService = class {
3507
3984
  if (Number.isNaN(parseFloat(input.amount))) {
3508
3985
  throw new Error("amount must be a valid number");
3509
3986
  }
3510
- const currency = input.currency || DEFAULT_CURRENCY;
3987
+ const currency = input.currency || DEFAULT_CURRENCY2;
3511
3988
  if (!CURRENCY_PATTERN.test(currency)) {
3512
3989
  throw new Error("currency must be 3 uppercase letters");
3513
3990
  }
@@ -3881,6 +4358,7 @@ var PeekAccessService = class {
3881
4358
  productServiceOptions;
3882
4359
  accessOptions;
3883
4360
  jwtSecret;
4361
+ issuer;
3884
4362
  productService;
3885
4363
  accountUserService;
3886
4364
  resourcePoolService;
@@ -3891,7 +4369,7 @@ var PeekAccessService = class {
3891
4369
  dailyNoteService;
3892
4370
  availabilityService;
3893
4371
  membershipService;
3894
- bookingService;
4372
+ bookingServices = /* @__PURE__ */ new Map();
3895
4373
  reviewService;
3896
4374
  constructor(config) {
3897
4375
  const hasApiUrl = !!config.apiUrl;
@@ -3902,6 +4380,7 @@ var PeekAccessService = class {
3902
4380
  if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
3903
4381
  if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
3904
4382
  this.jwtSecret = config.jwtSecret;
4383
+ this.issuer = config.issuer;
3905
4384
  const logger = config.logger ?? noopLogger;
3906
4385
  const tokens = createTokenManager(config);
3907
4386
  const useRegistrySlug = hasApiUrl || isV2;
@@ -4062,17 +4541,25 @@ var PeekAccessService = class {
4062
4541
  }
4063
4542
  /**
4064
4543
  * Returns the {@link BookingService} for this install, bound to the shared
4065
- * authenticated transport. The instance is created lazily and reused.
4544
+ * authenticated transport. Instances are created lazily and reused per
4545
+ * `integrator`.
4546
+ *
4547
+ * `integrator` scopes `getMetaData` results to a custom integrator id. **Do
4548
+ * not set it** unless Peek engineering created a custom integrator id for this
4549
+ * integration — omit it and the correct default is used.
4066
4550
  */
4067
- getBookingService() {
4068
- if (!this.bookingService) {
4069
- this.bookingService = new BookingService(
4551
+ getBookingService(integrator) {
4552
+ const resolved = integrator ?? this.issuer;
4553
+ let service = this.bookingServices.get(resolved);
4554
+ if (!service) {
4555
+ service = new BookingService(
4070
4556
  this.client,
4071
4557
  { productService: this.getProductService() },
4072
- { accessOptions: this.accessOptions }
4558
+ { accessOptions: this.accessOptions, integrator: resolved }
4073
4559
  );
4560
+ this.bookingServices.set(resolved, service);
4074
4561
  }
4075
- return this.bookingService;
4562
+ return service;
4076
4563
  }
4077
4564
  /**
4078
4565
  * Returns the {@link ReviewService} for this install, bound to the shared