@ticketboothapp/booking 1.2.176 → 1.2.178

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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/src/components/booking/AddOnsSection.module.css +36 -5
  3. package/src/components/booking/AddOnsSection.tsx +236 -144
  4. package/src/components/booking/AdminChangeBookingFlow.tsx +3 -2
  5. package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +4 -1
  6. package/src/components/booking/CollapsibleAddOnItem.module.css +72 -0
  7. package/src/components/booking/CollapsibleAddOnItem.tsx +38 -0
  8. package/src/components/booking/MealDrinkAddOnSelector.tsx +48 -27
  9. package/src/components/booking/PrivateShuttleAddOnsSection.tsx +124 -57
  10. package/src/components/booking/PrivateShuttleBookingFlow.module.css +26 -0
  11. package/src/components/booking/PrivateShuttleBookingFlow.tsx +8 -5
  12. package/src/components/booking/PrivateShuttlePreferencesSection.tsx +29 -55
  13. package/src/components/booking/add-on-selection-helpers.ts +13 -0
  14. package/src/components/booking/change-booking-checkout-builders.ts +1 -1
  15. package/src/components/booking/private-shuttle-checkout-builders.ts +2 -2
  16. package/src/components/booking/private-shuttle-lunch-visibility.ts +20 -0
  17. package/src/components/booking/standard-booking-checkout-builders.ts +1 -1
  18. package/src/components/booking/useAdminChangeProtectedPricing.ts +1 -1
  19. package/src/components/booking/useBookingAvailabilityAddOns.ts +26 -4
  20. package/src/components/booking/useChangeBookingProtectedPricing.ts +1 -1
  21. package/src/components/booking/usePrivateShuttlePriceSummary.ts +13 -6
  22. package/src/components/booking/useStandardBookingPriceSummary.ts +1 -1
  23. package/src/lib/booking-api.ts +3 -1
  24. package/test/add-on-selection-helpers.test.ts +24 -0
  25. package/test/change-booking-helpers.test.ts +42 -1
  26. package/test/private-shuttle-lunch-visibility.test.ts +27 -0
@@ -1,15 +1,19 @@
1
1
  import { useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from 'react';
2
+ import { formatInTimeZone } from 'date-fns-tz';
2
3
  import {
3
4
  getAddOns,
4
5
  type AddOn,
5
6
  type Availability,
6
7
  } from '../../lib/booking-api';
7
- import { getAvailabilityOptionId } from './change-booking-flow-helpers';
8
+ import { getAvailabilityOptionId, parseAvailabilityDateTime } from './change-booking-flow-helpers';
8
9
 
9
10
  export interface UseBookingAvailabilityAddOnsParams {
10
11
  selectedAvailability: Availability | null;
11
12
  companyId?: string | null;
12
13
  onProductOptionChange?: () => void;
14
+ /** Ignore lead-time cutoffs for admin changes while continuing to honor blackout dates. */
15
+ ignoreCutoff?: boolean;
16
+ companyTimezone?: string;
13
17
  }
14
18
 
15
19
  export interface UseBookingAvailabilityAddOnsResult {
@@ -23,6 +27,8 @@ export function useBookingAvailabilityAddOns({
23
27
  selectedAvailability,
24
28
  companyId,
25
29
  onProductOptionChange,
30
+ ignoreCutoff = false,
31
+ companyTimezone,
26
32
  }: UseBookingAvailabilityAddOnsParams): UseBookingAvailabilityAddOnsResult {
27
33
  const [addOns, setAddOns] = useState<AddOn[]>([]);
28
34
  const [loadedProductOptionId, setLoadedProductOptionId] = useState<string | null>(null);
@@ -50,11 +56,20 @@ export function useBookingAvailabilityAddOns({
50
56
  getAddOns(companyId, {
51
57
  productOptionId: availabilityProductOptionId,
52
58
  preCheckout: true,
53
- dateTime: availabilityDateTime ?? undefined,
59
+ dateTime: ignoreCutoff ? undefined : (availabilityDateTime ?? undefined),
54
60
  })
55
61
  .then((rows) => {
56
62
  if (cancelled) return;
57
- setAddOns(rows);
63
+ let visibleRows = rows;
64
+ if (ignoreCutoff && availabilityDateTime && companyTimezone) {
65
+ const localDate = formatInTimeZone(
66
+ parseAvailabilityDateTime(availabilityDateTime),
67
+ companyTimezone,
68
+ 'yyyy-MM-dd',
69
+ );
70
+ visibleRows = rows.filter((addOn) => !addOn.blackoutDates?.includes(localDate));
71
+ }
72
+ setAddOns(visibleRows);
58
73
  setLoadedProductOptionId(availabilityProductOptionId);
59
74
  })
60
75
  .catch(() => {
@@ -65,7 +80,14 @@ export function useBookingAvailabilityAddOns({
65
80
  return () => {
66
81
  cancelled = true;
67
82
  };
68
- }, [availabilityProductOptionId, availabilityDateTime, companyId, onProductOptionChange]);
83
+ }, [
84
+ availabilityProductOptionId,
85
+ availabilityDateTime,
86
+ companyTimezone,
87
+ companyId,
88
+ ignoreCutoff,
89
+ onProductOptionChange,
90
+ ]);
69
91
 
70
92
  return {
71
93
  addOns,
@@ -252,7 +252,7 @@ export function useChangeBookingProtectedPricing({
252
252
  : null;
253
253
  const name = variantLabel
254
254
  ? `${addOn.name} (${variantLabel})${qty > 1 ? ` \u00d7 ${qty}` : ''}`
255
- : addOn.name;
255
+ : `${addOn.name}${qty > 1 ? ` \u00d7 ${qty}` : ''}`;
256
256
  return { name, totalAmount: amt, description: addOn.description ?? undefined };
257
257
  })
258
258
  .filter((x): x is NonNullable<typeof x> => x != null);
@@ -305,9 +305,12 @@ export function usePrivateShuttlePriceSummary({
305
305
  : null;
306
306
  const amount =
307
307
  ((addOn.price ?? 0) + (variant?.priceAdjustment ?? 0)) * (selection.quantity ?? 1);
308
+ const quantity = selection.quantity ?? 1;
308
309
  lineItems.push({
309
310
  type: 'FEE',
310
- label: variant?.label ? `${addOn.name} (${variant.label})` : addOn.name,
311
+ label: variant?.label
312
+ ? `${addOn.name} (${variant.label})${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`
313
+ : `${addOn.name}${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`,
311
314
  amount: roundMoney(amount),
312
315
  });
313
316
  }
@@ -419,13 +422,17 @@ export function usePrivateShuttlePriceSummary({
419
422
  const hasVariant =
420
423
  (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
421
424
  selection.variantId;
422
- const adjustment = hasVariant
423
- ? (addOn.variants?.find((variant) => variant.id === selection.variantId)?.priceAdjustment ?? 0)
424
- : 0;
425
+ const variant = hasVariant
426
+ ? addOn.variants?.find((candidate) => candidate.id === selection.variantId)
427
+ : null;
428
+ const adjustment = variant?.priceAdjustment ?? 0;
429
+ const quantity = selection.quantity ?? 1;
425
430
  return {
426
431
  kind: 'line',
427
- label: addOn.name,
428
- amount: (base + adjustment) * (selection.quantity ?? 1),
432
+ label: variant?.label
433
+ ? `${addOn.name} (${variant.label})${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`
434
+ : `${addOn.name}${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`,
435
+ amount: (base + adjustment) * quantity,
429
436
  type: 'fee',
430
437
  };
431
438
  })
@@ -267,7 +267,7 @@ export function useStandardBookingPriceSummary({
267
267
  : null;
268
268
  const name = variantLabel
269
269
  ? `${addOn.name} (${variantLabel})${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`
270
- : addOn.name;
270
+ : `${addOn.name}${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`;
271
271
  return { name, totalAmount: amount, description: addOn.description ?? undefined };
272
272
  })
273
273
  .filter((line): line is NonNullable<typeof line> => line != null);
@@ -808,13 +808,15 @@ export interface AddOn {
808
808
  addOnId: string;
809
809
  name: string;
810
810
  description?: string | null;
811
+ emoji?: string | null;
811
812
  price: number;
812
813
  currency: string;
813
814
  preCheckout: boolean;
814
- postCheckout: boolean;
815
815
  variantType: 'none' | 'single_choice' | 'multi_quantity';
816
816
  variants: AddOnVariant[];
817
817
  productOptionIds: string[];
818
+ minHoursBeforeStart?: number | null;
819
+ blackoutDates?: string[];
818
820
  }
819
821
 
820
822
  export interface CompanySettings {
@@ -0,0 +1,24 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { selectedAddOnQuantity } from '../src/components/booking/add-on-selection-helpers';
4
+
5
+ test('selectedAddOnQuantity totals variants and treats an omitted quantity as one', () => {
6
+ assert.equal(
7
+ selectedAddOnQuantity('addon_lunch', [
8
+ { addOnId: 'addon_lunch', quantity: 2 },
9
+ { addOnId: 'addon_lunch' },
10
+ { addOnId: 'addon_other', quantity: 9 },
11
+ ]),
12
+ 3,
13
+ );
14
+ });
15
+
16
+ test('selectedAddOnQuantity ignores zero and negative quantities', () => {
17
+ assert.equal(
18
+ selectedAddOnQuantity('addon_lunch', [
19
+ { addOnId: 'addon_lunch', quantity: 0 },
20
+ { addOnId: 'addon_lunch', quantity: -2 },
21
+ ]),
22
+ 0,
23
+ );
24
+ });
@@ -114,6 +114,7 @@ import {
114
114
  resolveInitialPrivateShuttlePassengerCount,
115
115
  } from '../src/components/booking/private-shuttle-passenger-count';
116
116
  import { hasEffectiveAdminChangeSelection } from '../src/components/booking/useAdminProviderPricingAdjustments';
117
+ import { buildPrivateShuttleCheckoutArtifacts } from '../src/components/booking/private-shuttle-checkout-builders';
117
118
  import {
118
119
  lineFromActiveAdjustment,
119
120
  reverseAdjustmentIdsForAdminCustomLines,
@@ -1903,7 +1904,6 @@ test('keeps an incompatible add-on visible until the admin explicitly removes it
1903
1904
  price: 28,
1904
1905
  currency: 'CAD',
1905
1906
  preCheckout: true,
1906
- postCheckout: false,
1907
1907
  variantType: 'multi_quantity' as const,
1908
1908
  variants: [
1909
1909
  {
@@ -2264,3 +2264,44 @@ test('exposes only authoritative selectable amendment components to admin adjust
2264
2264
  },
2265
2265
  ]);
2266
2266
  });
2267
+
2268
+ test('private shuttle checkout receipt labels add-on variants and quantities', () => {
2269
+ const artifacts = buildPrivateShuttleCheckoutArtifacts({
2270
+ resourceCount: 1,
2271
+ resourcePrice: 2199.99,
2272
+ basePrice: 2199.99,
2273
+ addOnSelections: [
2274
+ { addOnId: 'addon_croissants', variantId: 'butter', quantity: 2 },
2275
+ ],
2276
+ addOns: [
2277
+ {
2278
+ addOnId: 'addon_croissants',
2279
+ name: 'Croissants',
2280
+ price: 5.99,
2281
+ currency: 'CAD',
2282
+ preCheckout: true,
2283
+ variantType: 'multi_quantity',
2284
+ variants: [{ id: 'butter', label: 'Butter Croissant', priceAdjustment: 0 }],
2285
+ productOptionIds: [],
2286
+ },
2287
+ ],
2288
+ additionalHoursAmount: 0,
2289
+ additionalHoursCount: 0,
2290
+ perBookingFeeLineItems: [],
2291
+ cancellationPolicyFee: 0,
2292
+ effectiveTaxAmount: 0,
2293
+ taxLabel: 'Taxes and fees',
2294
+ effectivePromoDiscountAmount: 0,
2295
+ discountLabel: 'Discount',
2296
+ promoLineType: 'PROMO_CODE',
2297
+ depositInfo: null,
2298
+ totalPrice: 2211.97,
2299
+ currency: 'CAD',
2300
+ pricingV2Quote: null,
2301
+ roundingLabel: 'Rounding adjustment',
2302
+ });
2303
+
2304
+ assert.deepEqual(artifacts.feeLineItemsForModal, [
2305
+ { name: 'Croissants (Butter Croissant) \u00d7 2', totalAmount: 11.98 },
2306
+ ]);
2307
+ });
@@ -0,0 +1,27 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ EMERALD_LAKE_LUNCH_ADD_ON_ID,
5
+ WILD_FLOUR_LUNCH_ADD_ON_ID,
6
+ privateShuttleHiddenLunchAddOnId,
7
+ privateShuttleLunchAddOnIsVisible,
8
+ } from '../src/components/booking/private-shuttle-lunch-visibility';
9
+
10
+ test('private shuttle shows only Emerald Lake lunch when Emerald Lake is selected', () => {
11
+ const destinations = ['moraine_lake', 'emerald_lake'];
12
+ assert.equal(privateShuttleLunchAddOnIsVisible(EMERALD_LAKE_LUNCH_ADD_ON_ID, destinations), true);
13
+ assert.equal(privateShuttleLunchAddOnIsVisible(WILD_FLOUR_LUNCH_ADD_ON_ID, destinations), false);
14
+ assert.equal(privateShuttleHiddenLunchAddOnId(destinations), WILD_FLOUR_LUNCH_ADD_ON_ID);
15
+ });
16
+
17
+ test('private shuttle shows only Wild Flour lunch without Emerald Lake', () => {
18
+ const destinations = ['moraine_lake', 'lake_louise'];
19
+ assert.equal(privateShuttleLunchAddOnIsVisible(EMERALD_LAKE_LUNCH_ADD_ON_ID, destinations), false);
20
+ assert.equal(privateShuttleLunchAddOnIsVisible(WILD_FLOUR_LUNCH_ADD_ON_ID, destinations), true);
21
+ assert.equal(privateShuttleHiddenLunchAddOnId(destinations), EMERALD_LAKE_LUNCH_ADD_ON_ID);
22
+ });
23
+
24
+ test('private shuttle leaves non-lunch add-ons visible', () => {
25
+ assert.equal(privateShuttleLunchAddOnIsVisible('addon_animals', ['emerald_lake']), true);
26
+ assert.equal(privateShuttleLunchAddOnIsVisible('addon_breakfast', []), true);
27
+ });