@ticketboothapp/booking 1.2.123 → 1.2.124

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 +154 -3
  111. package/src/lib/pricing-v2-shadow.ts +272 -0
  112. package/test/change-booking-helpers.test.ts +206 -0
@@ -0,0 +1,744 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ cancelReservation,
4
+ cancelReservationBestEffort,
5
+ confirmFreeBooking,
6
+ confirmPartnerBookingWithoutPayment,
7
+ createPaymentIntent,
8
+ describePrivateShuttleCapacityConflictMessage,
9
+ isInsufficientCapacityReserveError,
10
+ reportReserveCapacityConflictClientContext,
11
+ type AddOn,
12
+ type Availability,
13
+ type PricingConfig,
14
+ } from '../../lib/booking-api';
15
+ import type { OrderSummaryFeeLine } from '../../lib/booking/pricing';
16
+ import type { BookingSourceMetadata } from '../../lib/booking/source-metadata';
17
+ import { formatBookingRefForDisplay } from '../../lib/booking-ref';
18
+ import { optionalCheckoutContactFields } from '../../lib/booking/checkout-contact';
19
+ import type { Currency } from './CurrencySwitcher';
20
+ import {
21
+ findMergedPrivateShuttleAvailability,
22
+ privateShuttleBookingCutoffMessage,
23
+ privateShuttleStartTimeAllowed,
24
+ } from './private-shuttle-availability';
25
+ import {
26
+ buildPrivateShuttleCheckoutArtifacts,
27
+ type BuildPrivateShuttleCheckoutArtifactsParams,
28
+ type PrivateShuttleDepositInfo,
29
+ } from './private-shuttle-checkout-builders';
30
+ import type {
31
+ ProviderDashboardChangeBookingPayload,
32
+ ProviderDashboardInitialBooking,
33
+ } from './provider-dashboard-change-booking';
34
+ import {
35
+ reservePrivateShuttleForCheckout,
36
+ type PrivateShuttleAddOnSelection,
37
+ type PrivateShuttleCheckoutItineraryItem,
38
+ type ReservePrivateShuttleForCheckoutParams,
39
+ } from './private-shuttle-reservation-runner';
40
+ import { applyPrivateShuttleProviderChange } from './private-shuttle-provider-change-runner';
41
+ import {
42
+ buildPrivateShuttleAdminChoiceData,
43
+ buildPrivateShuttlePaidCheckoutModalData,
44
+ buildPrivateShuttlePayNowCheckoutModalData,
45
+ confirmPrivateShuttleAdminBookingWithoutPayment,
46
+ type PrivateShuttleAdminChoiceData,
47
+ type PrivateShuttleCheckoutModalData,
48
+ } from './private-shuttle-payment-choice-runner';
49
+ import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provider';
50
+
51
+ export interface UsePrivateShuttleCheckoutControllerParams {
52
+ selectedOption: string;
53
+ selectedDate: string;
54
+ selectedStartTime: string;
55
+ selectedAvailability: Availability | null;
56
+ companyTimezone: string;
57
+ bookingCutoffNow: Date;
58
+ bookingCutoffMinutes?: number | null;
59
+ productId: string;
60
+ currency: Currency;
61
+ bookingSourceAttribution: Partial<BookingSourceMetadata>;
62
+ partnerPortalBooking: boolean;
63
+ forceDashboardSource: boolean;
64
+ isAdmin: boolean;
65
+ isProviderChangeMode: boolean;
66
+ onChangeBooking?: (data: ProviderDashboardChangeBookingPayload) => Promise<void>;
67
+ initialBooking?: ProviderDashboardInitialBooking;
68
+ email: string;
69
+ firstName: string;
70
+ lastName: string;
71
+ phoneNumber: string;
72
+ passengerCount: number;
73
+ maxVacancies: number;
74
+ isSpecialRequest: boolean;
75
+ billableResourceCount: number;
76
+ resourceCount: number;
77
+ resourcePrice: number;
78
+ basePrice: number;
79
+ pricingConfig: PricingConfig | null;
80
+ refreshSelectedDateDetailsForCheckout: ReservePrivateShuttleForCheckoutParams['refreshSelectedDateDetailsForCheckout'];
81
+ reloadAvailabilitiesAfterReserveConflict: () => Promise<Availability[]>;
82
+ totalPrice: number;
83
+ subtotal: number;
84
+ activePromoCode: string | null;
85
+ cancellationPolicyId: string | null;
86
+ selectedCancellationPolicyLabel?: string;
87
+ cancellationPolicyFee: number;
88
+ addOnSelections: PrivateShuttleAddOnSelection[];
89
+ addOns: AddOn[];
90
+ additionalHoursCount: number;
91
+ additionalHoursAmount: number;
92
+ perBookingFeeLineItems: OrderSummaryFeeLine[];
93
+ effectiveTaxAmount: number;
94
+ taxRate: number;
95
+ effectivePromoDiscountAmount: number;
96
+ isGiftCard: boolean;
97
+ isTaxIncludedInPrice: boolean;
98
+ depositInfo: PrivateShuttleDepositInfo | null;
99
+ depositConfig?: BuildPrivateShuttleCheckoutArtifactsParams['depositConfig'];
100
+ balanceChargeDaysBefore?: number;
101
+ selectedPickupLocationName?: string | null;
102
+ customPickupAddress?: string | null;
103
+ pickupLocationId: string | null;
104
+ pickupLocationSkipped: boolean;
105
+ hasPickupLocations: boolean;
106
+ draftItineraryDestinations: string[];
107
+ draftItineraryPlanningNotes: string;
108
+ childSafetySeatsCount: number;
109
+ foodRestrictions: string;
110
+ itineraryDisplayItems: PrivateShuttleCheckoutItineraryItem[];
111
+ providerChangeAuthoritativeReceipt?: ProviderDashboardChangeBookingPayload['authoritativeReceipt'];
112
+ partnerDeferredInvoice: boolean;
113
+ termsAcceptedAt: string | null;
114
+ skipConfirmationCommunications: boolean;
115
+ disableAutoCommunications: boolean;
116
+ setLoading: (loading: boolean) => void;
117
+ setError: (error: string) => void;
118
+ onSuccess?: (data: { reservationReference: string; bookingReference?: string }) => void;
119
+ onShowManage?: (params: { ref: string; lastName: string }) => void;
120
+ t: (key: string) => string;
121
+ }
122
+
123
+ export function usePrivateShuttleCheckoutController({
124
+ selectedOption,
125
+ selectedDate,
126
+ selectedStartTime,
127
+ selectedAvailability,
128
+ companyTimezone,
129
+ bookingCutoffNow,
130
+ bookingCutoffMinutes,
131
+ productId,
132
+ currency,
133
+ bookingSourceAttribution,
134
+ partnerPortalBooking,
135
+ forceDashboardSource,
136
+ isAdmin,
137
+ isProviderChangeMode,
138
+ onChangeBooking,
139
+ initialBooking,
140
+ email,
141
+ firstName,
142
+ lastName,
143
+ phoneNumber,
144
+ passengerCount,
145
+ maxVacancies,
146
+ isSpecialRequest,
147
+ billableResourceCount,
148
+ resourceCount,
149
+ resourcePrice,
150
+ basePrice,
151
+ pricingConfig,
152
+ refreshSelectedDateDetailsForCheckout,
153
+ reloadAvailabilitiesAfterReserveConflict,
154
+ totalPrice,
155
+ subtotal,
156
+ activePromoCode,
157
+ cancellationPolicyId,
158
+ selectedCancellationPolicyLabel,
159
+ cancellationPolicyFee,
160
+ addOnSelections,
161
+ addOns,
162
+ additionalHoursCount,
163
+ additionalHoursAmount,
164
+ perBookingFeeLineItems,
165
+ effectiveTaxAmount,
166
+ taxRate,
167
+ effectivePromoDiscountAmount,
168
+ isGiftCard,
169
+ isTaxIncludedInPrice,
170
+ depositInfo,
171
+ depositConfig,
172
+ balanceChargeDaysBefore,
173
+ selectedPickupLocationName,
174
+ customPickupAddress,
175
+ pickupLocationId,
176
+ pickupLocationSkipped,
177
+ hasPickupLocations,
178
+ draftItineraryDestinations,
179
+ draftItineraryPlanningNotes,
180
+ childSafetySeatsCount,
181
+ foodRestrictions,
182
+ itineraryDisplayItems,
183
+ providerChangeAuthoritativeReceipt,
184
+ partnerDeferredInvoice,
185
+ termsAcceptedAt,
186
+ skipConfirmationCommunications,
187
+ disableAutoCommunications,
188
+ setLoading,
189
+ setError,
190
+ onSuccess,
191
+ onShowManage,
192
+ t,
193
+ }: UsePrivateShuttleCheckoutControllerParams) {
194
+ const [showCheckoutModal, setShowCheckoutModal] = useState(false);
195
+ const [checkoutClientSecret, setCheckoutClientSecret] = useState('');
196
+ const [checkoutModalData, setCheckoutModalData] = useState<PrivateShuttleCheckoutModalData | null>(null);
197
+ const [showAdminPaymentChoice, setShowAdminPaymentChoice] = useState(false);
198
+ const [adminChoiceData, setAdminChoiceData] = useState<PrivateShuttleAdminChoiceData | null>(null);
199
+ const pendingReservationRef = useRef<{ reservationReference: string } | null>(null);
200
+ const paymentSubmitInFlightRef = useRef(false);
201
+
202
+ const cancelPendingReservation = useCallback(() => {
203
+ if (paymentSubmitInFlightRef.current) return;
204
+ const pending = pendingReservationRef.current;
205
+ if (pending) {
206
+ pendingReservationRef.current = null;
207
+ cancelReservation(pending.reservationReference).catch(() => {});
208
+ }
209
+ setShowCheckoutModal(false);
210
+ setCheckoutModalData(null);
211
+ setCheckoutClientSecret('');
212
+ }, []);
213
+
214
+ const cancelPendingReservationBestEffort = useCallback(() => {
215
+ if (paymentSubmitInFlightRef.current) return;
216
+ const pending = pendingReservationRef.current;
217
+ if (pending) {
218
+ pendingReservationRef.current = null;
219
+ cancelReservationBestEffort(pending.reservationReference);
220
+ }
221
+ setShowCheckoutModal(false);
222
+ setCheckoutModalData(null);
223
+ setCheckoutClientSecret('');
224
+ }, []);
225
+
226
+ useEffect(() => {
227
+ const handleAbandon = () => {
228
+ cancelPendingReservation();
229
+ };
230
+ window.addEventListener(BOOKING_FLOW_ABANDON_EVENT, handleAbandon);
231
+ return () => window.removeEventListener(BOOKING_FLOW_ABANDON_EVENT, handleAbandon);
232
+ }, [cancelPendingReservation]);
233
+
234
+ useEffect(() => {
235
+ const handlePageHide = () => {
236
+ cancelPendingReservationBestEffort();
237
+ };
238
+ window.addEventListener('pagehide', handlePageHide);
239
+ return () => window.removeEventListener('pagehide', handlePageHide);
240
+ }, [cancelPendingReservationBestEffort]);
241
+
242
+ const handleCheckout = async () => {
243
+ const availabilityForCheckout = selectedAvailability;
244
+ if (!selectedDate || !selectedStartTime || passengerCount < 1) {
245
+ setError('Please select a date, start time, and passenger count');
246
+ return;
247
+ }
248
+ if (!availabilityForCheckout?.availabilityId) {
249
+ setError('Please choose an available date before continuing.');
250
+ return;
251
+ }
252
+ if (
253
+ !isAdmin &&
254
+ !privateShuttleStartTimeAllowed(
255
+ selectedDate,
256
+ selectedStartTime,
257
+ companyTimezone,
258
+ bookingCutoffNow,
259
+ bookingCutoffMinutes,
260
+ )
261
+ ) {
262
+ setError(privateShuttleBookingCutoffMessage(bookingCutoffMinutes));
263
+ return;
264
+ }
265
+ if (
266
+ hasPickupLocations &&
267
+ !pickupLocationId &&
268
+ !customPickupAddress &&
269
+ !pickupLocationSkipped
270
+ ) {
271
+ setError(t('booking.selectPickupLocation'));
272
+ return;
273
+ }
274
+ if (!isProviderChangeMode) {
275
+ if (!email) {
276
+ setError(t('booking.enterEmail') || 'Please enter your email address');
277
+ return;
278
+ }
279
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
280
+ setError(t('booking.invalidEmail') || 'Please enter a valid email address');
281
+ return;
282
+ }
283
+ if (!firstName?.trim()) {
284
+ setError(t('booking.enterFirstName') || 'Please enter your first name');
285
+ return;
286
+ }
287
+ if (!lastName?.trim()) {
288
+ setError(t('booking.enterLastName') || 'Please enter your last name');
289
+ return;
290
+ }
291
+ }
292
+
293
+ setLoading(true);
294
+ setError('');
295
+ paymentSubmitInFlightRef.current = false;
296
+
297
+ try {
298
+ if (!selectedOption) {
299
+ setError('No product option selected');
300
+ setLoading(false);
301
+ return;
302
+ }
303
+ if (isProviderChangeMode && onChangeBooking) {
304
+ await applyPrivateShuttleProviderChange({
305
+ onChangeBooking,
306
+ selectedOption,
307
+ selectedDate,
308
+ selectedStartTime,
309
+ billableResourceCount,
310
+ pickupLocationId,
311
+ selectedPickupLocationName,
312
+ customPickupAddress,
313
+ initialBooking,
314
+ passengerCount,
315
+ childSafetySeatsCount,
316
+ foodRestrictions,
317
+ addOnSelections,
318
+ cancellationPolicyId,
319
+ activePromoCode,
320
+ totalPrice,
321
+ additionalHoursCount,
322
+ isAdmin,
323
+ providerChangeAuthoritativeReceipt,
324
+ });
325
+ setLoading(false);
326
+ return;
327
+ }
328
+
329
+ const reserveResult = await reservePrivateShuttleForCheckout({
330
+ selectedOption,
331
+ selectedDate,
332
+ selectedStartTime,
333
+ selectedAvailability: availabilityForCheckout,
334
+ companyTimezone,
335
+ productId,
336
+ currency,
337
+ bookingSourceAttribution,
338
+ partnerPortalBooking,
339
+ forceDashboardSource,
340
+ isAdmin,
341
+ isSpecialRequest,
342
+ passengerCount,
343
+ maxVacancies,
344
+ billableResourceCount,
345
+ resourceCount,
346
+ pricingConfig,
347
+ refreshSelectedDateDetailsForCheckout,
348
+ totalPrice,
349
+ activePromoCode,
350
+ cancellationPolicyId,
351
+ addOnSelections,
352
+ additionalHoursCount,
353
+ selectedPickupLocationName,
354
+ customPickupAddress,
355
+ pickupLocationId,
356
+ draftItineraryDestinations,
357
+ draftItineraryPlanningNotes,
358
+ childSafetySeatsCount,
359
+ foodRestrictions,
360
+ itineraryDisplayItems,
361
+ });
362
+ if (reserveResult.kind === 'blocked') {
363
+ setError(reserveResult.message);
364
+ setLoading(false);
365
+ return;
366
+ }
367
+ const {
368
+ bookingSourceContext,
369
+ pricingV2Quote,
370
+ reservation,
371
+ itineraryDisplayForStorage,
372
+ } = reserveResult;
373
+ pendingReservationRef.current = { reservationReference: reservation.reservationReference };
374
+
375
+ const taxLabel = t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees';
376
+ const {
377
+ checkoutBreakdown,
378
+ pricingV2SummaryOverride,
379
+ receiptTotal,
380
+ checkoutDepositInfo,
381
+ ticketLinesForModal,
382
+ feeLineItemsForModal,
383
+ } = buildPrivateShuttleCheckoutArtifacts({
384
+ resourceCount,
385
+ resourcePrice,
386
+ basePrice,
387
+ addOnSelections,
388
+ addOns,
389
+ additionalHoursAmount,
390
+ additionalHoursCount,
391
+ perBookingFeeLineItems,
392
+ cancellationPolicyFee,
393
+ cancellationPolicyLabel: selectedCancellationPolicyLabel,
394
+ effectiveTaxAmount,
395
+ taxLabel,
396
+ effectivePromoDiscountAmount,
397
+ activePromoCode,
398
+ discountLabel: t('booking.discount') || 'Discount',
399
+ promoLineType: isGiftCard ? 'GIFT_CARD' : 'PROMO_CODE',
400
+ depositInfo,
401
+ depositConfig,
402
+ totalPrice,
403
+ currency,
404
+ pricingV2Quote: pricingV2Quote?.quote,
405
+ roundingLabel: t('booking.rounding') || 'Rounding',
406
+ });
407
+
408
+ if (partnerDeferredInvoice) {
409
+ const depositAmount = checkoutDepositInfo ? checkoutDepositInfo.depositAmount : 0;
410
+ const balanceAmount = checkoutDepositInfo ? checkoutDepositInfo.balanceAmount : receiptTotal;
411
+ const confirmedBooking = await confirmPartnerBookingWithoutPayment({
412
+ reservationReference: reservation.reservationReference,
413
+ productId,
414
+ optionId: selectedOption,
415
+ date: selectedDate,
416
+ time: selectedStartTime,
417
+ customerEmail: email || undefined,
418
+ customerFirstName: firstName.trim() || undefined,
419
+ customerLastName: lastName.trim() || undefined,
420
+ ...optionalCheckoutContactFields(phoneNumber),
421
+ currency,
422
+ travelerHotel: selectedPickupLocationName || customPickupAddress || undefined,
423
+ pickupLocationId: pickupLocationId || undefined,
424
+ itineraryDisplay: itineraryDisplayForStorage,
425
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
426
+ skipConfirmationCommunications: skipConfirmationCommunications ? true : undefined,
427
+ disableAutoCommunications: disableAutoCommunications ? true : undefined,
428
+ checkoutBreakdown,
429
+ depositAmount,
430
+ balanceAmount,
431
+ totalAmount: receiptTotal,
432
+ balanceChargeDaysBefore: checkoutDepositInfo ? (balanceChargeDaysBefore ?? 7) : undefined,
433
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
434
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
435
+ ...bookingSourceContext,
436
+ });
437
+ pendingReservationRef.current = null;
438
+ const ref = formatBookingRefForDisplay(confirmedBooking.bookingReference);
439
+ const ln = lastName.trim();
440
+ onSuccess?.({
441
+ reservationReference: reservation.reservationReference,
442
+ bookingReference: confirmedBooking.bookingReference,
443
+ });
444
+ if (onShowManage) {
445
+ onShowManage({ ref, lastName: ln });
446
+ } else {
447
+ window.location.href = `/manage-booking?ref=${ref}&lastName=${encodeURIComponent(ln)}&booking_complete=1`;
448
+ }
449
+ setLoading(false);
450
+ return;
451
+ }
452
+
453
+ if (isAdmin && checkoutDepositInfo) {
454
+ const paymentIntent = await createPaymentIntent({
455
+ productId,
456
+ optionId: selectedOption,
457
+ date: selectedDate,
458
+ time: selectedStartTime,
459
+ quantity: resourceCount,
460
+ customerEmail: email,
461
+ customerFirstName: firstName.trim() || undefined,
462
+ customerLastName: lastName.trim() || undefined,
463
+ ...optionalCheckoutContactFields(phoneNumber),
464
+ currency,
465
+ reservationReference: reservation.reservationReference,
466
+ travelerHotel: selectedPickupLocationName || customPickupAddress || undefined,
467
+ pickupLocationId: pickupLocationId || undefined,
468
+ itineraryDisplay: itineraryDisplayForStorage,
469
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
470
+ cancellationPolicyId: cancellationPolicyId || undefined,
471
+ promoCode: activePromoCode || undefined,
472
+ checkoutBreakdown,
473
+ paymentPlanType: 'DEPOSIT',
474
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
475
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
476
+ idempotencyKey: pricingV2Quote?.quote?.quoteId
477
+ ? `checkout-payment-intent:${reservation.reservationReference}:${pricingV2Quote.quote.quoteId}`
478
+ : undefined,
479
+ depositAmount: checkoutDepositInfo.depositAmount,
480
+ balanceAmount: checkoutDepositInfo.balanceAmount,
481
+ totalAmount: checkoutDepositInfo.totalPrice,
482
+ balanceChargeDaysBefore: balanceChargeDaysBefore ?? 7,
483
+ skipConfirmationCommunications: skipConfirmationCommunications ? true : undefined,
484
+ disableAutoCommunications: disableAutoCommunications ? true : undefined,
485
+ ...bookingSourceContext,
486
+ });
487
+ setAdminChoiceData(buildPrivateShuttleAdminChoiceData({
488
+ reservation,
489
+ checkoutBreakdown,
490
+ totalAmount: checkoutDepositInfo.totalPrice,
491
+ datePart: selectedDate,
492
+ timePart: selectedStartTime,
493
+ availabilityProductOptionId: selectedOption,
494
+ itineraryDisplay: itineraryDisplayForStorage,
495
+ clientSecret: paymentIntent.clientSecret ?? '',
496
+ ticketLinesForModal,
497
+ feeLineItems: feeLineItemsForModal,
498
+ cancellationPolicyFee,
499
+ cancellationPolicyLabel: selectedCancellationPolicyLabel,
500
+ subtotal,
501
+ tax: effectiveTaxAmount,
502
+ totalQuantity: resourceCount,
503
+ isTaxIncludedInPrice,
504
+ taxRate,
505
+ promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
506
+ discountLabel: activePromoCode ? `Promo: ${activePromoCode}` : undefined,
507
+ summaryOverride: pricingV2SummaryOverride,
508
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
509
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
510
+ }));
511
+ setShowAdminPaymentChoice(true);
512
+ setLoading(false);
513
+ return;
514
+ }
515
+
516
+ const paymentIntent = await createPaymentIntent({
517
+ productId,
518
+ optionId: selectedOption,
519
+ date: selectedDate,
520
+ time: selectedStartTime,
521
+ quantity: resourceCount,
522
+ customerEmail: email,
523
+ customerFirstName: firstName.trim() || undefined,
524
+ customerLastName: lastName.trim() || undefined,
525
+ ...optionalCheckoutContactFields(phoneNumber),
526
+ currency,
527
+ reservationReference: reservation.reservationReference,
528
+ travelerHotel: selectedPickupLocationName || customPickupAddress || undefined,
529
+ pickupLocationId: pickupLocationId || undefined,
530
+ itineraryDisplay: itineraryDisplayForStorage,
531
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
532
+ cancellationPolicyId: cancellationPolicyId || undefined,
533
+ promoCode: activePromoCode || undefined,
534
+ checkoutBreakdown,
535
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
536
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
537
+ idempotencyKey: pricingV2Quote?.quote?.quoteId
538
+ ? `checkout-payment-intent:${reservation.reservationReference}:${pricingV2Quote.quote.quoteId}`
539
+ : undefined,
540
+ ...(checkoutDepositInfo && {
541
+ paymentPlanType: 'DEPOSIT' as const,
542
+ depositAmount: checkoutDepositInfo.depositAmount,
543
+ balanceAmount: checkoutDepositInfo.balanceAmount,
544
+ totalAmount: checkoutDepositInfo.totalPrice,
545
+ balanceChargeDaysBefore: balanceChargeDaysBefore ?? 7,
546
+ }),
547
+ skipConfirmationCommunications: skipConfirmationCommunications ? true : undefined,
548
+ disableAutoCommunications: disableAutoCommunications ? true : undefined,
549
+ ...bookingSourceContext,
550
+ });
551
+
552
+ if (paymentIntent.freeBooking) {
553
+ const freeResult = await confirmFreeBooking({
554
+ reservationReference: reservation.reservationReference,
555
+ productId,
556
+ optionId: selectedOption,
557
+ date: selectedDate,
558
+ time: selectedStartTime,
559
+ customerEmail: email || undefined,
560
+ customerFirstName: firstName.trim() || undefined,
561
+ customerLastName: lastName.trim() || undefined,
562
+ ...optionalCheckoutContactFields(phoneNumber),
563
+ currency,
564
+ travelerHotel: selectedPickupLocationName || customPickupAddress || undefined,
565
+ pickupLocationId: pickupLocationId || undefined,
566
+ itineraryDisplay: itineraryDisplayForStorage,
567
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
568
+ skipConfirmationCommunications: skipConfirmationCommunications ? true : undefined,
569
+ disableAutoCommunications: disableAutoCommunications ? true : undefined,
570
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
571
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
572
+ ...bookingSourceContext,
573
+ });
574
+ pendingReservationRef.current = null;
575
+ const ref = formatBookingRefForDisplay(freeResult.bookingReference);
576
+ onSuccess?.({ reservationReference: reservation.reservationReference });
577
+ if (onShowManage) {
578
+ onShowManage({ ref, lastName: lastName.trim() });
579
+ } else {
580
+ window.location.href = `/manage-booking?ref=${ref}&lastName=${encodeURIComponent(lastName.trim())}&booking_complete=1`;
581
+ }
582
+ setLoading(false);
583
+ return;
584
+ }
585
+
586
+ setCheckoutClientSecret(paymentIntent.clientSecret ?? '');
587
+ setCheckoutModalData(buildPrivateShuttlePaidCheckoutModalData({
588
+ reservation,
589
+ customerLastName: lastName.trim(),
590
+ bookingDate: selectedDate,
591
+ ticketLines: ticketLinesForModal,
592
+ feeLineItems: feeLineItemsForModal,
593
+ subtotal: checkoutDepositInfo ? checkoutDepositInfo.totalPrice : receiptTotal,
594
+ tax: effectiveTaxAmount,
595
+ total: checkoutDepositInfo ? checkoutDepositInfo.depositAmount : receiptTotal,
596
+ totalQuantity: resourceCount,
597
+ isTaxIncludedInPrice,
598
+ taxRate,
599
+ cancellationPolicyFee,
600
+ cancellationPolicyLabel: selectedCancellationPolicyLabel,
601
+ promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
602
+ discountLabel: activePromoCode ? `Promo: ${activePromoCode}` : undefined,
603
+ summaryOverride: pricingV2SummaryOverride,
604
+ isDepositPayment: !!checkoutDepositInfo,
605
+ balanceChargeDaysBefore: checkoutDepositInfo ? (balanceChargeDaysBefore ?? 7) : undefined,
606
+ }));
607
+ setShowCheckoutModal(true);
608
+ setLoading(false);
609
+ } catch (err) {
610
+ if (isInsufficientCapacityReserveError(err)) {
611
+ try {
612
+ const merged = await reloadAvailabilitiesAfterReserveConflict();
613
+ const slot = findMergedPrivateShuttleAvailability(
614
+ merged,
615
+ availabilityForCheckout,
616
+ selectedOption || null,
617
+ selectedDate || null,
618
+ companyTimezone
619
+ );
620
+ setError(
621
+ describePrivateShuttleCapacityConflictMessage({
622
+ passengersRequested: passengerCount,
623
+ vacancies: slot?.vacancies ?? null,
624
+ })
625
+ );
626
+ reportReserveCapacityConflictClientContext({
627
+ flow: 'private_shuttle',
628
+ productId,
629
+ selectedDate: selectedDate || null,
630
+ outboundDateTime: availabilityForCheckout?.dateTime ?? null,
631
+ outboundVacanciesAfterRefresh: slot?.vacancies ?? null,
632
+ returnVacanciesAfterRefresh: null,
633
+ partySizeOrPassengers: passengerCount,
634
+ hasReturnSelection: false,
635
+ returnAvailabilityId: null,
636
+ reloadAvailabilitiesSucceeded: true,
637
+ });
638
+ } catch (reloadErr) {
639
+ setError(
640
+ describePrivateShuttleCapacityConflictMessage({
641
+ passengersRequested: passengerCount,
642
+ vacancies: null,
643
+ })
644
+ );
645
+ reportReserveCapacityConflictClientContext({
646
+ flow: 'private_shuttle',
647
+ productId,
648
+ selectedDate: selectedDate || null,
649
+ outboundDateTime: availabilityForCheckout?.dateTime ?? null,
650
+ outboundVacanciesAfterRefresh: null,
651
+ returnVacanciesAfterRefresh: null,
652
+ partySizeOrPassengers: passengerCount,
653
+ hasReturnSelection: false,
654
+ returnAvailabilityId: null,
655
+ reloadAvailabilitiesSucceeded: false,
656
+ reloadErrorMessage:
657
+ reloadErr instanceof Error ? reloadErr.message : String(reloadErr),
658
+ });
659
+ }
660
+ setLoading(false);
661
+ return;
662
+ }
663
+ setError(err instanceof Error ? err.message : 'Something went wrong');
664
+ setLoading(false);
665
+ }
666
+ };
667
+
668
+ const handleConfirmWithoutPayment = async () => {
669
+ const choice = adminChoiceData;
670
+ if (!choice) return;
671
+ setLoading(true);
672
+ setError('');
673
+ try {
674
+ const result = await confirmPrivateShuttleAdminBookingWithoutPayment({
675
+ adminChoiceData: choice,
676
+ bookingSourceAttribution,
677
+ partnerPortalBooking,
678
+ forceDashboardSource,
679
+ productId,
680
+ email,
681
+ firstName,
682
+ lastName,
683
+ phoneNumber,
684
+ currency,
685
+ travelerHotel: selectedPickupLocationName || customPickupAddress || undefined,
686
+ pickupLocationId: pickupLocationId || undefined,
687
+ termsAcceptedAt,
688
+ skipConfirmationCommunications,
689
+ disableAutoCommunications,
690
+ });
691
+ pendingReservationRef.current = null;
692
+ setShowAdminPaymentChoice(false);
693
+ setAdminChoiceData(null);
694
+ onSuccess?.({
695
+ reservationReference: choice.reservationReference,
696
+ bookingReference: result.bookingReference,
697
+ });
698
+ if (onShowManage) {
699
+ onShowManage({ ref: result.displayReference, lastName: result.lastName });
700
+ } else {
701
+ window.location.href = `/manage-booking?ref=${result.displayReference}&lastName=${encodeURIComponent(result.lastName)}&booking_complete=1`;
702
+ }
703
+ } catch (err) {
704
+ setError(err instanceof Error ? err.message : 'Failed to confirm booking');
705
+ } finally {
706
+ setLoading(false);
707
+ }
708
+ };
709
+
710
+ const handlePayNow = () => {
711
+ const choice = adminChoiceData;
712
+ if (!choice) return;
713
+ setShowAdminPaymentChoice(false);
714
+ setCheckoutClientSecret(choice.clientSecret);
715
+ setCheckoutModalData(buildPrivateShuttlePayNowCheckoutModalData(choice, lastName.trim()));
716
+ setShowCheckoutModal(true);
717
+ setAdminChoiceData(null);
718
+ };
719
+
720
+ const handleAdminPaymentChoiceCancel = () => {
721
+ setShowAdminPaymentChoice(false);
722
+ setAdminChoiceData(null);
723
+ setError('');
724
+ };
725
+
726
+ return {
727
+ showCheckoutModal,
728
+ checkoutClientSecret,
729
+ checkoutModalData,
730
+ showAdminPaymentChoice,
731
+ adminChoiceData,
732
+ cancelPendingReservation,
733
+ handleCheckout,
734
+ handleConfirmWithoutPayment,
735
+ handlePayNow,
736
+ handlePaymentSubmitStart: () => {
737
+ paymentSubmitInFlightRef.current = true;
738
+ },
739
+ handlePaymentSubmitError: () => {
740
+ paymentSubmitInFlightRef.current = false;
741
+ },
742
+ handleAdminPaymentChoiceCancel,
743
+ };
744
+ }