@peektravel/app-utilities 0.7.4 → 0.8.1

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
@@ -1061,6 +1061,107 @@ function toBookingAddon(item) {
1061
1061
  return { addonId, addonName, total: item.total, addonOptions };
1062
1062
  }
1063
1063
 
1064
+ // src/internal/peek/bookings/custom-question-answer.ts
1065
+ var QUESTION_TYPE_CHECK_BOX = "CHECK_BOX";
1066
+ var QUESTION_TYPE_TEXT = "TEXT";
1067
+ var QUESTION_TYPE_SELECT_ONE = "SELECT_ONE";
1068
+ var QUESTION_TYPE_LOCATION = "LOCATION";
1069
+ var QUESTION_ID_REGEX = /^cq_[a-z0-9]+$/;
1070
+ var OPTION_ID_REGEX = /^cqao_[a-z0-9]+$/;
1071
+ var CHECKBOX_TRUE = /* @__PURE__ */ new Set(["yes", "true"]);
1072
+ var CHECKBOX_VALID = /* @__PURE__ */ new Set(["yes", "no", "true", "false"]);
1073
+ var CHECKBOX_TEXT_TRUE = "Yes";
1074
+ var CHECKBOX_TEXT_FALSE = "No";
1075
+ var errUnknownQuestionId = (v) => `No custom question matches id "${v}" for this activity`;
1076
+ var errNoTextMatch = (v) => `No custom question matches text "${v}" for this activity`;
1077
+ var errAmbiguousText = (v) => `Custom question text "${v}" matches more than one question`;
1078
+ var errPerGuestUnsupported = (id) => `Per-guest custom question "${id}" is not yet supported`;
1079
+ var errUnsupportedType = (type, id) => `Custom question "${id}" has unsupported type "${type}"`;
1080
+ var errInvalidCheckbox = (v, id) => `Checkbox answer for question "${id}" must be yes/no/true/false, got "${v}"`;
1081
+ var errUnknownOptionId = (v, id) => `No option matches id "${v}" for question "${id}"`;
1082
+ var errNoOptionMatch = (v, id) => `No option matches "${v}" for question "${id}"`;
1083
+ var errAmbiguousOption = (v, id) => `Option "${v}" matches more than one option for question "${id}"`;
1084
+ var errMissingRequired = (text, id) => `Custom question "${text}" (${id}) is required but was not answered`;
1085
+ function normalize(value) {
1086
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
1087
+ }
1088
+ function resolveQuestion(idOrText, questions) {
1089
+ if (QUESTION_ID_REGEX.test(idOrText)) {
1090
+ const byId = questions.find((question) => question.id === idOrText);
1091
+ if (!byId) throw new Error(errUnknownQuestionId(idOrText));
1092
+ return byId;
1093
+ }
1094
+ const target = normalize(idOrText);
1095
+ const matches = questions.filter(
1096
+ (question) => normalize(question.questionText) === target
1097
+ );
1098
+ if (matches.length === 0) throw new Error(errNoTextMatch(idOrText));
1099
+ if (matches.length > 1) throw new Error(errAmbiguousText(idOrText));
1100
+ return matches[0];
1101
+ }
1102
+ function resolveOption(value, question) {
1103
+ if (OPTION_ID_REGEX.test(value)) {
1104
+ const byId = question.options.find((option) => option.id === value);
1105
+ if (!byId) throw new Error(errUnknownOptionId(value, question.id));
1106
+ return byId;
1107
+ }
1108
+ const target = normalize(value);
1109
+ const matches = question.options.filter(
1110
+ (option) => normalize(option.value) === target
1111
+ );
1112
+ if (matches.length === 0) throw new Error(errNoOptionMatch(value, question.id));
1113
+ if (matches.length > 1) throw new Error(errAmbiguousOption(value, question.id));
1114
+ return matches[0];
1115
+ }
1116
+ function resolveOne(answer, questions) {
1117
+ const question = resolveQuestion(answer.questionIdOrText, questions);
1118
+ if (question.perGuest) {
1119
+ throw new Error(errPerGuestUnsupported(question.id));
1120
+ }
1121
+ switch (question.questionType) {
1122
+ case QUESTION_TYPE_CHECK_BOX: {
1123
+ const normalized = answer.value.trim().toLowerCase();
1124
+ if (!CHECKBOX_VALID.has(normalized)) {
1125
+ throw new Error(errInvalidCheckbox(answer.value, question.id));
1126
+ }
1127
+ const isChecked = CHECKBOX_TRUE.has(normalized);
1128
+ return {
1129
+ questionId: question.id,
1130
+ isChecked,
1131
+ questionAnswerText: isChecked ? CHECKBOX_TEXT_TRUE : CHECKBOX_TEXT_FALSE
1132
+ };
1133
+ }
1134
+ case QUESTION_TYPE_TEXT:
1135
+ return { questionId: question.id, questionAnswerText: answer.value };
1136
+ case QUESTION_TYPE_SELECT_ONE:
1137
+ case QUESTION_TYPE_LOCATION: {
1138
+ const option = resolveOption(answer.value, question);
1139
+ return {
1140
+ questionId: question.id,
1141
+ questionAnswerOptionId: option.id,
1142
+ questionAnswerText: option.value
1143
+ };
1144
+ }
1145
+ default:
1146
+ throw new Error(errUnsupportedType(question.questionType, question.id));
1147
+ }
1148
+ }
1149
+ function resolveCustomQuestionAnswers(answers, questions, options) {
1150
+ const resolved = answers.map((answer) => resolveOne(answer, questions));
1151
+ if (options?.requireRequired) {
1152
+ assertRequiredAnswered(resolved, questions);
1153
+ }
1154
+ return resolved;
1155
+ }
1156
+ function assertRequiredAnswered(resolved, questions) {
1157
+ const answered = new Set(resolved.map((answer) => answer.questionId));
1158
+ for (const question of questions) {
1159
+ if (question.isRequired && !question.perGuest && !answered.has(question.id)) {
1160
+ throw new Error(errMissingRequired(question.questionText, question.id));
1161
+ }
1162
+ }
1163
+ }
1164
+
1064
1165
  // src/internal/peek/bookings/booking-service.ts
1065
1166
  var DEFAULT_PAGE_SIZE2 = 50;
1066
1167
  var DEFAULT_CANCEL_NOTE = "Canceled";
@@ -1575,11 +1676,13 @@ ${note}`;
1575
1676
  *
1576
1677
  * @throws {Error} when `activityId`, `availabilityTimeId`, a ticket
1577
1678
  * `resourceOptionId`/positive `quantity`, or the guest `name` is missing; when
1578
- * `markAsPaid` is set without an `idempotencyKey`; or when the quote/order
1579
- * mutations fail.
1679
+ * `markAsPaid` is set without an `idempotencyKey`; when
1680
+ * `requireRequiredQuestions` is set and a required custom question is left
1681
+ * unanswered; or when the quote/order mutations fail.
1580
1682
  */
1581
1683
  async create(input) {
1582
1684
  validateCreateInput(input);
1685
+ const questionAnswers = await this.buildQuestionAnswers(input);
1583
1686
  const tickets = input.tickets.flatMap(
1584
1687
  (ticket) => Array.from({ length: ticket.quantity }, () => ({
1585
1688
  resourceOptionId: ticket.resourceOptionId,
@@ -1606,6 +1709,9 @@ ${note}`;
1606
1709
  if (input.operatorNotes) {
1607
1710
  bookingQuote.operatorNotes = input.operatorNotes;
1608
1711
  }
1712
+ if (questionAnswers) {
1713
+ bookingQuote.questionAnswers = questionAnswers;
1714
+ }
1609
1715
  const quoteInput = { bookingQuotes: [bookingQuote] };
1610
1716
  if (input.parentOrderId) {
1611
1717
  quoteInput.source = { clonedFromId: input.parentOrderId };
@@ -1645,6 +1751,32 @@ ${note}`;
1645
1751
  }
1646
1752
  return created;
1647
1753
  }
1754
+ /**
1755
+ * Resolves the input's custom-question answers into the quote's
1756
+ * `questionAnswers` payload (each tagged with a fresh `refid`), or `null` when
1757
+ * none were supplied. Fetches the activity's custom questions to validate
1758
+ * against; throws on the first unmatched/ambiguous/invalid answer.
1759
+ */
1760
+ async buildQuestionAnswers(input) {
1761
+ const answers = input.customQuestionAnswers ?? [];
1762
+ const requireRequired = input.requireRequiredQuestions ?? false;
1763
+ if (answers.length === 0 && !requireRequired) {
1764
+ return null;
1765
+ }
1766
+ const questions = await this.deps.productService.getCustomQuestions(
1767
+ input.activityId
1768
+ );
1769
+ const resolved = resolveCustomQuestionAnswers(answers, questions, {
1770
+ requireRequired
1771
+ });
1772
+ if (resolved.length === 0) {
1773
+ return null;
1774
+ }
1775
+ return resolved.map((answer) => ({
1776
+ ...answer,
1777
+ refid: crypto.randomUUID()
1778
+ }));
1779
+ }
1648
1780
  async markCreatedBookingPaid(booking, input) {
1649
1781
  const parsedPartial = input.markAsPaidAmount ? parseFloat(input.markAsPaidAmount) : NaN;
1650
1782
  const amount = !Number.isNaN(parsedPartial) && parsedPartial > 0 ? parsedPartial.toFixed(2) : booking.balanceAmount;
@@ -2957,6 +3089,33 @@ function fromItemOptionNodes(nodes) {
2957
3089
  return Array.from(grouped.values());
2958
3090
  }
2959
3091
 
3092
+ // src/internal/peek/products/custom-question-converter.ts
3093
+ function fromQuestionConfigurations(configs) {
3094
+ return configs.map(fromQuestionConfiguration);
3095
+ }
3096
+ function fromQuestionConfiguration(config) {
3097
+ const question = config.question;
3098
+ return {
3099
+ id: question.id,
3100
+ order: config.order ?? 0,
3101
+ isRequired: config.isRequired ?? false,
3102
+ questionText: question.text,
3103
+ hintText: question.hint ?? null,
3104
+ questionType: question.questionType,
3105
+ internalLabel: question.internalLabel ?? null,
3106
+ perGuest: question.perGuest ?? false,
3107
+ defaultValue: question.answerDefaultValue ?? null,
3108
+ options: (question.options ?? []).map(fromQuestionOption)
3109
+ };
3110
+ }
3111
+ function fromQuestionOption(option) {
3112
+ return {
3113
+ id: option.id,
3114
+ order: option.order,
3115
+ value: option.text
3116
+ };
3117
+ }
3118
+
2960
3119
  // src/internal/peek/products/product-queries.ts
2961
3120
  var PRODUCTS_QUERY = `
2962
3121
  query Sales {
@@ -3015,7 +3174,38 @@ var ITEM_OPTIONS_QUERY = `
3015
3174
  }
3016
3175
  `;
3017
3176
 
3177
+ // src/internal/peek/products/custom-question-queries.ts
3178
+ var CUSTOM_QUESTIONS_QUERY = `
3179
+ query Sales($id: ID!) {
3180
+ activity(id: $id) {
3181
+ id
3182
+ name
3183
+ questionActivityConfigurations {
3184
+ isRequired
3185
+ order
3186
+ question {
3187
+ id
3188
+ questionType
3189
+ text
3190
+ answerDefaultValue
3191
+ hint
3192
+ internalLabel
3193
+ perGuest
3194
+ ... on ChoiceQuestion {
3195
+ options {
3196
+ id
3197
+ text
3198
+ order
3199
+ }
3200
+ }
3201
+ }
3202
+ }
3203
+ }
3204
+ }
3205
+ `;
3206
+
3018
3207
  // src/internal/peek/products/product-service.ts
3208
+ var ERROR_PRODUCT_ID_REQUIRED2 = "A non-empty product id is required.";
3019
3209
  var DEFAULT_ITEM_OPTIONS_PAGE_SIZE = 50;
3020
3210
  var ProductService = class {
3021
3211
  constructor(client, options = {}) {
@@ -3060,6 +3250,32 @@ var ProductService = class {
3060
3250
  const nodes = await this.fetchAllItemOptionNodes();
3061
3251
  return fromItemOptionNodes(nodes);
3062
3252
  }
3253
+ /**
3254
+ * Returns every custom question configured on a single activity, in the order
3255
+ * Peek reports them. Choice-style questions (`SELECT_ONE`, `LOCATION`, …)
3256
+ * carry their selectable {@link CustomQuestion.options}; free-text/checkbox
3257
+ * questions return an empty option list.
3258
+ *
3259
+ * These are question **definitions**, not customer answers — no PII is
3260
+ * involved, so the result is unaffected by `fullCustomerAccess`.
3261
+ *
3262
+ * @param productId The activity's id.
3263
+ * @returns The activity's custom questions, or an empty list when the activity
3264
+ * has none or is not found.
3265
+ * @throws Error when `productId` is missing or blank.
3266
+ */
3267
+ async getCustomQuestions(productId) {
3268
+ if (!productId?.trim()) {
3269
+ throw new Error(ERROR_PRODUCT_ID_REQUIRED2);
3270
+ }
3271
+ const body = await this.client.request(
3272
+ SALES_ENDPOINT,
3273
+ CUSTOM_QUESTIONS_QUERY,
3274
+ { id: productId }
3275
+ );
3276
+ const configs = body.data?.activity?.questionActivityConfigurations ?? [];
3277
+ return fromQuestionConfigurations(configs);
3278
+ }
3063
3279
  async fetchActivities() {
3064
3280
  const body = await this.client.request(
3065
3281
  SALES_ENDPOINT,
@@ -3773,6 +3989,10 @@ var PeekAccessService = class {
3773
3989
  getAllAddons() {
3774
3990
  return this.getProductService().getAllAddons();
3775
3991
  }
3992
+ /** An activity's custom questions. Delegates to {@link ProductService.getCustomQuestions}. */
3993
+ getCustomQuestions(productId) {
3994
+ return this.getProductService().getCustomQuestions(productId);
3995
+ }
3776
3996
  // ─── Account-user short-forms ─────────────────────────────────────────────
3777
3997
  /** All active account users. Delegates to {@link AccountUserService.getAll}. */
3778
3998
  getAllAccountUsers() {