@ticketboothapp/booking 1.2.123 → 1.2.125

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 (112) hide show
  1. package/CHANGE_BOOKING_BE_HANDOFF.md +15 -4
  2. package/package.json +1 -1
  3. package/src/components/booking/AdminChangeBookingContent.tsx +276 -0
  4. package/src/components/booking/AdminChangeBookingFlow.tsx +651 -6202
  5. package/src/components/booking/AdminChangeCheckoutDialogs.tsx +105 -0
  6. package/src/components/booking/AdminChangeCheckoutPanel.tsx +307 -0
  7. package/src/components/booking/AdminChangePricingPanel.tsx +244 -0
  8. package/src/components/booking/AdminChangeProductSwitcher.tsx +50 -0
  9. package/src/components/booking/AdminChangeReceiptComparison.tsx +167 -0
  10. package/src/components/booking/ChangeBookingCalendarPanel.tsx +126 -0
  11. package/src/components/booking/ChangeBookingCheckoutPanel.tsx +204 -0
  12. package/src/components/booking/ChangeBookingFlow.tsx +576 -4648
  13. package/src/components/booking/ChangeBookingItineraryPanel.tsx +115 -0
  14. package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +57 -0
  15. package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +157 -0
  16. package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +102 -0
  17. package/src/components/booking/CheckoutModal.tsx +68 -49
  18. package/src/components/booking/NewBookingFlow.tsx +485 -3505
  19. package/src/components/booking/PriceBreakdown.tsx +17 -3
  20. package/src/components/booking/PriceSummary.tsx +21 -3
  21. package/src/components/booking/PrivateShuttleAddOnsSection.tsx +102 -0
  22. package/src/components/booking/PrivateShuttleBookingFlow.tsx +407 -2788
  23. package/src/components/booking/PrivateShuttleCheckoutDialogs.tsx +106 -0
  24. package/src/components/booking/PrivateShuttleCheckoutSection.tsx +333 -0
  25. package/src/components/booking/PrivateShuttleItinerarySection.tsx +95 -0
  26. package/src/components/booking/PrivateShuttleMediaIntro.tsx +62 -0
  27. package/src/components/booking/PrivateShuttleOptionSection.tsx +81 -0
  28. package/src/components/booking/PrivateShuttlePassengerSection.tsx +148 -0
  29. package/src/components/booking/PrivateShuttlePickupSection.tsx +91 -0
  30. package/src/components/booking/PrivateShuttlePreferencesSection.tsx +75 -0
  31. package/src/components/booking/PrivateShuttleStartTimeSection.tsx +88 -0
  32. package/src/components/booking/StandardBookingCheckoutDialogs.tsx +106 -0
  33. package/src/components/booking/StandardBookingCheckoutSection.tsx +185 -0
  34. package/src/components/booking/StandardBookingItineraryPanel.tsx +125 -0
  35. package/src/components/booking/StandardBookingMediaIntro.tsx +69 -0
  36. package/src/components/booking/StandardBookingSelectionControlsPanel.tsx +236 -0
  37. package/src/components/booking/admin-change-customer-quote-runner.ts +119 -0
  38. package/src/components/booking/admin-change-flow-state-helpers.ts +124 -0
  39. package/src/components/booking/admin-change-payment-choice-runner.ts +172 -0
  40. package/src/components/booking/admin-change-provider-payload.ts +101 -0
  41. package/src/components/booking/change-booking-checkout-builders.ts +185 -0
  42. package/src/components/booking/change-booking-checkout-quote-runner.ts +207 -0
  43. package/src/components/booking/change-booking-flow-helpers.ts +706 -0
  44. package/src/components/booking/change-booking-payment-modal-builders.ts +151 -0
  45. package/src/components/booking/change-booking-quote-guards.ts +223 -0
  46. package/src/components/booking/change-booking-quote-state.ts +64 -0
  47. package/src/components/booking/pricing-v2-checkout-summary.ts +144 -0
  48. package/src/components/booking/private-shuttle-availability.ts +111 -0
  49. package/src/components/booking/private-shuttle-checkout-builders.ts +264 -0
  50. package/src/components/booking/private-shuttle-checkout-controller.ts +744 -0
  51. package/src/components/booking/private-shuttle-payment-choice-runner.ts +271 -0
  52. package/src/components/booking/private-shuttle-provider-change-runner.ts +73 -0
  53. package/src/components/booking/private-shuttle-reservation-runner.ts +260 -0
  54. package/src/components/booking/standard-booking-availability.ts +305 -0
  55. package/src/components/booking/standard-booking-checkout-builders.ts +289 -0
  56. package/src/components/booking/standard-booking-checkout-controller.ts +659 -0
  57. package/src/components/booking/standard-booking-payment-choice-runner.ts +274 -0
  58. package/src/components/booking/standard-booking-reservation-runner.ts +229 -0
  59. package/src/components/booking/use-private-shuttle-availability.ts +602 -0
  60. package/src/components/booking/use-standard-booking-availability.ts +806 -0
  61. package/src/components/booking/useActiveProductOptions.ts +26 -0
  62. package/src/components/booking/useAdminChangeCheckoutController.ts +806 -0
  63. package/src/components/booking/useAdminChangeOrderSummaryModel.ts +246 -0
  64. package/src/components/booking/useAdminChangePriceSummary.ts +416 -0
  65. package/src/components/booking/useAdminChangePricingDebugPanel.tsx +178 -0
  66. package/src/components/booking/useAdminChangeProductReset.ts +60 -0
  67. package/src/components/booking/useAdminChangeProducts.ts +102 -0
  68. package/src/components/booking/useAdminChangePromoDiscount.ts +235 -0
  69. package/src/components/booking/useAdminChangeProtectedPricing.ts +280 -0
  70. package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +437 -0
  71. package/src/components/booking/useAdminChangeQuotePreview.ts +506 -0
  72. package/src/components/booking/useAdminChangeReceiptDerivation.ts +203 -0
  73. package/src/components/booking/useAdminCustomReceiptLines.ts +57 -0
  74. package/src/components/booking/useAdminProviderPricingAdjustments.ts +146 -0
  75. package/src/components/booking/useBookingAvailabilityAddOns.ts +54 -0
  76. package/src/components/booking/useBookingPromoAndQuantityController.ts +163 -0
  77. package/src/components/booking/useBookingQuantityCapTrim.ts +56 -0
  78. package/src/components/booking/useBookingViewItemAnalytics.ts +30 -0
  79. package/src/components/booking/useChangeBookingAddOnFloorController.ts +109 -0
  80. package/src/components/booking/useChangeBookingAutoSelections.ts +148 -0
  81. package/src/components/booking/useChangeBookingAvailabilities.ts +692 -0
  82. package/src/components/booking/useChangeBookingCheckoutController.ts +461 -0
  83. package/src/components/booking/useChangeBookingInitialHydration.ts +204 -0
  84. package/src/components/booking/useChangeBookingItineraryUiState.ts +133 -0
  85. package/src/components/booking/useChangeBookingPickupItinerary.ts +472 -0
  86. package/src/components/booking/useChangeBookingPriceSummary.ts +199 -0
  87. package/src/components/booking/useChangeBookingPromoDiscount.ts +218 -0
  88. package/src/components/booking/useChangeBookingProtectedPricing.ts +256 -0
  89. package/src/components/booking/useChangeBookingQuotePreview.ts +442 -0
  90. package/src/components/booking/useChangeBookingReceiptDerivation.ts +128 -0
  91. package/src/components/booking/useChangeBookingReceiptFloorController.ts +449 -0
  92. package/src/components/booking/useChangeBookingSelection.ts +362 -0
  93. package/src/components/booking/useChangeBookingSelectionDetails.ts +200 -0
  94. package/src/components/booking/useChangeBookingSummaryLines.ts +169 -0
  95. package/src/components/booking/usePrivateShuttlePriceSummary.ts +513 -0
  96. package/src/components/booking/usePrivateShuttlePromoController.ts +143 -0
  97. package/src/components/booking/usePrivateShuttlePromoDiscount.ts +124 -0
  98. package/src/components/booking/useSeedPromoCodeInput.ts +13 -0
  99. package/src/components/booking/useStandardBookingAutoSelections.ts +224 -0
  100. package/src/components/booking/useStandardBookingPickupItinerary.ts +473 -0
  101. package/src/components/booking/useStandardBookingPriceSummary.ts +438 -0
  102. package/src/components/booking/useStandardBookingPromoController.ts +258 -0
  103. package/src/components/booking/useStandardBookingPromoDiscount.ts +203 -0
  104. package/src/data/products-config.json +1 -1
  105. package/src/lib/booking/change-booking-server-preview.ts +134 -20
  106. package/src/lib/booking/change-flow-pricing.ts +31 -11
  107. package/src/lib/booking/checkout-breakdown.ts +66 -1
  108. package/src/lib/booking/pricing-config.ts +10 -0
  109. package/src/lib/booking/source-metadata.ts +58 -0
  110. package/src/lib/booking-api.ts +155 -3
  111. package/src/lib/pricing-v2-shadow.ts +272 -0
  112. package/test/change-booking-helpers.test.ts +206 -0
@@ -0,0 +1,115 @@
1
+ import type { RefObject } from 'react';
2
+ import { format, parseISO } from 'date-fns';
3
+ import type {
4
+ Availability,
5
+ ItineraryDisplayStep,
6
+ PickupLocation,
7
+ ProductOption,
8
+ ReturnOption,
9
+ } from '../../lib/booking-api';
10
+ import { ItineraryBox } from './ItineraryBox';
11
+ import { ItineraryPlaceholder } from './ItineraryPlaceholder';
12
+
13
+ type TranslationFn = (key: string, params?: Record<string, string>) => string;
14
+
15
+ export interface ChangeBookingItineraryPanelProps {
16
+ selectedDate: string;
17
+ hideItineraryBox: boolean;
18
+ activeOptions: ProductOption[];
19
+ destinationsCount: number;
20
+ selectedAvailability: Availability | null;
21
+ selectedReturnOption: ReturnOption | null;
22
+ pickupLocationId: string | null;
23
+ pickupLocationSkipped: boolean;
24
+ pickupLocationsCount: number;
25
+ quantities: Record<string, number>;
26
+ email: string;
27
+ firstName: string;
28
+ lastName: string;
29
+ getItineraryItems: () => ItineraryDisplayStep[] | null;
30
+ isItinerarySticky: boolean;
31
+ stickyTopPx?: number;
32
+ isMobile: boolean;
33
+ useWindowScroll: boolean;
34
+ showTooltip: boolean;
35
+ selectedPickupLocation?: PickupLocation | null;
36
+ itineraryRef: RefObject<HTMLDivElement | null>;
37
+ t: TranslationFn;
38
+ onTooltipToggle: () => void;
39
+ onTooltipShow: (show: boolean) => void;
40
+ }
41
+
42
+ export function ChangeBookingItineraryPanel({
43
+ selectedDate,
44
+ hideItineraryBox,
45
+ activeOptions,
46
+ destinationsCount,
47
+ selectedAvailability,
48
+ selectedReturnOption,
49
+ pickupLocationId,
50
+ pickupLocationSkipped,
51
+ pickupLocationsCount,
52
+ quantities,
53
+ email,
54
+ firstName,
55
+ lastName,
56
+ getItineraryItems,
57
+ isItinerarySticky,
58
+ stickyTopPx,
59
+ isMobile,
60
+ useWindowScroll,
61
+ showTooltip,
62
+ selectedPickupLocation,
63
+ itineraryRef,
64
+ t,
65
+ onTooltipToggle,
66
+ onTooltipShow,
67
+ }: ChangeBookingItineraryPanelProps) {
68
+ if (!selectedDate || hideItineraryBox) return null;
69
+
70
+ const hasItineraryAny =
71
+ activeOptions.some((option) => option.itinerary?.length) && destinationsCount > 0;
72
+ if (!hasItineraryAny) return null;
73
+
74
+ const formattedDate = format(parseISO(selectedDate), 'MMM d');
75
+ if (!selectedAvailability) {
76
+ return <ItineraryPlaceholder formattedDate={formattedDate} t={t} />;
77
+ }
78
+
79
+ const itineraryItems = getItineraryItems();
80
+ if (!itineraryItems || itineraryItems.length === 0) return null;
81
+
82
+ const hasTickets = Object.values(quantities).some((qty) => qty > 0);
83
+ const hasPickupChoice =
84
+ Boolean(pickupLocationId) || pickupLocationSkipped || pickupLocationsCount === 0;
85
+ const isBookingComplete = Boolean(
86
+ selectedAvailability &&
87
+ selectedReturnOption &&
88
+ hasPickupChoice &&
89
+ hasTickets &&
90
+ email.trim() !== '' &&
91
+ firstName.trim() !== '' &&
92
+ lastName.trim() !== '',
93
+ );
94
+
95
+ return (
96
+ <ItineraryBox
97
+ selectedDate={selectedDate}
98
+ formattedDate={formattedDate}
99
+ itineraryItems={itineraryItems}
100
+ isBookingComplete={isBookingComplete}
101
+ isItinerarySticky={isItinerarySticky}
102
+ stickyTopPx={stickyTopPx}
103
+ isMobile={isMobile}
104
+ useWindowScroll={useWindowScroll}
105
+ showTooltip={showTooltip}
106
+ selectedPickupLocation={selectedPickupLocation}
107
+ pickupLocationSkipped={pickupLocationSkipped}
108
+ pickupLocationsCount={pickupLocationsCount}
109
+ itineraryRef={itineraryRef}
110
+ t={t}
111
+ onTooltipToggle={onTooltipToggle}
112
+ onTooltipShow={onTooltipShow}
113
+ />
114
+ );
115
+ }
@@ -0,0 +1,57 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface ChangeBookingQuoteStatusPlaceholderParams {
4
+ suppressSelfServeCurrencyUi: boolean;
5
+ hasSelectedAvailability: boolean;
6
+ totalQuantity: number;
7
+ selfServePricingConfirmed: boolean;
8
+ changeQuoteLoading: boolean;
9
+ changeQuoteFetchError?: string | null;
10
+ quoteCanProceed?: boolean;
11
+ quoteBlockedReason?: string | null;
12
+ blockedDetails?: ReactNode;
13
+ t: (key: string) => string;
14
+ }
15
+
16
+ export function buildChangeBookingQuoteStatusPlaceholder({
17
+ suppressSelfServeCurrencyUi,
18
+ hasSelectedAvailability,
19
+ totalQuantity,
20
+ selfServePricingConfirmed,
21
+ changeQuoteLoading,
22
+ changeQuoteFetchError,
23
+ quoteCanProceed,
24
+ quoteBlockedReason,
25
+ blockedDetails,
26
+ t,
27
+ }: ChangeBookingQuoteStatusPlaceholderParams): ReactNode | undefined {
28
+ if (!suppressSelfServeCurrencyUi || !hasSelectedAvailability || totalQuantity <= 0) return undefined;
29
+ if (selfServePricingConfirmed) return undefined;
30
+ if (changeQuoteLoading) {
31
+ return (
32
+ <div className="rounded-lg border border-stone-200 bg-stone-50 px-4 py-3 text-sm text-stone-600">
33
+ {t('booking.updatingPrice') !== 'booking.updatingPrice' ? t('booking.updatingPrice') : <>Getting confirmed price&hellip;</>}
34
+ </div>
35
+ );
36
+ }
37
+ if (changeQuoteFetchError) {
38
+ return (
39
+ <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" role="alert">
40
+ {changeQuoteFetchError}
41
+ </div>
42
+ );
43
+ }
44
+ if (quoteCanProceed === false) {
45
+ return (
46
+ <div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950">
47
+ <div>{quoteBlockedReason ?? 'This booking change is not available.'}</div>
48
+ {blockedDetails}
49
+ </div>
50
+ );
51
+ }
52
+ return (
53
+ <div className="rounded-lg border border-stone-200 bg-stone-50 px-4 py-3 text-sm text-stone-600">
54
+ Prices update once your selection is confirmed with our booking system.
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,157 @@
1
+ import type { AddOn, Availability, ProductOption, ReturnOption } from '../../lib/booking-api';
2
+ import type { AddOnSelection } from './AddOnsSection';
3
+ import type { Currency } from './CurrencySwitcher';
4
+ import { ChangeBookingTicketsAndAddOnsPanel } from './ChangeBookingTicketsAndAddOnsPanel';
5
+ import { PickupTimeSelector, type PickupTimeInfo } from './PickupTimeSelector';
6
+ import { ReturnTimeSelector } from './ReturnTimeSelector';
7
+ import type { TicketRate } from './TicketSelector';
8
+
9
+ type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
10
+
11
+ type AddOnSelectionsChangeHandler = (
12
+ selections: AddOnSelection[] | ((prev: AddOnSelection[]) => AddOnSelection[]),
13
+ ) => void;
14
+
15
+ export interface ChangeBookingSelectionControlsPanelProps {
16
+ selectedDate: string;
17
+ selectedAvailability: Availability | null;
18
+ pickupTimes: PickupTimeInfo[];
19
+ optionsMap: Map<string, ProductOption>;
20
+ hasAnyMostPopular: boolean;
21
+ isAdmin: boolean;
22
+ pickupLocationSkipped: boolean;
23
+ t: TranslationFn;
24
+ onTimeSelect: (availability: Availability) => void;
25
+ returnOptions: ReturnOption[];
26
+ selectedReturnOption: ReturnOption | null;
27
+ selectedReturnOptionForDisplay: ReturnOption | null;
28
+ onSelectedReturnOptionChange: (option: ReturnOption) => void;
29
+ getStaySummary: (returnDateTime: Date) => string | null;
30
+ pricing: TicketRate[];
31
+ quantities: Record<string, number>;
32
+ totalQuantity: number;
33
+ selectedVacancies: number;
34
+ companyTimezone: string;
35
+ pickupVacancies: number;
36
+ returnVacancies: number | null;
37
+ currency: Currency;
38
+ locale: string;
39
+ isSimplifiedPricingView: boolean;
40
+ onQuantityChange: (category: string, delta: number) => void;
41
+ minimumQuantities?: Record<string, number>;
42
+ applyReceiptPaidFloors: boolean;
43
+ ticketUnitFloorByCategory?: Map<string, number>;
44
+ addOns: AddOn[];
45
+ addOnSelections: AddOnSelection[];
46
+ onAddOnSelectionsChange: AddOnSelectionsChangeHandler;
47
+ minimumTotalByAddOnId?: Map<string, number>;
48
+ suppressUnitPrices?: boolean;
49
+ suppressReturnPerPersonPrices?: boolean;
50
+ suppressAddOnPrices?: boolean;
51
+ }
52
+
53
+ export function ChangeBookingSelectionControlsPanel({
54
+ selectedDate,
55
+ selectedAvailability,
56
+ pickupTimes,
57
+ optionsMap,
58
+ hasAnyMostPopular,
59
+ isAdmin,
60
+ pickupLocationSkipped,
61
+ t,
62
+ onTimeSelect,
63
+ returnOptions,
64
+ selectedReturnOption,
65
+ selectedReturnOptionForDisplay,
66
+ onSelectedReturnOptionChange,
67
+ getStaySummary,
68
+ pricing,
69
+ quantities,
70
+ totalQuantity,
71
+ selectedVacancies,
72
+ companyTimezone,
73
+ pickupVacancies,
74
+ returnVacancies,
75
+ currency,
76
+ locale,
77
+ isSimplifiedPricingView,
78
+ onQuantityChange,
79
+ minimumQuantities,
80
+ applyReceiptPaidFloors,
81
+ ticketUnitFloorByCategory,
82
+ addOns,
83
+ addOnSelections,
84
+ onAddOnSelectionsChange,
85
+ minimumTotalByAddOnId,
86
+ suppressUnitPrices = false,
87
+ suppressReturnPerPersonPrices = false,
88
+ suppressAddOnPrices = false,
89
+ }: ChangeBookingSelectionControlsPanelProps) {
90
+ return (
91
+ <>
92
+ {selectedDate ? (
93
+ <PickupTimeSelector
94
+ pickupTimes={pickupTimes}
95
+ selectedDateTime={selectedAvailability?.dateTime ?? null}
96
+ selectedTicketCount={totalQuantity}
97
+ optionsMap={optionsMap}
98
+ hasAnyMostPopular={hasAnyMostPopular}
99
+ isAdmin={isAdmin}
100
+ pickupLocationSkipped={pickupLocationSkipped}
101
+ t={t}
102
+ onTimeSelect={onTimeSelect}
103
+ />
104
+ ) : null}
105
+
106
+ {selectedAvailability?.returnOptions && selectedAvailability.returnOptions.length > 0 ? (
107
+ <ReturnTimeSelector
108
+ returnOptions={returnOptions}
109
+ selectedReturnOption={selectedReturnOptionForDisplay}
110
+ selectedTicketCount={totalQuantity}
111
+ companyTimezone={companyTimezone}
112
+ currency={currency}
113
+ locale={locale}
114
+ isAdmin={isAdmin}
115
+ t={t}
116
+ onReturnSelect={(option) => {
117
+ const raw = selectedAvailability.returnOptions?.find(
118
+ (opt) => opt.returnAvailabilityId === option.returnAvailabilityId,
119
+ );
120
+ onSelectedReturnOptionChange(raw ?? option);
121
+ }}
122
+ getStaySummary={getStaySummary}
123
+ suppressPerPersonPrices={suppressReturnPerPersonPrices}
124
+ />
125
+ ) : null}
126
+
127
+ {selectedAvailability ? (
128
+ <ChangeBookingTicketsAndAddOnsPanel
129
+ selectedAvailability={selectedAvailability}
130
+ pricing={pricing}
131
+ quantities={quantities}
132
+ totalQuantity={totalQuantity}
133
+ selectedVacancies={selectedVacancies}
134
+ companyTimezone={companyTimezone}
135
+ pickupVacancies={pickupVacancies}
136
+ selectedReturnOption={selectedReturnOption}
137
+ returnVacancies={returnVacancies}
138
+ currency={currency}
139
+ locale={locale}
140
+ isAdmin={isAdmin}
141
+ isSimplifiedPricingView={isSimplifiedPricingView}
142
+ t={t}
143
+ onQuantityChange={onQuantityChange}
144
+ minimumQuantities={minimumQuantities}
145
+ applyReceiptPaidFloors={applyReceiptPaidFloors}
146
+ ticketUnitFloorByCategory={ticketUnitFloorByCategory}
147
+ suppressUnitPrices={suppressUnitPrices}
148
+ addOns={addOns}
149
+ addOnSelections={addOnSelections}
150
+ onAddOnSelectionsChange={onAddOnSelectionsChange}
151
+ minimumTotalByAddOnId={minimumTotalByAddOnId}
152
+ suppressAddOnPrices={suppressAddOnPrices}
153
+ />
154
+ ) : null}
155
+ </>
156
+ );
157
+ }
@@ -0,0 +1,102 @@
1
+ import type { AddOn, Availability, ReturnOption } from '../../lib/booking-api';
2
+ import { AddOnsSection, type AddOnSelection } from './AddOnsSection';
3
+ import type { Currency } from './CurrencySwitcher';
4
+ import { TicketSelector, type TicketRate } from './TicketSelector';
5
+
6
+ type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
7
+
8
+ type AddOnSelectionsChangeHandler = (
9
+ selections: AddOnSelection[] | ((prev: AddOnSelection[]) => AddOnSelection[]),
10
+ ) => void;
11
+
12
+ export interface ChangeBookingTicketsAndAddOnsPanelProps {
13
+ selectedAvailability: Availability;
14
+ pricing: TicketRate[];
15
+ quantities: Record<string, number>;
16
+ totalQuantity: number;
17
+ selectedVacancies: number;
18
+ companyTimezone: string;
19
+ pickupVacancies: number;
20
+ selectedReturnOption?: ReturnOption | null;
21
+ returnVacancies: number | null;
22
+ currency: Currency;
23
+ locale: string;
24
+ isAdmin: boolean;
25
+ isSimplifiedPricingView: boolean;
26
+ t: TranslationFn;
27
+ onQuantityChange: (category: string, delta: number) => void;
28
+ minimumQuantities?: Record<string, number>;
29
+ applyReceiptPaidFloors: boolean;
30
+ ticketUnitFloorByCategory?: Map<string, number>;
31
+ addOns: AddOn[];
32
+ addOnSelections: AddOnSelection[];
33
+ onAddOnSelectionsChange: AddOnSelectionsChangeHandler;
34
+ minimumTotalByAddOnId?: Map<string, number>;
35
+ suppressUnitPrices?: boolean;
36
+ suppressAddOnPrices?: boolean;
37
+ }
38
+
39
+ export function ChangeBookingTicketsAndAddOnsPanel({
40
+ selectedAvailability,
41
+ pricing,
42
+ quantities,
43
+ totalQuantity,
44
+ selectedVacancies,
45
+ companyTimezone,
46
+ pickupVacancies,
47
+ selectedReturnOption,
48
+ returnVacancies,
49
+ currency,
50
+ locale,
51
+ isAdmin,
52
+ isSimplifiedPricingView,
53
+ t,
54
+ onQuantityChange,
55
+ minimumQuantities,
56
+ applyReceiptPaidFloors,
57
+ ticketUnitFloorByCategory,
58
+ addOns,
59
+ addOnSelections,
60
+ onAddOnSelectionsChange,
61
+ minimumTotalByAddOnId,
62
+ suppressUnitPrices = false,
63
+ suppressAddOnPrices = false,
64
+ }: ChangeBookingTicketsAndAddOnsPanelProps) {
65
+ return (
66
+ <>
67
+ <TicketSelector
68
+ pricing={pricing}
69
+ quantities={quantities}
70
+ totalQuantity={totalQuantity}
71
+ selectedVacancies={selectedVacancies}
72
+ companyTimezone={companyTimezone}
73
+ pickupDateTime={selectedAvailability.dateTime}
74
+ pickupVacancies={pickupVacancies}
75
+ returnDateTime={selectedReturnOption?.dateTime ?? null}
76
+ returnVacancies={returnVacancies}
77
+ resourceCount={selectedReturnOption ? null : (selectedAvailability.resourceCount ?? null)}
78
+ currency={currency}
79
+ locale={locale}
80
+ isAdmin={isAdmin}
81
+ isSimplifiedPricingView={isSimplifiedPricingView}
82
+ t={t}
83
+ onQuantityChange={onQuantityChange}
84
+ minimumQuantities={minimumQuantities}
85
+ ticketUnitFloorByCategory={applyReceiptPaidFloors ? ticketUnitFloorByCategory : undefined}
86
+ suppressUnitPrices={suppressUnitPrices}
87
+ />
88
+
89
+ {totalQuantity > 0 && addOns.length > 0 ? (
90
+ <AddOnsSection
91
+ addOns={addOns}
92
+ addOnSelections={addOnSelections}
93
+ currency={currency}
94
+ locale={locale}
95
+ onSelectionsChange={onAddOnSelectionsChange}
96
+ minimumTotalByAddOnId={minimumTotalByAddOnId}
97
+ suppressPrices={suppressAddOnPrices}
98
+ />
99
+ ) : null}
100
+ </>
101
+ );
102
+ }
@@ -15,6 +15,15 @@ export interface CheckoutModalLineItem {
15
15
  breakdown: PriceBreakdownType | null;
16
16
  }
17
17
 
18
+ export interface CheckoutModalSummaryOverride {
19
+ lines: PriceSummaryLine[];
20
+ subtotal?: number;
21
+ taxAmount?: number;
22
+ discountAmount?: number;
23
+ discountLabel?: string | null;
24
+ fullTotalAmount?: number;
25
+ }
26
+
18
27
  export interface CheckoutModalProps {
19
28
  open: boolean;
20
29
  onClose: () => void;
@@ -37,6 +46,7 @@ export interface CheckoutModalProps {
37
46
  total: number;
38
47
  promoDiscountAmount?: number;
39
48
  discountLabel?: string | null;
49
+ summaryOverride?: CheckoutModalSummaryOverride;
40
50
  totalQuantity: number;
41
51
  isTaxIncludedInPrice: boolean;
42
52
  taxRate?: number;
@@ -161,6 +171,7 @@ export function CheckoutModal({
161
171
  totalQuantity,
162
172
  promoDiscountAmount = 0,
163
173
  discountLabel,
174
+ summaryOverride,
164
175
  isTaxIncludedInPrice,
165
176
  taxRate = 0,
166
177
  currency,
@@ -182,6 +193,57 @@ export function CheckoutModal({
182
193
  if (customerLastName?.trim()) manageParams.set('lastName', customerLastName.trim());
183
194
  if (!successUrlOverride) manageParams.set('booking_complete', '1');
184
195
  const successUrl = successUrlOverride ?? `${baseUrl}/manage-booking?${manageParams.toString()}`;
196
+ const summaryLines: PriceSummaryLine[] =
197
+ summaryOverride?.lines ??
198
+ [
199
+ ...ticketLines.map(({ line, breakdown }): PriceSummaryLine => ({
200
+ kind: 'ticket',
201
+ category: line.category,
202
+ qty: line.qty,
203
+ itemTotal: line.itemTotal,
204
+ breakdown,
205
+ })),
206
+ ...(returnPriceAdjustment !== 0
207
+ ? [
208
+ {
209
+ kind: 'line' as const,
210
+ label: `${t('booking.returnOption')} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`,
211
+ amount: returnPriceAdjustment,
212
+ type: 'return',
213
+ },
214
+ ]
215
+ : []),
216
+ ...(cancellationPolicyFee > 0
217
+ ? [
218
+ {
219
+ kind: 'line' as const,
220
+ label: cancellationPolicyLabel ?? t('booking.flexibleCancellation') ?? 'Flexible cancellation',
221
+ amount: cancellationPolicyFee,
222
+ type: 'cancellation',
223
+ },
224
+ ]
225
+ : []),
226
+ ...feeLineItems.map((fee) => {
227
+ const isMoraineLakeRoadAccessFee = fee.name.toLowerCase().includes('moraine') && (fee.name.toLowerCase().includes('access') || fee.name.toLowerCase().includes('road') || fee.name.toLowerCase().includes('license'));
228
+ const showQuantityLabel = fee.showQuantityLabel !== false;
229
+ return {
230
+ kind: 'line' as const,
231
+ label: showQuantityLabel
232
+ ? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
233
+ : fee.name,
234
+ amount: fee.totalAmount,
235
+ type: 'fee',
236
+ tooltip: isMoraineLakeRoadAccessFee
237
+ ? "Since 2025, Parks Canada charges a per-trip fee for License of Occupation. Based on our capacity, this per-person fee contributes towards Parks Canada's Moraine Lake Road operations."
238
+ : undefined,
239
+ };
240
+ }),
241
+ ];
242
+ const summarySubtotal = summaryOverride?.subtotal ?? subtotal;
243
+ const summaryTaxAmount = summaryOverride?.taxAmount ?? (!isTaxIncludedInPrice && tax > 0 ? tax : 0);
244
+ const summaryDiscountAmount = summaryOverride?.discountAmount ?? promoDiscountAmount ?? 0;
245
+ const summaryDiscountLabel = summaryOverride?.discountLabel ?? discountLabel;
246
+ const summaryFullTotalAmount = summaryOverride?.fullTotalAmount ?? subtotal;
185
247
 
186
248
  const hasFiredBeginCheckout = useRef(false);
187
249
  /** Ensures we only fire expiry close once per modal open (avoids Strict Mode / repeated 0 remainingMs). */
@@ -304,61 +366,18 @@ export function CheckoutModal({
304
366
  {/* Order summary — shared PriceSummary component */}
305
367
  <div className="mt-4 min-w-0">
306
368
  <PriceSummary
307
- lines={[
308
- ...ticketLines.map(({ line, breakdown }): PriceSummaryLine => ({
309
- kind: 'ticket',
310
- category: line.category,
311
- qty: line.qty,
312
- itemTotal: line.itemTotal,
313
- breakdown,
314
- })),
315
- ...(returnPriceAdjustment !== 0
316
- ? [
317
- {
318
- kind: 'line' as const,
319
- label: `${t('booking.returnOption')} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`,
320
- amount: returnPriceAdjustment,
321
- type: 'return',
322
- },
323
- ]
324
- : []),
325
- ...(cancellationPolicyFee > 0
326
- ? [
327
- {
328
- kind: 'line' as const,
329
- label: cancellationPolicyLabel ?? t('booking.flexibleCancellation') ?? 'Flexible cancellation',
330
- amount: cancellationPolicyFee,
331
- type: 'cancellation',
332
- },
333
- ]
334
- : []),
335
- ...feeLineItems.map((fee) => {
336
- const isMoraineLakeRoadAccessFee = fee.name.toLowerCase().includes('moraine') && (fee.name.toLowerCase().includes('access') || fee.name.toLowerCase().includes('road') || fee.name.toLowerCase().includes('license'));
337
- const showQuantityLabel = fee.showQuantityLabel !== false;
338
- return {
339
- kind: 'line' as const,
340
- label: showQuantityLabel
341
- ? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
342
- : fee.name,
343
- amount: fee.totalAmount,
344
- type: 'fee',
345
- tooltip: isMoraineLakeRoadAccessFee
346
- ? "Since 2025, Parks Canada charges a per-trip fee for License of Occupation. Based on our capacity, this per-person fee contributes towards Parks Canada's Moraine Lake Road operations."
347
- : undefined,
348
- };
349
- }),
350
- ]}
369
+ lines={summaryLines}
351
370
  total={total}
352
371
  currency={currency}
353
372
  locale={locale}
354
- subtotal={subtotal !== total ? subtotal : undefined}
355
- discountAmount={promoDiscountAmount ?? 0}
356
- discountLabel={discountLabel}
357
- taxAmount={!isTaxIncludedInPrice && tax > 0 ? tax : 0}
373
+ subtotal={summarySubtotal !== total ? summarySubtotal : undefined}
374
+ discountAmount={summaryDiscountAmount}
375
+ discountLabel={summaryDiscountLabel}
376
+ taxAmount={summaryTaxAmount}
358
377
  taxRate={taxRate}
359
378
  size="sm"
360
379
  t={t}
361
- depositMode={isDepositPayment && subtotal > total ? { totalLabel: t('booking.deposit') || 'Deposit', balanceAmount: subtotal - total, fullTotalAmount: subtotal } : undefined}
380
+ depositMode={isDepositPayment && summaryFullTotalAmount > total ? { totalLabel: t('booking.deposit') || 'Deposit', balanceAmount: summaryFullTotalAmount - total, fullTotalAmount: summaryFullTotalAmount } : undefined}
362
381
  hideSubtotal={isDepositPayment}
363
382
  extraBetweenTaxAndTotal={
364
383
  changeTotals ? (