@peektravel/app-utilities 0.7.4 → 0.8.0

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,94 @@ 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
+ function normalize(value) {
1085
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
1086
+ }
1087
+ function resolveQuestion(idOrText, questions) {
1088
+ if (QUESTION_ID_REGEX.test(idOrText)) {
1089
+ const byId = questions.find((question) => question.id === idOrText);
1090
+ if (!byId) throw new Error(errUnknownQuestionId(idOrText));
1091
+ return byId;
1092
+ }
1093
+ const target = normalize(idOrText);
1094
+ const matches = questions.filter(
1095
+ (question) => normalize(question.questionText) === target
1096
+ );
1097
+ if (matches.length === 0) throw new Error(errNoTextMatch(idOrText));
1098
+ if (matches.length > 1) throw new Error(errAmbiguousText(idOrText));
1099
+ return matches[0];
1100
+ }
1101
+ function resolveOption(value, question) {
1102
+ if (OPTION_ID_REGEX.test(value)) {
1103
+ const byId = question.options.find((option) => option.id === value);
1104
+ if (!byId) throw new Error(errUnknownOptionId(value, question.id));
1105
+ return byId;
1106
+ }
1107
+ const target = normalize(value);
1108
+ const matches = question.options.filter(
1109
+ (option) => normalize(option.value) === target
1110
+ );
1111
+ if (matches.length === 0) throw new Error(errNoOptionMatch(value, question.id));
1112
+ if (matches.length > 1) throw new Error(errAmbiguousOption(value, question.id));
1113
+ return matches[0];
1114
+ }
1115
+ function resolveOne(answer, questions) {
1116
+ const question = resolveQuestion(answer.questionIdOrText, questions);
1117
+ if (question.perGuest) {
1118
+ throw new Error(errPerGuestUnsupported(question.id));
1119
+ }
1120
+ switch (question.questionType) {
1121
+ case QUESTION_TYPE_CHECK_BOX: {
1122
+ const normalized = answer.value.trim().toLowerCase();
1123
+ if (!CHECKBOX_VALID.has(normalized)) {
1124
+ throw new Error(errInvalidCheckbox(answer.value, question.id));
1125
+ }
1126
+ const isChecked = CHECKBOX_TRUE.has(normalized);
1127
+ return {
1128
+ questionId: question.id,
1129
+ isChecked,
1130
+ questionAnswerText: isChecked ? CHECKBOX_TEXT_TRUE : CHECKBOX_TEXT_FALSE
1131
+ };
1132
+ }
1133
+ case QUESTION_TYPE_TEXT:
1134
+ return { questionId: question.id, questionAnswerText: answer.value };
1135
+ case QUESTION_TYPE_SELECT_ONE:
1136
+ case QUESTION_TYPE_LOCATION: {
1137
+ const option = resolveOption(answer.value, question);
1138
+ return {
1139
+ questionId: question.id,
1140
+ questionAnswerOptionId: option.id,
1141
+ questionAnswerText: option.value
1142
+ };
1143
+ }
1144
+ default:
1145
+ throw new Error(errUnsupportedType(question.questionType, question.id));
1146
+ }
1147
+ }
1148
+ function resolveCustomQuestionAnswers(answers, questions) {
1149
+ return answers.map((answer) => resolveOne(answer, questions));
1150
+ }
1151
+
1064
1152
  // src/internal/peek/bookings/booking-service.ts
1065
1153
  var DEFAULT_PAGE_SIZE2 = 50;
1066
1154
  var DEFAULT_CANCEL_NOTE = "Canceled";
@@ -1580,6 +1668,7 @@ ${note}`;
1580
1668
  */
1581
1669
  async create(input) {
1582
1670
  validateCreateInput(input);
1671
+ const questionAnswers = await this.buildQuestionAnswers(input);
1583
1672
  const tickets = input.tickets.flatMap(
1584
1673
  (ticket) => Array.from({ length: ticket.quantity }, () => ({
1585
1674
  resourceOptionId: ticket.resourceOptionId,
@@ -1606,6 +1695,9 @@ ${note}`;
1606
1695
  if (input.operatorNotes) {
1607
1696
  bookingQuote.operatorNotes = input.operatorNotes;
1608
1697
  }
1698
+ if (questionAnswers) {
1699
+ bookingQuote.questionAnswers = questionAnswers;
1700
+ }
1609
1701
  const quoteInput = { bookingQuotes: [bookingQuote] };
1610
1702
  if (input.parentOrderId) {
1611
1703
  quoteInput.source = { clonedFromId: input.parentOrderId };
@@ -1645,6 +1737,25 @@ ${note}`;
1645
1737
  }
1646
1738
  return created;
1647
1739
  }
1740
+ /**
1741
+ * Resolves the input's custom-question answers into the quote's
1742
+ * `questionAnswers` payload (each tagged with a fresh `refid`), or `null` when
1743
+ * none were supplied. Fetches the activity's custom questions to validate
1744
+ * against; throws on the first unmatched/ambiguous/invalid answer.
1745
+ */
1746
+ async buildQuestionAnswers(input) {
1747
+ const answers = input.customQuestionAnswers;
1748
+ if (!answers || answers.length === 0) {
1749
+ return null;
1750
+ }
1751
+ const questions = await this.deps.productService.getCustomQuestions(
1752
+ input.activityId
1753
+ );
1754
+ return resolveCustomQuestionAnswers(answers, questions).map((resolved) => ({
1755
+ ...resolved,
1756
+ refid: crypto.randomUUID()
1757
+ }));
1758
+ }
1648
1759
  async markCreatedBookingPaid(booking, input) {
1649
1760
  const parsedPartial = input.markAsPaidAmount ? parseFloat(input.markAsPaidAmount) : NaN;
1650
1761
  const amount = !Number.isNaN(parsedPartial) && parsedPartial > 0 ? parsedPartial.toFixed(2) : booking.balanceAmount;
@@ -2957,6 +3068,33 @@ function fromItemOptionNodes(nodes) {
2957
3068
  return Array.from(grouped.values());
2958
3069
  }
2959
3070
 
3071
+ // src/internal/peek/products/custom-question-converter.ts
3072
+ function fromQuestionConfigurations(configs) {
3073
+ return configs.map(fromQuestionConfiguration);
3074
+ }
3075
+ function fromQuestionConfiguration(config) {
3076
+ const question = config.question;
3077
+ return {
3078
+ id: question.id,
3079
+ order: config.order ?? 0,
3080
+ isRequired: config.isRequired ?? false,
3081
+ questionText: question.text,
3082
+ hintText: question.hint ?? null,
3083
+ questionType: question.questionType,
3084
+ internalLabel: question.internalLabel ?? null,
3085
+ perGuest: question.perGuest ?? false,
3086
+ defaultValue: question.answerDefaultValue ?? null,
3087
+ options: (question.options ?? []).map(fromQuestionOption)
3088
+ };
3089
+ }
3090
+ function fromQuestionOption(option) {
3091
+ return {
3092
+ id: option.id,
3093
+ order: option.order,
3094
+ value: option.text
3095
+ };
3096
+ }
3097
+
2960
3098
  // src/internal/peek/products/product-queries.ts
2961
3099
  var PRODUCTS_QUERY = `
2962
3100
  query Sales {
@@ -3015,7 +3153,38 @@ var ITEM_OPTIONS_QUERY = `
3015
3153
  }
3016
3154
  `;
3017
3155
 
3156
+ // src/internal/peek/products/custom-question-queries.ts
3157
+ var CUSTOM_QUESTIONS_QUERY = `
3158
+ query Sales($id: ID!) {
3159
+ activity(id: $id) {
3160
+ id
3161
+ name
3162
+ questionActivityConfigurations {
3163
+ isRequired
3164
+ order
3165
+ question {
3166
+ id
3167
+ questionType
3168
+ text
3169
+ answerDefaultValue
3170
+ hint
3171
+ internalLabel
3172
+ perGuest
3173
+ ... on ChoiceQuestion {
3174
+ options {
3175
+ id
3176
+ text
3177
+ order
3178
+ }
3179
+ }
3180
+ }
3181
+ }
3182
+ }
3183
+ }
3184
+ `;
3185
+
3018
3186
  // src/internal/peek/products/product-service.ts
3187
+ var ERROR_PRODUCT_ID_REQUIRED2 = "A non-empty product id is required.";
3019
3188
  var DEFAULT_ITEM_OPTIONS_PAGE_SIZE = 50;
3020
3189
  var ProductService = class {
3021
3190
  constructor(client, options = {}) {
@@ -3060,6 +3229,32 @@ var ProductService = class {
3060
3229
  const nodes = await this.fetchAllItemOptionNodes();
3061
3230
  return fromItemOptionNodes(nodes);
3062
3231
  }
3232
+ /**
3233
+ * Returns every custom question configured on a single activity, in the order
3234
+ * Peek reports them. Choice-style questions (`SELECT_ONE`, `LOCATION`, …)
3235
+ * carry their selectable {@link CustomQuestion.options}; free-text/checkbox
3236
+ * questions return an empty option list.
3237
+ *
3238
+ * These are question **definitions**, not customer answers — no PII is
3239
+ * involved, so the result is unaffected by `fullCustomerAccess`.
3240
+ *
3241
+ * @param productId The activity's id.
3242
+ * @returns The activity's custom questions, or an empty list when the activity
3243
+ * has none or is not found.
3244
+ * @throws Error when `productId` is missing or blank.
3245
+ */
3246
+ async getCustomQuestions(productId) {
3247
+ if (!productId?.trim()) {
3248
+ throw new Error(ERROR_PRODUCT_ID_REQUIRED2);
3249
+ }
3250
+ const body = await this.client.request(
3251
+ SALES_ENDPOINT,
3252
+ CUSTOM_QUESTIONS_QUERY,
3253
+ { id: productId }
3254
+ );
3255
+ const configs = body.data?.activity?.questionActivityConfigurations ?? [];
3256
+ return fromQuestionConfigurations(configs);
3257
+ }
3063
3258
  async fetchActivities() {
3064
3259
  const body = await this.client.request(
3065
3260
  SALES_ENDPOINT,
@@ -3773,6 +3968,10 @@ var PeekAccessService = class {
3773
3968
  getAllAddons() {
3774
3969
  return this.getProductService().getAllAddons();
3775
3970
  }
3971
+ /** An activity's custom questions. Delegates to {@link ProductService.getCustomQuestions}. */
3972
+ getCustomQuestions(productId) {
3973
+ return this.getProductService().getCustomQuestions(productId);
3974
+ }
3776
3975
  // ─── Account-user short-forms ─────────────────────────────────────────────
3777
3976
  /** All active account users. Delegates to {@link AccountUserService.getAll}. */
3778
3977
  getAllAccountUsers() {