@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 +222 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -3
- package/dist/index.d.ts +129 -3
- package/dist/index.js +222 -2
- package/dist/index.js.map +1 -1
- package/llms.txt +19 -2
- package/package.json +1 -1
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`;
|
|
1579
|
-
*
|
|
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() {
|