@ticketboothapp/booking 1.2.122 → 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 +647 -6157
  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,659 @@
1
+ import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';
2
+ import {
3
+ cancelReservation,
4
+ cancelReservationBestEffort,
5
+ confirmFreeBooking,
6
+ createPaymentIntent,
7
+ confirmPartnerBookingWithoutPayment,
8
+ describeStandardTourCapacityConflictMessage,
9
+ isInsufficientCapacityReserveError,
10
+ reportReserveCapacityConflictClientContext,
11
+ type AddOn,
12
+ type Availability,
13
+ type ItineraryDisplayStep,
14
+ type PricingConfig,
15
+ type Product,
16
+ type ReturnOption,
17
+ } from '../../lib/booking-api';
18
+ import type { OrderSummary, OrderSummaryTicketLine } from '../../lib/booking/pricing';
19
+ import type { BookingSourceMetadata } from '../../lib/booking/source-metadata';
20
+ import { formatBookingRefForDisplay } from '../../lib/booking-ref';
21
+ import { optionalCheckoutContactFields } from '../../lib/booking/checkout-contact';
22
+ import type { Currency } from './CurrencySwitcher';
23
+ import {
24
+ findMergedAvailabilityForSelection,
25
+ findMergedReturnVacancies,
26
+ } from './standard-booking-availability';
27
+ import { buildStandardBookingCheckoutArtifacts, type BuildStandardBookingCheckoutArtifactsParams } from './standard-booking-checkout-builders';
28
+ import type { NewBookingFlowProps } from './booking-flow-types';
29
+ import type { PriceSummaryLine } from './PriceSummary';
30
+ import {
31
+ reserveStandardBookingForCheckout,
32
+ type ReserveStandardBookingForCheckoutParams,
33
+ type StandardAddOnSelection,
34
+ } from './standard-booking-reservation-runner';
35
+ import {
36
+ buildStandardAdminChoiceData,
37
+ buildStandardPaidCheckoutModalData,
38
+ buildStandardPayNowCheckoutModalData,
39
+ confirmStandardAdminBookingWithoutPayment,
40
+ type StandardAdminChoiceData,
41
+ type StandardCheckoutModalData,
42
+ } from './standard-booking-payment-choice-runner';
43
+ import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provider';
44
+
45
+ export interface UseStandardBookingCheckoutControllerParams {
46
+ selectedAvailability: Availability | null;
47
+ setSelectedAvailability: Dispatch<SetStateAction<Availability | null>>;
48
+ selectedReturnOption: ReturnOption | null;
49
+ setSelectedReturnOption: Dispatch<SetStateAction<ReturnOption | null>>;
50
+ isPublicSelectableAvailability: (availability: Availability, now?: Date) => boolean;
51
+ totalQuantity: number;
52
+ t: (key: string) => string;
53
+ email: string;
54
+ firstName: string;
55
+ lastName: string;
56
+ phoneNumber: string;
57
+ product: Product;
58
+ pickupLocationId: string | null;
59
+ pickupLocationSkipped: boolean;
60
+ hidePickupSkipOption: boolean;
61
+ validateBeforeCheckout?: NewBookingFlowProps['validateBeforeCheckout'];
62
+ selectedDate: string;
63
+ computeItineraryDisplay: () => ItineraryDisplayStep[] | null;
64
+ computeItineraryDisplayForStorage: () => ItineraryDisplayStep[] | null;
65
+ quantities: Record<string, number>;
66
+ bookingSourceAttribution: Partial<BookingSourceMetadata>;
67
+ partnerPortalBooking: boolean;
68
+ forceDashboardSource: boolean;
69
+ isAdmin: boolean;
70
+ pricingConfig: PricingConfig | null;
71
+ refreshSelectedDateDetailsForCheckout: ReserveStandardBookingForCheckoutParams['refreshSelectedDateDetailsForCheckout'];
72
+ displayTotalPrice: number;
73
+ displaySubtotal: number;
74
+ totalPrice: number;
75
+ currency: Currency;
76
+ appliedPromoCode: string | null;
77
+ cancellationPolicyId: string | null;
78
+ addOnSelections: StandardAddOnSelection[];
79
+ dependentAddOnSelection?: NewBookingFlowProps['dependentAddOnSelection'];
80
+ ticketLineItems: OrderSummaryTicketLine[];
81
+ pricing: BuildStandardBookingCheckoutArtifactsParams['pricing'];
82
+ getPriceBreakdown: BuildStandardBookingCheckoutArtifactsParams['getPriceBreakdown'];
83
+ checkoutReturnLineAmount: number;
84
+ cancellationPolicyFee: number;
85
+ effectiveCancellationPolicyLabel: string;
86
+ addOns: AddOn[];
87
+ feeLineItems: OrderSummary['feeLineItems'];
88
+ feeLineItemsWithAddOns: OrderSummary['feeLineItems'];
89
+ priceSummaryAugmentation?: {
90
+ lines?: PriceSummaryLine[];
91
+ subtotalAdjustment?: number;
92
+ totalAdjustment?: number;
93
+ } | null;
94
+ checkoutPriceSummaryLines: PriceSummaryLine[];
95
+ isTaxIncludedInPrice: boolean;
96
+ tax: number;
97
+ effectiveTax: number;
98
+ effectivePromoDiscountAmount: number;
99
+ isGiftCard: boolean;
100
+ partnerDeferredInvoice: boolean;
101
+ termsAcceptedAt: string | null;
102
+ skipConfirmationCommunications: boolean;
103
+ disableAutoCommunications: boolean;
104
+ reloadAvailabilitiesAfterReserveConflict: () => Promise<Availability[]>;
105
+ setLoading: (loading: boolean) => void;
106
+ setError: (error: string) => void;
107
+ onSuccess?: NewBookingFlowProps['onSuccess'];
108
+ onShowManage?: (params: { ref: string; lastName: string }) => void;
109
+ }
110
+
111
+ export function useStandardBookingCheckoutController({
112
+ selectedAvailability,
113
+ setSelectedAvailability,
114
+ selectedReturnOption,
115
+ setSelectedReturnOption,
116
+ isPublicSelectableAvailability,
117
+ totalQuantity,
118
+ t,
119
+ email,
120
+ firstName,
121
+ lastName,
122
+ phoneNumber,
123
+ product,
124
+ pickupLocationId,
125
+ pickupLocationSkipped,
126
+ hidePickupSkipOption,
127
+ validateBeforeCheckout,
128
+ selectedDate,
129
+ computeItineraryDisplay,
130
+ computeItineraryDisplayForStorage,
131
+ quantities,
132
+ bookingSourceAttribution,
133
+ partnerPortalBooking,
134
+ forceDashboardSource,
135
+ isAdmin,
136
+ pricingConfig,
137
+ refreshSelectedDateDetailsForCheckout,
138
+ displayTotalPrice,
139
+ displaySubtotal,
140
+ totalPrice,
141
+ currency,
142
+ appliedPromoCode,
143
+ cancellationPolicyId,
144
+ addOnSelections,
145
+ dependentAddOnSelection,
146
+ ticketLineItems,
147
+ pricing,
148
+ getPriceBreakdown,
149
+ checkoutReturnLineAmount,
150
+ cancellationPolicyFee,
151
+ effectiveCancellationPolicyLabel,
152
+ addOns,
153
+ feeLineItems,
154
+ feeLineItemsWithAddOns,
155
+ priceSummaryAugmentation,
156
+ checkoutPriceSummaryLines,
157
+ isTaxIncludedInPrice,
158
+ tax,
159
+ effectiveTax,
160
+ effectivePromoDiscountAmount,
161
+ isGiftCard,
162
+ partnerDeferredInvoice,
163
+ termsAcceptedAt,
164
+ skipConfirmationCommunications,
165
+ disableAutoCommunications,
166
+ reloadAvailabilitiesAfterReserveConflict,
167
+ setLoading,
168
+ setError,
169
+ onSuccess,
170
+ onShowManage,
171
+ }: UseStandardBookingCheckoutControllerParams) {
172
+ const [showCheckoutModal, setShowCheckoutModal] = useState(false);
173
+ const [checkoutClientSecret, setCheckoutClientSecret] = useState('');
174
+ const [checkoutModalData, setCheckoutModalData] = useState<StandardCheckoutModalData | null>(null);
175
+ const [showAdminPaymentChoice, setShowAdminPaymentChoice] = useState(false);
176
+ const [adminChoiceData, setAdminChoiceData] = useState<StandardAdminChoiceData | null>(null);
177
+ const pendingReservationRef = useRef<{ reservationReference: string } | null>(null);
178
+ const paymentSubmitInFlightRef = useRef(false);
179
+
180
+ const cancelPendingReservation = useCallback(() => {
181
+ if (paymentSubmitInFlightRef.current) return;
182
+ const pending = pendingReservationRef.current;
183
+ if (pending) {
184
+ pendingReservationRef.current = null;
185
+ cancelReservation(pending.reservationReference).catch(() => {});
186
+ }
187
+ setShowCheckoutModal(false);
188
+ setCheckoutModalData(null);
189
+ setCheckoutClientSecret('');
190
+ }, []);
191
+
192
+ const cancelPendingReservationBestEffort = useCallback(() => {
193
+ if (paymentSubmitInFlightRef.current) return;
194
+ const pending = pendingReservationRef.current;
195
+ if (pending) {
196
+ pendingReservationRef.current = null;
197
+ cancelReservationBestEffort(pending.reservationReference);
198
+ }
199
+ setShowCheckoutModal(false);
200
+ setCheckoutModalData(null);
201
+ setCheckoutClientSecret('');
202
+ }, []);
203
+
204
+ useEffect(() => {
205
+ const handleAbandon = () => {
206
+ cancelPendingReservation();
207
+ };
208
+ window.addEventListener(BOOKING_FLOW_ABANDON_EVENT, handleAbandon);
209
+ return () => window.removeEventListener(BOOKING_FLOW_ABANDON_EVENT, handleAbandon);
210
+ }, [cancelPendingReservation]);
211
+
212
+ useEffect(() => {
213
+ const handlePageHide = () => {
214
+ cancelPendingReservationBestEffort();
215
+ };
216
+ window.addEventListener('pagehide', handlePageHide);
217
+ return () => window.removeEventListener('pagehide', handlePageHide);
218
+ }, [cancelPendingReservationBestEffort]);
219
+
220
+ const handleCheckout = async () => {
221
+ const availabilityForCheckout = selectedAvailability;
222
+ if (!availabilityForCheckout || totalQuantity === 0) {
223
+ setError(t('booking.selectTimeAndTickets'));
224
+ return;
225
+ }
226
+ if (!isPublicSelectableAvailability(availabilityForCheckout, new Date())) {
227
+ setSelectedAvailability(null);
228
+ setSelectedReturnOption(null);
229
+ setError('This departure is no longer available online because it starts too soon. Please choose a later time.');
230
+ return;
231
+ }
232
+ if (!email) {
233
+ setError(t('booking.enterEmail') || 'Please enter your email address');
234
+ return;
235
+ }
236
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
237
+ setError(t('booking.invalidEmail') || 'Please enter a valid email address');
238
+ return;
239
+ }
240
+ if (!firstName?.trim()) {
241
+ setError(t('booking.enterFirstName') || 'Please enter your first name');
242
+ return;
243
+ }
244
+ if (!lastName?.trim()) {
245
+ setError(t('booking.enterLastName') || 'Please enter your last name');
246
+ return;
247
+ }
248
+ if (
249
+ product.pickupLocations &&
250
+ product.pickupLocations.length > 0 &&
251
+ !pickupLocationId &&
252
+ (!pickupLocationSkipped || hidePickupSkipOption)
253
+ ) {
254
+ setError(t('booking.selectPickupLocation'));
255
+ return;
256
+ }
257
+ const externalValidationError = validateBeforeCheckout?.({
258
+ selectedDate: selectedDate || null,
259
+ selectedAvailability: availabilityForCheckout,
260
+ itineraryItems: computeItineraryDisplay(),
261
+ });
262
+ if (externalValidationError) {
263
+ setError(externalValidationError);
264
+ return;
265
+ }
266
+
267
+ setLoading(true);
268
+ setError('');
269
+ paymentSubmitInFlightRef.current = false;
270
+
271
+ try {
272
+ const reserveResult = await reserveStandardBookingForCheckout({
273
+ quantities,
274
+ selectedAvailability: availabilityForCheckout,
275
+ selectedReturnOption,
276
+ product,
277
+ pickupLocationId,
278
+ bookingSourceAttribution,
279
+ partnerPortalBooking,
280
+ forceDashboardSource,
281
+ isAdmin,
282
+ totalQuantity,
283
+ pricingConfig,
284
+ refreshSelectedDateDetailsForCheckout,
285
+ displayTotalPrice,
286
+ totalPrice,
287
+ currency,
288
+ appliedPromoCode,
289
+ cancellationPolicyId,
290
+ addOnSelections,
291
+ itineraryDisplay: computeItineraryDisplayForStorage() ?? computeItineraryDisplay(),
292
+ });
293
+ if (reserveResult.kind === 'blocked') {
294
+ setError(reserveResult.message);
295
+ setLoading(false);
296
+ return;
297
+ }
298
+ const {
299
+ availabilityProductOptionId,
300
+ bookingSourceContext,
301
+ selectedPickupLocation,
302
+ pricingV2Quote,
303
+ reservation,
304
+ datePart,
305
+ timePart,
306
+ itineraryDisplay,
307
+ } = reserveResult;
308
+ pendingReservationRef.current = { reservationReference: reservation.reservationReference };
309
+
310
+ const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
311
+ const {
312
+ checkoutBreakdown,
313
+ pricingV2SummaryOverride,
314
+ amountDueForCheckout,
315
+ ticketLinesForModal,
316
+ feeLineItemsForModal,
317
+ } = buildStandardBookingCheckoutArtifacts({
318
+ ticketLineItems,
319
+ pricing,
320
+ getPriceBreakdown,
321
+ totalQuantity,
322
+ checkoutReturnLineAmount,
323
+ returnOptionLabel: t('booking.returnOption') || 'Return option',
324
+ personLabel: t('booking.person') || 'person',
325
+ peopleLabel: t('booking.people') || 'people',
326
+ cancellationPolicyFee,
327
+ cancellationPolicyLabel: effectiveCancellationPolicyLabel,
328
+ addOnSelections,
329
+ addOns,
330
+ feeLineItems,
331
+ feeLineItemsWithAddOns,
332
+ priceSummaryLines: priceSummaryAugmentation?.lines,
333
+ checkoutPriceSummaryLineCount: checkoutPriceSummaryLines.length,
334
+ isTaxIncludedInPrice,
335
+ taxForBreakdown,
336
+ taxLabel: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
337
+ effectivePromoDiscountAmount,
338
+ appliedPromoCode,
339
+ discountLabel: t('booking.discount') || 'Discount',
340
+ promoLineType: isGiftCard ? 'GIFT_CARD' : 'PROMO_CODE',
341
+ displayTotalPrice,
342
+ currency,
343
+ pricingV2Quote: pricingV2Quote?.quote,
344
+ roundingLabel: t('booking.rounding') || 'Rounding',
345
+ });
346
+
347
+ if (partnerDeferredInvoice) {
348
+ if (!reservation?.reservationReference) {
349
+ throw new Error('Missing reservation reference for partner booking confirmation');
350
+ }
351
+ const confirmedBooking = await confirmPartnerBookingWithoutPayment({
352
+ reservationReference: reservation.reservationReference,
353
+ productId: product.productId,
354
+ optionId: availabilityProductOptionId,
355
+ date: datePart,
356
+ time: timePart,
357
+ customerEmail: email || undefined,
358
+ customerFirstName: firstName.trim() || undefined,
359
+ customerLastName: lastName.trim() || undefined,
360
+ ...optionalCheckoutContactFields(phoneNumber),
361
+ currency,
362
+ travelerHotel: selectedPickupLocation?.name || undefined,
363
+ pickupLocationId: pickupLocationId || undefined,
364
+ itineraryDisplay: itineraryDisplay ?? undefined,
365
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
366
+ skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
367
+ disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
368
+ checkoutBreakdown,
369
+ dependentAddOnSelection: dependentAddOnSelection || undefined,
370
+ depositAmount: 0,
371
+ balanceAmount: amountDueForCheckout,
372
+ totalAmount: amountDueForCheckout,
373
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
374
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
375
+ ...bookingSourceContext,
376
+ });
377
+ pendingReservationRef.current = null;
378
+ const ref = formatBookingRefForDisplay(confirmedBooking.bookingReference);
379
+ const ln = lastName.trim();
380
+ onSuccess?.({
381
+ reservationReference: reservation.reservationReference,
382
+ bookingReference: confirmedBooking.bookingReference,
383
+ });
384
+ if (onShowManage) {
385
+ onShowManage({ ref, lastName: ln });
386
+ } else {
387
+ const params = new URLSearchParams({ ref, lastName: ln, booking_complete: '1' });
388
+ window.location.href = `/manage-booking?${params.toString()}`;
389
+ }
390
+ setLoading(false);
391
+ return;
392
+ }
393
+
394
+ const paymentIntent = await createPaymentIntent({
395
+ productId: product.productId,
396
+ optionId: availabilityProductOptionId,
397
+ date: datePart,
398
+ time: timePart,
399
+ quantity: totalQuantity,
400
+ customerEmail: email,
401
+ customerFirstName: firstName.trim() || undefined,
402
+ customerLastName: lastName.trim() || undefined,
403
+ ...optionalCheckoutContactFields(phoneNumber),
404
+ currency,
405
+ reservationReference: reservation?.reservationReference,
406
+ travelerHotel: selectedPickupLocation?.name || undefined,
407
+ pickupLocationId: pickupLocationId || undefined,
408
+ itineraryDisplay: itineraryDisplay ?? undefined,
409
+ returnAvailabilityId: selectedReturnOption?.returnAvailabilityId,
410
+ promoCode: appliedPromoCode || undefined,
411
+ cancellationPolicyId: cancellationPolicyId || undefined,
412
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
413
+ checkoutBreakdown,
414
+ dependentAddOnSelection: dependentAddOnSelection || undefined,
415
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
416
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
417
+ idempotencyKey: pricingV2Quote?.quote?.quoteId && reservation?.reservationReference
418
+ ? `checkout-payment-intent:${reservation.reservationReference}:${pricingV2Quote.quote.quoteId}`
419
+ : undefined,
420
+ skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
421
+ disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
422
+ ...bookingSourceContext,
423
+ });
424
+
425
+ if ('freeBooking' in paymentIntent && paymentIntent.freeBooking) {
426
+ if (!reservation?.reservationReference) {
427
+ throw new Error('Missing reservation reference for free booking confirmation');
428
+ }
429
+ const freeBookingResult = await confirmFreeBooking({
430
+ reservationReference: reservation?.reservationReference ?? '',
431
+ productId: product.productId,
432
+ optionId: availabilityProductOptionId,
433
+ date: datePart,
434
+ time: timePart,
435
+ customerEmail: email || undefined,
436
+ customerFirstName: firstName.trim() || undefined,
437
+ customerLastName: lastName.trim() || undefined,
438
+ ...optionalCheckoutContactFields(phoneNumber),
439
+ currency,
440
+ travelerHotel: selectedPickupLocation?.name || undefined,
441
+ pickupLocationId: pickupLocationId || undefined,
442
+ itineraryDisplay: itineraryDisplay ?? undefined,
443
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
444
+ skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
445
+ disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
446
+ dependentAddOnSelection: dependentAddOnSelection || undefined,
447
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
448
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
449
+ ...bookingSourceContext,
450
+ });
451
+ pendingReservationRef.current = null;
452
+ const ref = formatBookingRefForDisplay(freeBookingResult.bookingReference);
453
+ const ln = lastName.trim();
454
+ onSuccess?.({ reservationReference: reservation?.reservationReference ?? '' });
455
+ if (onShowManage) {
456
+ onShowManage({ ref, lastName: ln });
457
+ } else {
458
+ const params = new URLSearchParams({ ref, lastName: ln, booking_complete: '1' });
459
+ window.location.href = `/manage-booking?${params.toString()}`;
460
+ }
461
+ setLoading(false);
462
+ return;
463
+ }
464
+
465
+ if (isAdmin) {
466
+ if (!reservation?.reservationReference) {
467
+ throw new Error('Missing reservation reference for admin payment flow');
468
+ }
469
+ setError('');
470
+ setAdminChoiceData(buildStandardAdminChoiceData({
471
+ reservation,
472
+ checkoutBreakdown,
473
+ totalAmount: amountDueForCheckout,
474
+ datePart,
475
+ timePart,
476
+ availabilityProductOptionId,
477
+ itineraryDisplay: itineraryDisplay ?? undefined,
478
+ clientSecret: paymentIntent.clientSecret ?? '',
479
+ ticketLinesForModal,
480
+ feeLineItems: feeLineItemsForModal,
481
+ returnPriceAdjustment: checkoutReturnLineAmount,
482
+ cancellationPolicyFee,
483
+ cancellationPolicyLabel: effectiveCancellationPolicyLabel,
484
+ subtotal: displaySubtotal,
485
+ tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
486
+ totalQuantity,
487
+ isTaxIncludedInPrice,
488
+ taxRate: pricingConfig?.taxRate ?? 0,
489
+ promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
490
+ discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : undefined,
491
+ summaryOverride: pricingV2SummaryOverride,
492
+ pricingQuoteId: pricingV2Quote?.quote?.quoteId ?? undefined,
493
+ pricingQuoteInputsHash: pricingV2Quote?.quote?.inputsHash ?? undefined,
494
+ }));
495
+ setShowAdminPaymentChoice(true);
496
+ setLoading(false);
497
+ return;
498
+ }
499
+
500
+ setCheckoutClientSecret(paymentIntent.clientSecret ?? '');
501
+ setCheckoutModalData(buildStandardPaidCheckoutModalData({
502
+ reservation,
503
+ customerLastName: lastName.trim(),
504
+ bookingDate: datePart,
505
+ ticketLines: ticketLinesForModal,
506
+ feeLineItems: feeLineItemsForModal,
507
+ returnPriceAdjustment: checkoutReturnLineAmount,
508
+ cancellationPolicyFee,
509
+ cancellationPolicyLabel: effectiveCancellationPolicyLabel,
510
+ subtotal: displaySubtotal,
511
+ tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
512
+ total: amountDueForCheckout,
513
+ totalQuantity,
514
+ isTaxIncludedInPrice,
515
+ taxRate: pricingConfig?.taxRate ?? 0,
516
+ promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
517
+ discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : undefined,
518
+ summaryOverride: pricingV2SummaryOverride,
519
+ }));
520
+ setShowCheckoutModal(true);
521
+ setLoading(false);
522
+ } catch (err) {
523
+ if (isInsufficientCapacityReserveError(err)) {
524
+ try {
525
+ const merged = await reloadAvailabilitiesAfterReserveConflict();
526
+ const outbound = findMergedAvailabilityForSelection(merged, availabilityForCheckout);
527
+ const outboundVacancies = outbound?.vacancies ?? null;
528
+ const returnVacancies = findMergedReturnVacancies(outbound, selectedReturnOption);
529
+ setError(
530
+ describeStandardTourCapacityConflictMessage({
531
+ partySize: totalQuantity,
532
+ outboundVacancies,
533
+ returnVacancies,
534
+ hasReturnSelection: !!selectedReturnOption,
535
+ })
536
+ );
537
+ reportReserveCapacityConflictClientContext({
538
+ flow: 'standard_tour',
539
+ productId: product.productId,
540
+ selectedDate: selectedDate || null,
541
+ outboundDateTime: availabilityForCheckout.dateTime,
542
+ outboundVacanciesAfterRefresh: outboundVacancies,
543
+ returnVacanciesAfterRefresh: returnVacancies,
544
+ partySizeOrPassengers: totalQuantity,
545
+ hasReturnSelection: !!selectedReturnOption,
546
+ returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
547
+ reloadAvailabilitiesSucceeded: true,
548
+ });
549
+ } catch (reloadErr) {
550
+ setError(
551
+ describeStandardTourCapacityConflictMessage({
552
+ partySize: totalQuantity,
553
+ outboundVacancies: null,
554
+ returnVacancies: null,
555
+ hasReturnSelection: !!selectedReturnOption,
556
+ })
557
+ );
558
+ reportReserveCapacityConflictClientContext({
559
+ flow: 'standard_tour',
560
+ productId: product.productId,
561
+ selectedDate: selectedDate || null,
562
+ outboundDateTime: availabilityForCheckout.dateTime,
563
+ outboundVacanciesAfterRefresh: null,
564
+ returnVacanciesAfterRefresh: null,
565
+ partySizeOrPassengers: totalQuantity,
566
+ hasReturnSelection: !!selectedReturnOption,
567
+ returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
568
+ reloadAvailabilitiesSucceeded: false,
569
+ reloadErrorMessage:
570
+ reloadErr instanceof Error ? reloadErr.message : String(reloadErr),
571
+ });
572
+ }
573
+ setLoading(false);
574
+ return;
575
+ }
576
+ setError(err instanceof Error ? err.message : 'Something went wrong');
577
+ setLoading(false);
578
+ }
579
+ };
580
+
581
+ const handleConfirmWithoutPayment = async () => {
582
+ const choice = adminChoiceData;
583
+ if (!choice) return;
584
+ setLoading(true);
585
+ setError('');
586
+ try {
587
+ const result = await confirmStandardAdminBookingWithoutPayment({
588
+ adminChoiceData: choice,
589
+ bookingSourceAttribution,
590
+ partnerPortalBooking,
591
+ forceDashboardSource,
592
+ productId: product.productId,
593
+ email,
594
+ firstName,
595
+ lastName,
596
+ phoneNumber,
597
+ currency,
598
+ travelerHotel: product.pickupLocations?.find((loc) => loc.id === pickupLocationId)?.name || undefined,
599
+ pickupLocationId: pickupLocationId || undefined,
600
+ termsAcceptedAt,
601
+ skipConfirmationCommunications,
602
+ disableAutoCommunications,
603
+ });
604
+ pendingReservationRef.current = null;
605
+ setShowAdminPaymentChoice(false);
606
+ setAdminChoiceData(null);
607
+ onSuccess?.({ reservationReference: choice.reservationReference });
608
+ if (onShowManage) {
609
+ onShowManage({ ref: result.displayReference, lastName: result.lastName });
610
+ } else {
611
+ const params = new URLSearchParams({
612
+ ref: result.displayReference,
613
+ lastName: result.lastName,
614
+ booking_complete: '1',
615
+ });
616
+ window.location.href = `/manage-booking?${params.toString()}`;
617
+ }
618
+ } catch (err) {
619
+ setError(err instanceof Error ? err.message : 'Failed to confirm booking');
620
+ } finally {
621
+ setLoading(false);
622
+ }
623
+ };
624
+
625
+ const handlePayNow = () => {
626
+ const choice = adminChoiceData;
627
+ if (!choice) return;
628
+ setShowAdminPaymentChoice(false);
629
+ setCheckoutClientSecret(choice.clientSecret);
630
+ setCheckoutModalData(buildStandardPayNowCheckoutModalData(choice, lastName.trim()));
631
+ setShowCheckoutModal(true);
632
+ setAdminChoiceData(null);
633
+ };
634
+
635
+ const handleAdminPaymentChoiceCancel = () => {
636
+ setShowAdminPaymentChoice(false);
637
+ setAdminChoiceData(null);
638
+ setError('');
639
+ };
640
+
641
+ return {
642
+ showCheckoutModal,
643
+ checkoutClientSecret,
644
+ checkoutModalData,
645
+ showAdminPaymentChoice,
646
+ adminChoiceData,
647
+ cancelPendingReservation,
648
+ handleCheckout,
649
+ handleConfirmWithoutPayment,
650
+ handlePayNow,
651
+ handlePaymentSubmitStart: () => {
652
+ paymentSubmitInFlightRef.current = true;
653
+ },
654
+ handlePaymentSubmitError: () => {
655
+ paymentSubmitInFlightRef.current = false;
656
+ },
657
+ handleAdminPaymentChoiceCancel,
658
+ };
659
+ }