@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.121",
3
+ "version": "1.2.123",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -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: normalizedType || undefined,
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: String(line.type || 'FEE'),
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).map(([name, amount]) => ({
1104
- name,
1105
- totalAmount: amount,
1106
- description: feeMetadata[name]?.description,
1107
- }));
1108
- }, [currency, isTaxIncludedInPrice, pricingConfig]);
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
  ? [