@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,125 @@
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 type { NewBookingFlowProps } from './booking-flow-types';
11
+ import { ItineraryBox } from './ItineraryBox';
12
+ import { ItineraryPlaceholder } from './ItineraryPlaceholder';
13
+
14
+ type TranslationFn = (key: string, params?: Record<string, string>) => string;
15
+
16
+ export interface StandardBookingItineraryPanelProps {
17
+ selectedDate: string;
18
+ hideItineraryBox: boolean;
19
+ activeOptions: ProductOption[];
20
+ destinationsCount: number;
21
+ selectedAvailability: Availability | null;
22
+ selectedReturnOption: ReturnOption | null;
23
+ pickupLocationId: string | null;
24
+ pickupLocationSkipped: boolean;
25
+ pickupLocationsCount: number;
26
+ quantities: Record<string, number>;
27
+ email: string;
28
+ firstName: string;
29
+ lastName: string;
30
+ getBaseItineraryItems: () => ItineraryDisplayStep[] | null;
31
+ augmentItineraryItems: NewBookingFlowProps['augmentItineraryItems'];
32
+ isItinerarySticky: boolean;
33
+ stickyTopPx?: number;
34
+ isMobile: boolean;
35
+ useWindowScroll: boolean;
36
+ showTooltip: boolean;
37
+ selectedPickupLocation?: PickupLocation | null;
38
+ itineraryRef: RefObject<HTMLDivElement | null>;
39
+ t: TranslationFn;
40
+ onTooltipToggle: () => void;
41
+ onTooltipShow: (show: boolean) => void;
42
+ }
43
+
44
+ export function StandardBookingItineraryPanel({
45
+ selectedDate,
46
+ hideItineraryBox,
47
+ activeOptions,
48
+ destinationsCount,
49
+ selectedAvailability,
50
+ selectedReturnOption,
51
+ pickupLocationId,
52
+ pickupLocationSkipped,
53
+ pickupLocationsCount,
54
+ quantities,
55
+ email,
56
+ firstName,
57
+ lastName,
58
+ getBaseItineraryItems,
59
+ augmentItineraryItems,
60
+ isItinerarySticky,
61
+ stickyTopPx,
62
+ isMobile,
63
+ useWindowScroll,
64
+ showTooltip,
65
+ selectedPickupLocation,
66
+ itineraryRef,
67
+ t,
68
+ onTooltipToggle,
69
+ onTooltipShow,
70
+ }: StandardBookingItineraryPanelProps) {
71
+ if (!selectedDate || hideItineraryBox) return null;
72
+
73
+ const hasItineraryAny =
74
+ activeOptions.some((option) => option.itinerary?.length) && destinationsCount > 0;
75
+ if (!hasItineraryAny) return null;
76
+
77
+ const formattedDate = format(parseISO(selectedDate), 'MMM d');
78
+ if (!selectedAvailability) {
79
+ return <ItineraryPlaceholder formattedDate={formattedDate} t={t} />;
80
+ }
81
+
82
+ const baseItineraryItems = getBaseItineraryItems();
83
+ const itineraryItems = augmentItineraryItems
84
+ ? augmentItineraryItems({
85
+ selectedDate,
86
+ selectedAvailability,
87
+ itineraryItems: baseItineraryItems,
88
+ })
89
+ : baseItineraryItems;
90
+ if (!itineraryItems || itineraryItems.length === 0) return null;
91
+
92
+ const hasTickets = Object.values(quantities).some((qty) => qty > 0);
93
+ const hasPickupChoice =
94
+ Boolean(pickupLocationId) || pickupLocationSkipped || pickupLocationsCount === 0;
95
+ const isBookingComplete = Boolean(
96
+ selectedAvailability &&
97
+ selectedReturnOption &&
98
+ hasPickupChoice &&
99
+ hasTickets &&
100
+ email.trim() !== '' &&
101
+ firstName.trim() !== '' &&
102
+ lastName.trim() !== '',
103
+ );
104
+
105
+ return (
106
+ <ItineraryBox
107
+ selectedDate={selectedDate}
108
+ formattedDate={formattedDate}
109
+ itineraryItems={itineraryItems}
110
+ isBookingComplete={isBookingComplete}
111
+ isItinerarySticky={isItinerarySticky}
112
+ stickyTopPx={stickyTopPx}
113
+ isMobile={isMobile}
114
+ useWindowScroll={useWindowScroll}
115
+ showTooltip={showTooltip}
116
+ selectedPickupLocation={selectedPickupLocation}
117
+ pickupLocationSkipped={pickupLocationSkipped}
118
+ pickupLocationsCount={pickupLocationsCount}
119
+ itineraryRef={itineraryRef}
120
+ t={t}
121
+ onTooltipToggle={onTooltipToggle}
122
+ onTooltipShow={onTooltipShow}
123
+ />
124
+ );
125
+ }
@@ -0,0 +1,69 @@
1
+ import { useBookingHost } from '../../runtime';
2
+ import type { Locale } from '../../lib/booking/i18n/config';
3
+ import type { VideoSources } from '../../constants/products';
4
+ import { BookingFlowCollage } from './BookingFlowCollage';
5
+ import { TourDescription } from './TourDescription';
6
+
7
+ export interface StandardBookingMediaIntroProps {
8
+ productId?: string;
9
+ productName: string;
10
+ locale: Locale;
11
+ showCollage?: boolean;
12
+ showTourDescription?: boolean;
13
+ tourDescriptionDefaultExpanded?: boolean;
14
+ }
15
+
16
+ export function StandardBookingMediaIntro({
17
+ productId,
18
+ productName,
19
+ locale,
20
+ showCollage = true,
21
+ showTourDescription = true,
22
+ tourDescriptionDefaultExpanded = false,
23
+ }: StandardBookingMediaIntroProps) {
24
+ const { strings: defaultStrings, catalog } = useBookingHost();
25
+
26
+ const config = productId
27
+ ? (catalog.getProductByIdOrSlug(productId) as {
28
+ display?: {
29
+ collageImageIds?: string[];
30
+ imageIds?: string[];
31
+ };
32
+ } | null)
33
+ : null;
34
+ const displayProducts = catalog.getProducts(defaultStrings) as Record<
35
+ string,
36
+ { id: string; videoUrl?: VideoSources }
37
+ >;
38
+ const displayProduct = Object.values(displayProducts).find((candidate) => candidate.id === productId);
39
+ const collageImageIds = config?.display?.collageImageIds ?? config?.display?.imageIds ?? [];
40
+ const hasVideo = !!displayProduct?.videoUrl;
41
+ const hasImages = collageImageIds.length > 0;
42
+
43
+ return (
44
+ <>
45
+ {productId && showCollage && (hasVideo || hasImages) && (
46
+ <div className="booking-collage-wrapper">
47
+ <BookingFlowCollage
48
+ video={displayProduct?.videoUrl}
49
+ videoPosterImageId={config?.display?.imageIds?.[0]}
50
+ imageIds={
51
+ hasImages
52
+ ? collageImageIds
53
+ : ([config?.display?.imageIds?.[0]].filter(Boolean) as string[])
54
+ }
55
+ altPrefix={productName}
56
+ />
57
+ </div>
58
+ )}
59
+
60
+ {showTourDescription && (
61
+ <TourDescription
62
+ productSlug={productId}
63
+ locale={locale}
64
+ defaultExpanded={tourDescriptionDefaultExpanded}
65
+ />
66
+ )}
67
+ </>
68
+ );
69
+ }
@@ -0,0 +1,236 @@
1
+ import type { ReactNode, RefObject } from 'react';
2
+ import type {
3
+ AddOn,
4
+ Availability,
5
+ CancellationPolicyOption,
6
+ ItineraryDisplayStep,
7
+ ProductOption,
8
+ RefundTierOption,
9
+ ReturnOption,
10
+ } from '../../lib/booking-api';
11
+ import { AddOnsSection, type AddOnSelection } from './AddOnsSection';
12
+ import { CancellationPolicySelector } from './CancellationPolicySelector';
13
+ import type { NewBookingFlowProps } from './booking-flow-types';
14
+ import type { Currency } from './CurrencySwitcher';
15
+ import { PickupTimeSelector, type PickupTimeInfo } from './PickupTimeSelector';
16
+ import { ReturnTimeSelector } from './ReturnTimeSelector';
17
+ import { TicketSelector, type TicketRate } from './TicketSelector';
18
+
19
+ type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
20
+ type AddOnSelectionsChangeHandler = (
21
+ selections: AddOnSelection[] | ((prev: AddOnSelection[]) => AddOnSelection[]),
22
+ ) => void;
23
+
24
+ type ForcedCancellationPolicy = {
25
+ id: string;
26
+ label: string;
27
+ refundTiers?: RefundTierOption[];
28
+ changeWindowHoursBefore?: number | null;
29
+ } | null;
30
+
31
+ export interface StandardBookingSelectionControlsPanelProps {
32
+ selectedBookingOptionsHydrating: boolean;
33
+ selectedDate: string;
34
+ selectedAvailabilityMatchesDate: boolean;
35
+ selectedAvailability: Availability | null;
36
+ selectedReturnOption: ReturnOption | null;
37
+ pickupTimes: PickupTimeInfo[];
38
+ optionsMap: Map<string, ProductOption>;
39
+ hasAnyMostPopular: boolean;
40
+ isAdmin: boolean;
41
+ pickupLocationSkipped: boolean;
42
+ t: TranslationFn;
43
+ onTimeSelect: (availability: Availability) => void;
44
+ returnOptions: ReturnOption[];
45
+ selectedReturnOptionForDisplay: ReturnOption | null;
46
+ onSelectedReturnOptionChange: (option: ReturnOption) => void;
47
+ getStaySummary: (returnDateTime: Date) => string | null;
48
+ companyTimezone: string;
49
+ currency: Currency;
50
+ locale: string;
51
+ afterItinerary: NewBookingFlowProps['afterItinerary'];
52
+ getItineraryItems: () => ItineraryDisplayStep[] | null;
53
+ beforeCancellationPolicy?: ReactNode;
54
+ cancellationPolicies?: CancellationPolicyOption[];
55
+ forcedCancellationPolicy: ForcedCancellationPolicy;
56
+ cancellationPolicyId: string | null;
57
+ cancellationPolicyRef: RefObject<HTMLDivElement | null>;
58
+ cancellationPolicySectionLabel?: string;
59
+ onCancellationPolicySelect: (policyId: string) => void;
60
+ pricing: TicketRate[];
61
+ quantities: Record<string, number>;
62
+ totalQuantity: number;
63
+ selectedVacancies: number;
64
+ pickupVacancies: number;
65
+ returnVacancies: number | null;
66
+ isSimplifiedPricingView: boolean;
67
+ onQuantityChange: (category: string, delta: number) => void;
68
+ addOns: AddOn[];
69
+ addOnSelections: AddOnSelection[];
70
+ onAddOnSelectionsChange: AddOnSelectionsChangeHandler;
71
+ }
72
+
73
+ export function StandardBookingSelectionControlsPanel({
74
+ selectedBookingOptionsHydrating,
75
+ selectedDate,
76
+ selectedAvailabilityMatchesDate,
77
+ selectedAvailability,
78
+ selectedReturnOption,
79
+ pickupTimes,
80
+ optionsMap,
81
+ hasAnyMostPopular,
82
+ isAdmin,
83
+ pickupLocationSkipped,
84
+ t,
85
+ onTimeSelect,
86
+ returnOptions,
87
+ selectedReturnOptionForDisplay,
88
+ onSelectedReturnOptionChange,
89
+ getStaySummary,
90
+ companyTimezone,
91
+ currency,
92
+ locale,
93
+ afterItinerary,
94
+ getItineraryItems,
95
+ beforeCancellationPolicy,
96
+ cancellationPolicies,
97
+ forcedCancellationPolicy,
98
+ cancellationPolicyId,
99
+ cancellationPolicyRef,
100
+ cancellationPolicySectionLabel,
101
+ onCancellationPolicySelect,
102
+ pricing,
103
+ quantities,
104
+ totalQuantity,
105
+ selectedVacancies,
106
+ pickupVacancies,
107
+ returnVacancies,
108
+ isSimplifiedPricingView,
109
+ onQuantityChange,
110
+ addOns,
111
+ addOnSelections,
112
+ onAddOnSelectionsChange,
113
+ }: StandardBookingSelectionControlsPanelProps) {
114
+ const showReturnSelector =
115
+ !selectedBookingOptionsHydrating &&
116
+ selectedAvailabilityMatchesDate &&
117
+ selectedAvailability &&
118
+ !selectedAvailability.isSummary &&
119
+ selectedAvailability.returnOptions &&
120
+ selectedAvailability.returnOptions.length > 0;
121
+
122
+ const showCancellationPolicy =
123
+ !selectedBookingOptionsHydrating &&
124
+ selectedAvailabilityMatchesDate &&
125
+ selectedAvailability &&
126
+ ((cancellationPolicies?.length ?? 0) > 0 || forcedCancellationPolicy);
127
+
128
+ return (
129
+ <>
130
+ {selectedDate ? (
131
+ <PickupTimeSelector
132
+ pickupTimes={pickupTimes}
133
+ selectedDateTime={selectedAvailability?.dateTime ?? null}
134
+ selectedTicketCount={totalQuantity}
135
+ optionsMap={optionsMap}
136
+ hasAnyMostPopular={hasAnyMostPopular}
137
+ isAdmin={isAdmin}
138
+ pickupLocationSkipped={pickupLocationSkipped}
139
+ t={t}
140
+ onTimeSelect={onTimeSelect}
141
+ />
142
+ ) : null}
143
+
144
+ {selectedBookingOptionsHydrating ? (
145
+ <div className="flex items-center justify-center gap-3 rounded-lg border border-stone-200 bg-white p-4 text-stone-600">
146
+ <div className="booking-loading-spinner" aria-hidden />
147
+ <div>{t('booking.loadingTimes')}</div>
148
+ </div>
149
+ ) : null}
150
+
151
+ {showReturnSelector ? (
152
+ <ReturnTimeSelector
153
+ returnOptions={returnOptions}
154
+ selectedReturnOption={selectedReturnOptionForDisplay}
155
+ selectedTicketCount={totalQuantity}
156
+ companyTimezone={companyTimezone}
157
+ currency={currency}
158
+ locale={locale}
159
+ isAdmin={isAdmin}
160
+ t={t}
161
+ onReturnSelect={(option) => {
162
+ const raw = selectedAvailability.returnOptions?.find(
163
+ (opt) => opt.returnAvailabilityId === option.returnAvailabilityId,
164
+ );
165
+ onSelectedReturnOptionChange(raw ?? option);
166
+ }}
167
+ getStaySummary={getStaySummary}
168
+ />
169
+ ) : null}
170
+
171
+ {!selectedBookingOptionsHydrating && selectedDate && selectedAvailability && afterItinerary
172
+ ? typeof afterItinerary === 'function'
173
+ ? afterItinerary({
174
+ selectedDate,
175
+ selectedAvailability,
176
+ itineraryItems: getItineraryItems(),
177
+ })
178
+ : afterItinerary
179
+ : null}
180
+
181
+ {showCancellationPolicy ? (
182
+ <div ref={cancellationPolicyRef as RefObject<HTMLDivElement>}>
183
+ {beforeCancellationPolicy}
184
+ <CancellationPolicySelector
185
+ policies={[...(cancellationPolicies ?? [])].sort((a, b) => {
186
+ const feeA = a.feeByCurrency[currency] ?? 0;
187
+ const feeB = b.feeByCurrency[currency] ?? 0;
188
+ return feeA - feeB;
189
+ })}
190
+ selectedPolicyId={cancellationPolicyId}
191
+ currency={currency}
192
+ locale={locale}
193
+ t={t}
194
+ onPolicySelect={onCancellationPolicySelect}
195
+ forcedPolicy={forcedCancellationPolicy}
196
+ sectionLabel={cancellationPolicySectionLabel}
197
+ />
198
+ </div>
199
+ ) : null}
200
+
201
+ {!selectedBookingOptionsHydrating && selectedAvailability ? (
202
+ <TicketSelector
203
+ pricing={pricing}
204
+ quantities={quantities}
205
+ totalQuantity={totalQuantity}
206
+ selectedVacancies={selectedVacancies}
207
+ companyTimezone={companyTimezone}
208
+ pickupDateTime={selectedAvailability.dateTime}
209
+ pickupVacancies={pickupVacancies}
210
+ returnDateTime={selectedReturnOption?.dateTime ?? null}
211
+ returnVacancies={returnVacancies}
212
+ resourceCount={selectedReturnOption ? null : (selectedAvailability.resourceCount ?? null)}
213
+ currency={currency}
214
+ locale={locale}
215
+ isAdmin={isAdmin}
216
+ isSimplifiedPricingView={isSimplifiedPricingView}
217
+ t={t}
218
+ onQuantityChange={onQuantityChange}
219
+ minimumQuantities={undefined}
220
+ ticketUnitFloorByCategory={undefined}
221
+ />
222
+ ) : null}
223
+
224
+ {!selectedBookingOptionsHydrating && selectedAvailability && totalQuantity > 0 && addOns.length > 0 ? (
225
+ <AddOnsSection
226
+ addOns={addOns}
227
+ addOnSelections={addOnSelections}
228
+ currency={currency}
229
+ locale={locale}
230
+ onSelectionsChange={onAddOnSelectionsChange}
231
+ minimumTotalByAddOnId={undefined}
232
+ />
233
+ ) : null}
234
+ </>
235
+ );
236
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ confirmFreeChangeBooking,
3
+ type AdminFeAuthoritativeReceipt,
4
+ type Availability,
5
+ type ReturnOption,
6
+ } from '../../lib/booking-api';
7
+ import {
8
+ normalizeNearZeroOwed,
9
+ roundMoney,
10
+ } from '../../lib/booking/change-flow-pricing';
11
+ import type { Currency } from './CurrencySwitcher';
12
+ import {
13
+ quoteAdminChangeForCheckout,
14
+ type ChangeCheckoutQuoteResult,
15
+ } from './change-booking-checkout-quote-runner';
16
+ import type {
17
+ AdminChangeAddOnSelection,
18
+ AdminChangeBookingItem,
19
+ AdminChangeProviderAdditionalAdjustment,
20
+ } from './admin-change-provider-payload';
21
+
22
+ export interface QuoteAdminCustomerChangeForCheckoutParams {
23
+ bookingReference?: string | null;
24
+ lastName: string;
25
+ parentProductId: string;
26
+ availabilityProductOptionId: string;
27
+ selectedAvailability: Availability;
28
+ pickupLocationId: string | null;
29
+ selectedReturnOption: ReturnOption | null;
30
+ bookingItems: AdminChangeBookingItem[];
31
+ addOnSelections: AdminChangeAddOnSelection[];
32
+ manualLineAdjustments: AdminChangeProviderAdditionalAdjustment[];
33
+ useAdminFeAuthoritativeQuote: boolean;
34
+ adminFeAuthoritativeReceipt: AdminFeAuthoritativeReceipt;
35
+ originalReceiptTotal?: number | null;
36
+ changeFlowClientEstimateDue: number;
37
+ latestServerDisplayTotal?: number | null;
38
+ changeFlowNewBookingTotal: number;
39
+ effectiveSubtotalForCheckout: number;
40
+ effectiveTax: number;
41
+ changeFlowInitialTicketCount: number;
42
+ previousAvailabilityId?: string | null;
43
+ previousReturnAvailabilityId?: string | null;
44
+ currency: Currency;
45
+ }
46
+
47
+ export async function quoteAdminCustomerChangeForCheckout({
48
+ bookingReference,
49
+ lastName,
50
+ parentProductId,
51
+ availabilityProductOptionId,
52
+ selectedAvailability,
53
+ pickupLocationId,
54
+ selectedReturnOption,
55
+ bookingItems,
56
+ addOnSelections,
57
+ manualLineAdjustments,
58
+ useAdminFeAuthoritativeQuote,
59
+ adminFeAuthoritativeReceipt,
60
+ originalReceiptTotal,
61
+ changeFlowClientEstimateDue,
62
+ latestServerDisplayTotal,
63
+ changeFlowNewBookingTotal,
64
+ effectiveSubtotalForCheckout,
65
+ effectiveTax,
66
+ changeFlowInitialTicketCount,
67
+ previousAvailabilityId,
68
+ previousReturnAvailabilityId,
69
+ currency,
70
+ }: QuoteAdminCustomerChangeForCheckoutParams): Promise<ChangeCheckoutQuoteResult> {
71
+ const adminFeAmountDueMajorUnits = normalizeNearZeroOwed(
72
+ roundMoney(
73
+ originalReceiptTotal != null
74
+ ? adminFeAuthoritativeReceipt.total - originalReceiptTotal
75
+ : changeFlowClientEstimateDue,
76
+ ),
77
+ );
78
+
79
+ return quoteAdminChangeForCheckout({
80
+ bookingReference,
81
+ lastName,
82
+ parentProductId,
83
+ availabilityProductOptionId,
84
+ dateTime: selectedAvailability.dateTime,
85
+ availabilityId: selectedAvailability.availabilityId || null,
86
+ pickupLocationId: pickupLocationId || null,
87
+ returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
88
+ bookingItems,
89
+ addOnSelections,
90
+ manualLineAdjustments,
91
+ clientProposedTotal:
92
+ useAdminFeAuthoritativeQuote
93
+ ? adminFeAuthoritativeReceipt.total
94
+ : (latestServerDisplayTotal ?? changeFlowNewBookingTotal),
95
+ capacitySeatCredit: {
96
+ enabled: true,
97
+ previousPassengerCount: changeFlowInitialTicketCount,
98
+ previousAvailabilityId: previousAvailabilityId ?? null,
99
+ previousReturnAvailabilityId: previousReturnAvailabilityId ?? null,
100
+ },
101
+ fallbackCart: {
102
+ total: changeFlowNewBookingTotal,
103
+ subtotal: effectiveSubtotalForCheckout,
104
+ tax: effectiveTax,
105
+ },
106
+ currency,
107
+ originalReceiptTotal: originalReceiptTotal ?? 0,
108
+ useAdminFeAuthoritativeQuote,
109
+ adminFeAuthoritativeReceipt,
110
+ adminFeAmountDueMajorUnits,
111
+ });
112
+ }
113
+
114
+ export async function confirmFreeAdminCustomerChange(changeIntentId: string): Promise<void> {
115
+ const freeConfirm = await confirmFreeChangeBooking(changeIntentId);
116
+ if (freeConfirm.status && freeConfirm.status !== 'APPLIED') {
117
+ throw new Error('We could not apply this booking change yet. Please try again.');
118
+ }
119
+ }
@@ -0,0 +1,124 @@
1
+ import { formatInTimeZone } from 'date-fns-tz';
2
+ import type { Availability, Product } from '../../lib/booking-api';
3
+ import {
4
+ effectiveProductOptionIdForChangeFlow,
5
+ normalizeProductOptionIdForChangeFlow,
6
+ } from '../../lib/booking/product-option-id';
7
+ import { parseAvailabilityDateTime } from './change-booking-flow-helpers';
8
+
9
+ type ProductOption = NonNullable<Product['options']>[number];
10
+
11
+ export function isAdminManualOverrideEligibleLine(line: {
12
+ editable: boolean;
13
+ type?: string;
14
+ label?: string;
15
+ }): boolean {
16
+ if (!line.editable) return false;
17
+ const type = (line.type ?? '').toUpperCase();
18
+ const label = (line.label ?? '').toLowerCase();
19
+ const isPromoLikeType =
20
+ type.includes('PROMO') ||
21
+ type.includes('DISCOUNT') ||
22
+ type.includes('VOUCHER') ||
23
+ type.includes('GIFT');
24
+ const isPromoLikeLabel =
25
+ label.includes('promo') ||
26
+ label.includes('discount') ||
27
+ label.includes('voucher') ||
28
+ label.includes('gift');
29
+ return !(isPromoLikeType || isPromoLikeLabel);
30
+ }
31
+
32
+ export function resolveChangeFlowOriginalDate(
33
+ initialDateTime: string | null | undefined,
34
+ companyTimezone: string
35
+ ): string | null {
36
+ if (!initialDateTime?.trim()) return null;
37
+ try {
38
+ return formatInTimeZone(
39
+ parseAvailabilityDateTime(initialDateTime.trim()),
40
+ companyTimezone,
41
+ 'yyyy-MM-dd',
42
+ );
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ export function buildChangeBookingMinimumQuantities({
49
+ isCustomerSelfServeChange,
50
+ isAdmin,
51
+ bookingItems,
52
+ }: {
53
+ isCustomerSelfServeChange: boolean;
54
+ isAdmin: boolean;
55
+ bookingItems?: Array<{ category?: string | null; count?: number | null }> | null;
56
+ }): Record<string, number> | undefined {
57
+ if (!isCustomerSelfServeChange || !bookingItems?.length) return undefined;
58
+ if (isAdmin) return undefined;
59
+
60
+ const minimums: Record<string, number> = {};
61
+ for (const item of bookingItems) {
62
+ const key = item.category?.trim();
63
+ if (!key) continue;
64
+ minimums[key] = Math.max(0, Number(item.count) || 0);
65
+ }
66
+ return minimums;
67
+ }
68
+
69
+ export function resolveInitialChangeProductOptionId({
70
+ initialProductId,
71
+ initialProductOptionId,
72
+ initialAvailabilityId,
73
+ initialDateTime,
74
+ availabilities,
75
+ activeOptions,
76
+ companyTimezone,
77
+ }: {
78
+ initialProductId?: string | null;
79
+ initialProductOptionId?: string | null;
80
+ initialAvailabilityId?: string | null;
81
+ initialDateTime?: string | null;
82
+ availabilities: Availability[];
83
+ activeOptions: ProductOption[];
84
+ companyTimezone: string;
85
+ }): string | null {
86
+ const fromFields = effectiveProductOptionIdForChangeFlow({
87
+ productId: initialProductId,
88
+ productOptionId: initialProductOptionId,
89
+ });
90
+ if (fromFields) return fromFields;
91
+
92
+ const availabilityId = initialAvailabilityId?.trim();
93
+ if (availabilityId) {
94
+ for (const availability of availabilities) {
95
+ if (availability.availabilityId === availabilityId) {
96
+ const productOptionId = normalizeProductOptionIdForChangeFlow(availability.productOptionId);
97
+ if (productOptionId) return productOptionId;
98
+ }
99
+ }
100
+ }
101
+
102
+ if (initialDateTime?.trim()) {
103
+ try {
104
+ const initialDate = parseAvailabilityDateTime(initialDateTime.trim());
105
+ const initialMs = initialDate.getTime();
106
+ const initialDay = formatInTimeZone(initialDate, companyTimezone, 'yyyy-MM-dd');
107
+ for (const availability of availabilities) {
108
+ const availabilityDate = parseAvailabilityDateTime(availability.dateTime);
109
+ if (formatInTimeZone(availabilityDate, companyTimezone, 'yyyy-MM-dd') !== initialDay) continue;
110
+ if (availabilityDate.getTime() !== initialMs) continue;
111
+ const productOptionId = normalizeProductOptionIdForChangeFlow(availability.productOptionId);
112
+ if (productOptionId) return productOptionId;
113
+ }
114
+ } catch {
115
+ /* ignore */
116
+ }
117
+ }
118
+
119
+ if (activeOptions.length === 1) {
120
+ return normalizeProductOptionIdForChangeFlow(activeOptions[0].optionId);
121
+ }
122
+
123
+ return null;
124
+ }