@ticketboothapp/booking 1.2.121 → 1.2.123
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/package.json
CHANGED
|
@@ -209,6 +209,48 @@ function resolveTicketQtyFromQuantities(
|
|
|
209
209
|
return undefined;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
function normalizeFeReceiptLineItemType(type: string | null | undefined): string | undefined {
|
|
213
|
+
const normalized = String(type ?? '').trim().toUpperCase();
|
|
214
|
+
if (!normalized) return undefined;
|
|
215
|
+
switch (normalized) {
|
|
216
|
+
case 'TICKET':
|
|
217
|
+
case 'ADULT':
|
|
218
|
+
case 'CHILD':
|
|
219
|
+
case 'INFANT':
|
|
220
|
+
case 'SENIOR':
|
|
221
|
+
case 'STUDENT':
|
|
222
|
+
return 'TICKET';
|
|
223
|
+
case 'RETURN':
|
|
224
|
+
case 'RETURN_OPTION':
|
|
225
|
+
case 'RETURNOPTION':
|
|
226
|
+
return 'RETURN_OPTION';
|
|
227
|
+
case 'CANCELLATION':
|
|
228
|
+
case 'CANCELLATION_UPGRADE':
|
|
229
|
+
case 'CANCELLATION_POLICY_UPGRADE':
|
|
230
|
+
return 'CANCELLATION_UPGRADE';
|
|
231
|
+
case 'PROMO':
|
|
232
|
+
case 'PROMO_CODE':
|
|
233
|
+
return 'PROMO_CODE';
|
|
234
|
+
case 'VOUCHER':
|
|
235
|
+
case 'DISCOUNT':
|
|
236
|
+
return 'DISCOUNT';
|
|
237
|
+
case 'GIFT_CARD':
|
|
238
|
+
return 'GIFT_CARD';
|
|
239
|
+
case 'DEAL':
|
|
240
|
+
return 'DEAL';
|
|
241
|
+
case 'TAX':
|
|
242
|
+
return 'TAX';
|
|
243
|
+
case 'ROUNDING':
|
|
244
|
+
return 'ROUNDING';
|
|
245
|
+
case 'BOOKING_CHANGE':
|
|
246
|
+
return 'BOOKING_CHANGE';
|
|
247
|
+
case 'FEE':
|
|
248
|
+
return 'FEE';
|
|
249
|
+
default:
|
|
250
|
+
return 'FEE';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
212
254
|
/** Receipt TICKET rows sent on admin quote must follow picker {@link quantities}: BE apply uses these lines for headcount when feReceipt is authoritative — server `priceSummaryLines` can lag counts. */
|
|
213
255
|
function mapSummaryLinesToFeReceiptLineItems(
|
|
214
256
|
lines: PriceSummaryLine[],
|
|
@@ -231,11 +273,10 @@ function mapSummaryLinesToFeReceiptLineItems(
|
|
|
231
273
|
quantity: Math.max(0, effectiveQty),
|
|
232
274
|
};
|
|
233
275
|
}
|
|
234
|
-
const normalizedType = String(line.type ?? '').trim().toUpperCase();
|
|
235
276
|
return {
|
|
236
277
|
label: line.label,
|
|
237
278
|
amount: roundMoney(line.amount),
|
|
238
|
-
type:
|
|
279
|
+
type: normalizeFeReceiptLineItemType(line.type),
|
|
239
280
|
quantity: line.quantity ?? undefined,
|
|
240
281
|
};
|
|
241
282
|
});
|
|
@@ -4238,7 +4279,7 @@ export function AdminChangeBookingFlow({
|
|
|
4238
4279
|
totalAmount: adminFeAuthoritativeReceipt.total,
|
|
4239
4280
|
currency: adminFeAuthoritativeReceipt.currency ?? currency,
|
|
4240
4281
|
lineItems: adminFeAuthoritativeReceipt.lineItems.map((line) => ({
|
|
4241
|
-
type:
|
|
4282
|
+
type: normalizeFeReceiptLineItemType(line.type) || 'FEE',
|
|
4242
4283
|
label: String(line.label || 'Adjustment'),
|
|
4243
4284
|
amount: Number(line.amount) || 0,
|
|
4244
4285
|
...(line.quantity != null ? { quantity: Number(line.quantity) || 0 } : {}),
|
|
@@ -119,6 +119,27 @@ interface PrivateShuttleBookingFlowProps {
|
|
|
119
119
|
const RESOURCE_CAPACITY = 13;
|
|
120
120
|
const ADDITIONAL_HOUR_PRICE = 170;
|
|
121
121
|
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
122
|
+
const MORAINE_LAKE_ROAD_ACCESS_FEE_TOOLTIP =
|
|
123
|
+
"Since 2025, Parks Canada charges a per-trip fee for License of Occupation at Moraine Lake. This fee contributes towards Parks Canada's Moraine Lake Road operations.";
|
|
124
|
+
|
|
125
|
+
function isMoraineLakeRoadAccessFeeName(name: string): boolean {
|
|
126
|
+
const normalized = name.toLowerCase();
|
|
127
|
+
return (
|
|
128
|
+
normalized.includes('moraine') &&
|
|
129
|
+
(normalized.includes('access') ||
|
|
130
|
+
normalized.includes('road') ||
|
|
131
|
+
normalized.includes('license'))
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isMoraineLakeLocationText(value: string): boolean {
|
|
136
|
+
const normalized = value.toLowerCase().replace(/[_-]+/g, ' ');
|
|
137
|
+
return normalized.includes('moraine') && normalized.includes('lake');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function roundMoney(amount: number): number {
|
|
141
|
+
return Math.round(amount * 100) / 100;
|
|
142
|
+
}
|
|
122
143
|
|
|
123
144
|
function parseAvailabilityDateTime(value: string): Date {
|
|
124
145
|
// If API omits timezone offset, treat it as UTC to prevent user-local day shifts.
|
|
@@ -1095,17 +1116,38 @@ export function PrivateShuttleBookingFlow({
|
|
|
1095
1116
|
const cancellationPolicyFee = selectedCancellationPolicy
|
|
1096
1117
|
? (selectedCancellationPolicy.feeByCurrency[currency] ?? 0)
|
|
1097
1118
|
: 0;
|
|
1119
|
+
const draftItineraryIncludesMoraineLake = useMemo(() => {
|
|
1120
|
+
if (draftItineraryDestinations.length === 0) return false;
|
|
1121
|
+
const destinationLabelsById = new Map(
|
|
1122
|
+
(product.itineraryBuilder?.destinations ?? []).map((destination) => [
|
|
1123
|
+
destination.id,
|
|
1124
|
+
destination.label,
|
|
1125
|
+
])
|
|
1126
|
+
);
|
|
1127
|
+
return draftItineraryDestinations.some((destinationId) => {
|
|
1128
|
+
const label = destinationLabelsById.get(destinationId);
|
|
1129
|
+
return (
|
|
1130
|
+
isMoraineLakeLocationText(destinationId) ||
|
|
1131
|
+
(label ? isMoraineLakeLocationText(label) : false)
|
|
1132
|
+
);
|
|
1133
|
+
});
|
|
1134
|
+
}, [draftItineraryDestinations, product.itineraryBuilder?.destinations]);
|
|
1098
1135
|
const perBookingFeeLineItems = useMemo(() => {
|
|
1099
1136
|
if (isTaxIncludedInPrice) return [];
|
|
1100
1137
|
const amountsByName = pricingConfig?.perBookingFeesByCurrency?.[currency];
|
|
1101
1138
|
if (!amountsByName) return [];
|
|
1102
1139
|
const feeMetadata = pricingConfig?.perBookingFees ?? {};
|
|
1103
|
-
return Object.entries(amountsByName)
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1140
|
+
return Object.entries(amountsByName)
|
|
1141
|
+
.filter(
|
|
1142
|
+
([name]) =>
|
|
1143
|
+
draftItineraryIncludesMoraineLake || !isMoraineLakeRoadAccessFeeName(name)
|
|
1144
|
+
)
|
|
1145
|
+
.map(([name, amount]) => ({
|
|
1146
|
+
name,
|
|
1147
|
+
totalAmount: amount,
|
|
1148
|
+
description: feeMetadata[name]?.description,
|
|
1149
|
+
}));
|
|
1150
|
+
}, [currency, draftItineraryIncludesMoraineLake, isTaxIncludedInPrice, pricingConfig]);
|
|
1109
1151
|
|
|
1110
1152
|
const addOnTotal = useMemo(() => {
|
|
1111
1153
|
let sum = 0;
|
|
@@ -1134,6 +1176,116 @@ export function PrivateShuttleBookingFlow({
|
|
|
1134
1176
|
? (isTaxIncludedInPrice ? 0 : (subtotal - effectivePromoDiscountAmount) * taxRate)
|
|
1135
1177
|
: taxAmount;
|
|
1136
1178
|
const totalPrice = subtotal + effectiveTaxAmount - effectivePromoDiscountAmount;
|
|
1179
|
+
const providerChangeAuthoritativeReceipt = useMemo<
|
|
1180
|
+
ProviderDashboardChangeBookingPayload['authoritativeReceipt']
|
|
1181
|
+
>(() => {
|
|
1182
|
+
const lineItems: NonNullable<
|
|
1183
|
+
ProviderDashboardChangeBookingPayload['authoritativeReceipt']
|
|
1184
|
+
>['lineItems'] = [
|
|
1185
|
+
{
|
|
1186
|
+
type: 'TICKET',
|
|
1187
|
+
label: resourceCount > 1 ? 'Shuttles' : 'Shuttle',
|
|
1188
|
+
amount: roundMoney(basePrice),
|
|
1189
|
+
quantity: resourceCount,
|
|
1190
|
+
},
|
|
1191
|
+
];
|
|
1192
|
+
|
|
1193
|
+
for (const sel of addOnSelections) {
|
|
1194
|
+
const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
|
|
1195
|
+
if (!addOn) continue;
|
|
1196
|
+
const hasVariant =
|
|
1197
|
+
(addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
|
|
1198
|
+
sel.variantId;
|
|
1199
|
+
const variant = hasVariant
|
|
1200
|
+
? addOn.variants?.find((v) => v.id === sel.variantId)
|
|
1201
|
+
: null;
|
|
1202
|
+
const amount = ((addOn.price ?? 0) + (variant?.priceAdjustment ?? 0)) * (sel.quantity ?? 1);
|
|
1203
|
+
lineItems.push({
|
|
1204
|
+
type: 'FEE',
|
|
1205
|
+
label: variant?.label ? `${addOn.name} (${variant.label})` : addOn.name,
|
|
1206
|
+
amount: roundMoney(amount),
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (additionalHoursAmount > 0) {
|
|
1211
|
+
lineItems.push({
|
|
1212
|
+
type: 'FEE',
|
|
1213
|
+
label:
|
|
1214
|
+
additionalHoursCount === 1
|
|
1215
|
+
? 'Additional hour'
|
|
1216
|
+
: `Additional hours (${additionalHoursCount})`,
|
|
1217
|
+
amount: roundMoney(additionalHoursAmount),
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
for (const fee of perBookingFeeLineItems) {
|
|
1222
|
+
lineItems.push({
|
|
1223
|
+
type: 'FEE',
|
|
1224
|
+
label: fee.name,
|
|
1225
|
+
amount: roundMoney(fee.totalAmount),
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (cancellationPolicyFee > 0 && selectedCancellationPolicy) {
|
|
1230
|
+
lineItems.push({
|
|
1231
|
+
type: 'CANCELLATION_UPGRADE',
|
|
1232
|
+
label: selectedCancellationPolicy.label,
|
|
1233
|
+
amount: roundMoney(cancellationPolicyFee),
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (effectiveTaxAmount > 0) {
|
|
1238
|
+
lineItems.push({
|
|
1239
|
+
type: 'TAX',
|
|
1240
|
+
label: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
|
|
1241
|
+
amount: roundMoney(effectiveTaxAmount),
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (effectivePromoDiscountAmount > 0) {
|
|
1246
|
+
lineItems.push({
|
|
1247
|
+
type: isGiftCard ? 'GIFT_CARD' : 'PROMO_CODE',
|
|
1248
|
+
label: activePromoCode ? `Promo: ${activePromoCode}` : (t('booking.discount') || 'Discount'),
|
|
1249
|
+
amount: roundMoney(-effectivePromoDiscountAmount),
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const expectedTotal = roundMoney(totalPrice);
|
|
1254
|
+
const lineTotal = roundMoney(lineItems.reduce((sum, line) => sum + line.amount, 0));
|
|
1255
|
+
const roundingDelta = roundMoney(expectedTotal - lineTotal);
|
|
1256
|
+
if (Math.abs(roundingDelta) >= 0.01) {
|
|
1257
|
+
lineItems.push({
|
|
1258
|
+
type: 'ROUNDING',
|
|
1259
|
+
label: t('booking.rounding') || 'Rounding',
|
|
1260
|
+
amount: roundingDelta,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
return {
|
|
1265
|
+
currency,
|
|
1266
|
+
lineItems,
|
|
1267
|
+
subtotalBeforeTax: roundMoney(expectedTotal - roundMoney(effectiveTaxAmount)),
|
|
1268
|
+
taxAmount: roundMoney(effectiveTaxAmount),
|
|
1269
|
+
totalAmount: expectedTotal,
|
|
1270
|
+
};
|
|
1271
|
+
}, [
|
|
1272
|
+
activePromoCode,
|
|
1273
|
+
addOnSelections,
|
|
1274
|
+
addOns,
|
|
1275
|
+
additionalHoursAmount,
|
|
1276
|
+
additionalHoursCount,
|
|
1277
|
+
basePrice,
|
|
1278
|
+
cancellationPolicyFee,
|
|
1279
|
+
currency,
|
|
1280
|
+
effectivePromoDiscountAmount,
|
|
1281
|
+
effectiveTaxAmount,
|
|
1282
|
+
isGiftCard,
|
|
1283
|
+
perBookingFeeLineItems,
|
|
1284
|
+
resourceCount,
|
|
1285
|
+
selectedCancellationPolicy,
|
|
1286
|
+
t,
|
|
1287
|
+
totalPrice,
|
|
1288
|
+
]);
|
|
1137
1289
|
|
|
1138
1290
|
useEffect(() => {
|
|
1139
1291
|
if (!onPricePreviewChange) return;
|
|
@@ -1657,6 +1809,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
1657
1809
|
promoCode: activePromoCode ?? null,
|
|
1658
1810
|
newTotalAmount: totalPrice,
|
|
1659
1811
|
additionalHoursCount: isAdmin ? additionalHoursCount : null,
|
|
1812
|
+
authoritativeReceipt: providerChangeAuthoritativeReceipt,
|
|
1660
1813
|
});
|
|
1661
1814
|
setLoading(false);
|
|
1662
1815
|
return;
|
|
@@ -2939,6 +3092,9 @@ export function PrivateShuttleBookingFlow({
|
|
|
2939
3092
|
label: fee.name,
|
|
2940
3093
|
amount: fee.totalAmount,
|
|
2941
3094
|
type: 'fee' as const,
|
|
3095
|
+
tooltip: isMoraineLakeRoadAccessFeeName(fee.name)
|
|
3096
|
+
? MORAINE_LAKE_ROAD_ACCESS_FEE_TOOLTIP
|
|
3097
|
+
: undefined,
|
|
2942
3098
|
})),
|
|
2943
3099
|
...(cancellationPolicyFee > 0
|
|
2944
3100
|
? [
|