@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,271 @@
1
+ import {
2
+ confirmBookingWithoutPayment,
3
+ type CheckoutBreakdown,
4
+ type ItineraryDisplayStep,
5
+ } from '../../lib/booking-api';
6
+ import type { OrderSummaryFeeLine } from '../../lib/booking/pricing';
7
+ import {
8
+ buildBookingSourceContext,
9
+ inferClientBookingSourceFromProductIds,
10
+ type BookingSourceMetadata,
11
+ } from '../../lib/booking/source-metadata';
12
+ import { optionalCheckoutContactFields } from '../../lib/booking/checkout-contact';
13
+ import { formatBookingRefForDisplay } from '../../lib/booking-ref';
14
+ import type { CheckoutModalLineItem, CheckoutModalSummaryOverride } from './CheckoutModal';
15
+ import type { Currency } from './CurrencySwitcher';
16
+
17
+ export interface PrivateShuttleCheckoutModalData {
18
+ reservationReference: string;
19
+ reservationExpiration?: string;
20
+ customerLastName?: string;
21
+ bookingDate?: string;
22
+ ticketLines: CheckoutModalLineItem[];
23
+ feeLineItems: OrderSummaryFeeLine[];
24
+ returnPriceAdjustment: number;
25
+ subtotal: number;
26
+ tax: number;
27
+ total: number;
28
+ totalQuantity: number;
29
+ isTaxIncludedInPrice: boolean;
30
+ taxRate: number;
31
+ cancellationPolicyFee?: number;
32
+ cancellationPolicyLabel?: string;
33
+ promoDiscountAmount?: number;
34
+ discountLabel?: string | null;
35
+ summaryOverride?: CheckoutModalSummaryOverride;
36
+ isDepositPayment?: boolean;
37
+ balanceChargeDaysBefore?: number;
38
+ }
39
+
40
+ export interface PrivateShuttleAdminChoiceData {
41
+ reservationReference: string;
42
+ reservationExpiration?: string;
43
+ checkoutBreakdown: CheckoutBreakdown;
44
+ totalAmount: number;
45
+ datePart: string;
46
+ timePart: string;
47
+ availabilityProductOptionId: string;
48
+ itineraryDisplay?: ItineraryDisplayStep[] | null;
49
+ clientSecret: string;
50
+ ticketLinesForModal: CheckoutModalLineItem[];
51
+ feeLineItems: OrderSummaryFeeLine[];
52
+ cancellationPolicyFee: number;
53
+ cancellationPolicyLabel?: string;
54
+ subtotal: number;
55
+ tax: number;
56
+ totalQuantity: number;
57
+ isTaxIncludedInPrice: boolean;
58
+ taxRate: number;
59
+ promoDiscountAmount: number;
60
+ discountLabel?: string | null;
61
+ summaryOverride?: CheckoutModalSummaryOverride;
62
+ pricingQuoteId?: string;
63
+ pricingQuoteInputsHash?: string;
64
+ }
65
+
66
+ export interface BuildPrivateShuttleAdminChoiceDataParams {
67
+ reservation: { reservationReference: string; expiresAt?: string };
68
+ checkoutBreakdown: CheckoutBreakdown;
69
+ totalAmount: number;
70
+ datePart: string;
71
+ timePart: string;
72
+ availabilityProductOptionId: string;
73
+ itineraryDisplay?: ItineraryDisplayStep[] | null;
74
+ clientSecret: string;
75
+ ticketLinesForModal: CheckoutModalLineItem[];
76
+ feeLineItems: OrderSummaryFeeLine[];
77
+ cancellationPolicyFee: number;
78
+ cancellationPolicyLabel?: string;
79
+ subtotal: number;
80
+ tax: number;
81
+ totalQuantity: number;
82
+ isTaxIncludedInPrice: boolean;
83
+ taxRate: number;
84
+ promoDiscountAmount: number;
85
+ discountLabel?: string | null;
86
+ summaryOverride?: CheckoutModalSummaryOverride;
87
+ pricingQuoteId?: string;
88
+ pricingQuoteInputsHash?: string;
89
+ }
90
+
91
+ export function buildPrivateShuttleAdminChoiceData({
92
+ reservation,
93
+ ...rest
94
+ }: BuildPrivateShuttleAdminChoiceDataParams): PrivateShuttleAdminChoiceData {
95
+ return {
96
+ reservationReference: reservation.reservationReference,
97
+ reservationExpiration: reservation.expiresAt,
98
+ ...rest,
99
+ };
100
+ }
101
+
102
+ export interface BuildPrivateShuttlePaidCheckoutModalDataParams {
103
+ reservation: { reservationReference: string; expiresAt?: string };
104
+ customerLastName: string;
105
+ bookingDate: string;
106
+ ticketLines: CheckoutModalLineItem[];
107
+ feeLineItems: OrderSummaryFeeLine[];
108
+ subtotal: number;
109
+ tax: number;
110
+ total: number;
111
+ totalQuantity: number;
112
+ isTaxIncludedInPrice: boolean;
113
+ taxRate: number;
114
+ cancellationPolicyFee?: number;
115
+ cancellationPolicyLabel?: string;
116
+ promoDiscountAmount: number;
117
+ discountLabel?: string | null;
118
+ summaryOverride?: CheckoutModalSummaryOverride;
119
+ isDepositPayment: boolean;
120
+ balanceChargeDaysBefore?: number;
121
+ }
122
+
123
+ export function buildPrivateShuttlePaidCheckoutModalData({
124
+ reservation,
125
+ customerLastName,
126
+ bookingDate,
127
+ ticketLines,
128
+ feeLineItems,
129
+ subtotal,
130
+ tax,
131
+ total,
132
+ totalQuantity,
133
+ isTaxIncludedInPrice,
134
+ taxRate,
135
+ cancellationPolicyFee,
136
+ cancellationPolicyLabel,
137
+ promoDiscountAmount,
138
+ discountLabel,
139
+ summaryOverride,
140
+ isDepositPayment,
141
+ balanceChargeDaysBefore,
142
+ }: BuildPrivateShuttlePaidCheckoutModalDataParams): PrivateShuttleCheckoutModalData {
143
+ return {
144
+ reservationReference: reservation.reservationReference,
145
+ reservationExpiration: reservation.expiresAt,
146
+ customerLastName,
147
+ bookingDate,
148
+ ticketLines,
149
+ feeLineItems,
150
+ returnPriceAdjustment: 0,
151
+ subtotal,
152
+ tax,
153
+ total,
154
+ totalQuantity,
155
+ isTaxIncludedInPrice,
156
+ taxRate,
157
+ cancellationPolicyFee,
158
+ cancellationPolicyLabel,
159
+ promoDiscountAmount,
160
+ discountLabel,
161
+ summaryOverride,
162
+ isDepositPayment,
163
+ balanceChargeDaysBefore,
164
+ };
165
+ }
166
+
167
+ export function buildPrivateShuttlePayNowCheckoutModalData(
168
+ adminChoiceData: PrivateShuttleAdminChoiceData,
169
+ customerLastName: string
170
+ ): PrivateShuttleCheckoutModalData {
171
+ return {
172
+ reservationReference: adminChoiceData.reservationReference,
173
+ reservationExpiration: adminChoiceData.reservationExpiration,
174
+ customerLastName,
175
+ bookingDate: adminChoiceData.datePart,
176
+ ticketLines: adminChoiceData.ticketLinesForModal,
177
+ feeLineItems: adminChoiceData.feeLineItems,
178
+ returnPriceAdjustment: 0,
179
+ subtotal: adminChoiceData.subtotal,
180
+ tax: adminChoiceData.tax,
181
+ total: adminChoiceData.totalAmount,
182
+ totalQuantity: adminChoiceData.totalQuantity,
183
+ isTaxIncludedInPrice: adminChoiceData.isTaxIncludedInPrice,
184
+ taxRate: adminChoiceData.taxRate,
185
+ cancellationPolicyFee: adminChoiceData.cancellationPolicyFee,
186
+ cancellationPolicyLabel: adminChoiceData.cancellationPolicyLabel,
187
+ promoDiscountAmount: adminChoiceData.promoDiscountAmount,
188
+ discountLabel: adminChoiceData.discountLabel,
189
+ summaryOverride: adminChoiceData.summaryOverride,
190
+ };
191
+ }
192
+
193
+ export interface ConfirmPrivateShuttleAdminBookingWithoutPaymentParams {
194
+ adminChoiceData: PrivateShuttleAdminChoiceData;
195
+ bookingSourceAttribution: Partial<BookingSourceMetadata>;
196
+ partnerPortalBooking: boolean;
197
+ forceDashboardSource: boolean;
198
+ productId: string;
199
+ email: string;
200
+ firstName: string;
201
+ lastName: string;
202
+ phoneNumber: string;
203
+ currency: Currency;
204
+ travelerHotel?: string;
205
+ pickupLocationId?: string;
206
+ termsAcceptedAt?: string | null;
207
+ skipConfirmationCommunications: boolean;
208
+ disableAutoCommunications: boolean;
209
+ }
210
+
211
+ export async function confirmPrivateShuttleAdminBookingWithoutPayment({
212
+ adminChoiceData,
213
+ bookingSourceAttribution,
214
+ partnerPortalBooking,
215
+ forceDashboardSource,
216
+ productId,
217
+ email,
218
+ firstName,
219
+ lastName,
220
+ phoneNumber,
221
+ currency,
222
+ travelerHotel,
223
+ pickupLocationId,
224
+ termsAcceptedAt,
225
+ skipConfirmationCommunications,
226
+ disableAutoCommunications,
227
+ }: ConfirmPrivateShuttleAdminBookingWithoutPaymentParams): Promise<{
228
+ bookingReference: string;
229
+ displayReference: string;
230
+ lastName: string;
231
+ }> {
232
+ const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
233
+ clientChannelSource: inferClientBookingSourceFromProductIds(
234
+ productId,
235
+ adminChoiceData.availabilityProductOptionId,
236
+ ),
237
+ forcePartnerPortalChannel: partnerPortalBooking,
238
+ forceDashboardSource,
239
+ });
240
+ const result = await confirmBookingWithoutPayment({
241
+ reservationReference: adminChoiceData.reservationReference,
242
+ productId,
243
+ optionId: adminChoiceData.availabilityProductOptionId,
244
+ date: adminChoiceData.datePart,
245
+ time: adminChoiceData.timePart,
246
+ customerEmail: email || undefined,
247
+ customerFirstName: firstName.trim() || undefined,
248
+ customerLastName: lastName.trim() || undefined,
249
+ ...optionalCheckoutContactFields(phoneNumber),
250
+ currency,
251
+ travelerHotel,
252
+ pickupLocationId,
253
+ itineraryDisplay: adminChoiceData.itineraryDisplay ?? undefined,
254
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
255
+ skipConfirmationCommunications: skipConfirmationCommunications ? true : undefined,
256
+ disableAutoCommunications: disableAutoCommunications ? true : undefined,
257
+ checkoutBreakdown: adminChoiceData.checkoutBreakdown,
258
+ depositAmount: 0,
259
+ balanceAmount: adminChoiceData.totalAmount,
260
+ totalAmount: adminChoiceData.totalAmount,
261
+ pricingQuoteId: adminChoiceData.pricingQuoteId,
262
+ pricingQuoteInputsHash: adminChoiceData.pricingQuoteInputsHash,
263
+ ...bookingSourceContext,
264
+ });
265
+
266
+ return {
267
+ bookingReference: result.bookingReference,
268
+ displayReference: formatBookingRefForDisplay(result.bookingReference),
269
+ lastName: lastName.trim(),
270
+ };
271
+ }
@@ -0,0 +1,73 @@
1
+ import type {
2
+ ProviderDashboardChangeBookingPayload,
3
+ ProviderDashboardInitialBooking,
4
+ } from './provider-dashboard-change-booking';
5
+ import type { PrivateShuttleAddOnSelection } from './private-shuttle-reservation-runner';
6
+
7
+ export interface ApplyPrivateShuttleProviderChangeParams {
8
+ onChangeBooking: (data: ProviderDashboardChangeBookingPayload) => Promise<void>;
9
+ selectedOption: string;
10
+ selectedDate: string;
11
+ selectedStartTime: string;
12
+ billableResourceCount: number;
13
+ pickupLocationId: string | null;
14
+ selectedPickupLocationName?: string | null;
15
+ customPickupAddress?: string | null;
16
+ initialBooking?: ProviderDashboardInitialBooking;
17
+ passengerCount: number;
18
+ childSafetySeatsCount: number;
19
+ foodRestrictions: string;
20
+ addOnSelections: PrivateShuttleAddOnSelection[];
21
+ cancellationPolicyId: string | null;
22
+ activePromoCode: string | null;
23
+ totalPrice: number;
24
+ additionalHoursCount: number;
25
+ isAdmin: boolean;
26
+ providerChangeAuthoritativeReceipt?: ProviderDashboardChangeBookingPayload['authoritativeReceipt'];
27
+ }
28
+
29
+ export async function applyPrivateShuttleProviderChange({
30
+ onChangeBooking,
31
+ selectedOption,
32
+ selectedDate,
33
+ selectedStartTime,
34
+ billableResourceCount,
35
+ pickupLocationId,
36
+ selectedPickupLocationName,
37
+ customPickupAddress,
38
+ initialBooking,
39
+ passengerCount,
40
+ childSafetySeatsCount,
41
+ foodRestrictions,
42
+ addOnSelections,
43
+ cancellationPolicyId,
44
+ activePromoCode,
45
+ totalPrice,
46
+ additionalHoursCount,
47
+ isAdmin,
48
+ providerChangeAuthoritativeReceipt,
49
+ }: ApplyPrivateShuttleProviderChangeParams): Promise<void> {
50
+ const bookingItems = [{ category: 'RESOURCE' as const, count: billableResourceCount }];
51
+ const [hours, minutes] = selectedStartTime.split(':').map(Number);
52
+ const startTimeISO = `${selectedDate}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00-06:00`;
53
+
54
+ await onChangeBooking({
55
+ productId: selectedOption,
56
+ dateTime: selectedDate,
57
+ bookingItems,
58
+ returnAvailabilityId: null,
59
+ pickupLocationId: pickupLocationId ?? null,
60
+ travelerHotel:
61
+ selectedPickupLocationName ?? customPickupAddress ?? initialBooking?.travelerHotel ?? null,
62
+ startTime: startTimeISO,
63
+ passengerCount,
64
+ childSafetySeatsCount: childSafetySeatsCount > 0 ? childSafetySeatsCount : null,
65
+ foodRestrictions: foodRestrictions.trim() || null,
66
+ addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
67
+ cancellationPolicyId: cancellationPolicyId ?? initialBooking?.cancellationPolicyId ?? null,
68
+ promoCode: activePromoCode ?? null,
69
+ newTotalAmount: totalPrice,
70
+ additionalHoursCount: isAdmin ? additionalHoursCount : null,
71
+ authoritativeReceipt: providerChangeAuthoritativeReceipt,
72
+ });
73
+ }
@@ -0,0 +1,260 @@
1
+ import {
2
+ createReservation,
3
+ describePrivateShuttleCapacityConflictMessage,
4
+ type Availability,
5
+ type ItineraryDisplayStep,
6
+ ItineraryStepType,
7
+ type PricingConfig,
8
+ type PublicNewBookingQuoteV2Data,
9
+ type ReserveResponse,
10
+ } from '../../lib/booking-api';
11
+ import { shouldReplacePricingConfig } from '../../lib/booking/pricing-config';
12
+ import {
13
+ buildBookingSourceContext,
14
+ inferClientBookingSourceFromProductIds,
15
+ type BookingSourceMetadata,
16
+ } from '../../lib/booking/source-metadata';
17
+ import { quotePublicNewBookingV2ForCheckout } from '../../lib/pricing-v2-shadow';
18
+ import type { Currency } from './CurrencySwitcher';
19
+ import { findMergedPrivateShuttleAvailability } from './private-shuttle-availability';
20
+
21
+ type PrivateShuttleBookingItem = { category: 'RESOURCE'; count: number };
22
+ export type PrivateShuttleAddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
23
+ type PrivateShuttleBookingSourceContext = ReturnType<typeof buildBookingSourceContext>;
24
+
25
+ export type PrivateShuttleCheckoutItineraryItem = {
26
+ time: string | null;
27
+ label: string;
28
+ clickableLabel?: string;
29
+ prefix?: string;
30
+ timesNote?: string;
31
+ isProposedStops?: boolean;
32
+ };
33
+
34
+ export type PrivateShuttleCheckoutReserveResult =
35
+ | { kind: 'blocked'; message: string }
36
+ | {
37
+ kind: 'ready';
38
+ bookingItems: PrivateShuttleBookingItem[];
39
+ bookingSourceContext: PrivateShuttleBookingSourceContext;
40
+ startTimeISO: string;
41
+ pricingV2Quote: PublicNewBookingQuoteV2Data | null;
42
+ reservation: ReserveResponse;
43
+ itineraryDisplayForStorage: ItineraryDisplayStep[];
44
+ };
45
+
46
+ export interface ReservePrivateShuttleForCheckoutParams {
47
+ selectedOption: string;
48
+ selectedDate: string;
49
+ selectedStartTime: string;
50
+ selectedAvailability: Availability | null;
51
+ companyTimezone: string;
52
+ productId: string;
53
+ currency: Currency;
54
+ bookingSourceAttribution: Partial<BookingSourceMetadata>;
55
+ partnerPortalBooking: boolean;
56
+ forceDashboardSource: boolean;
57
+ isAdmin: boolean;
58
+ isSpecialRequest: boolean;
59
+ passengerCount: number;
60
+ maxVacancies: number;
61
+ billableResourceCount: number;
62
+ resourceCount: number;
63
+ pricingConfig: PricingConfig | null;
64
+ refreshSelectedDateDetailsForCheckout: () => Promise<{
65
+ availabilities: Availability[];
66
+ pricingConfig?: PricingConfig | null;
67
+ }>;
68
+ totalPrice: number;
69
+ activePromoCode: string | null;
70
+ cancellationPolicyId: string | null;
71
+ addOnSelections: PrivateShuttleAddOnSelection[];
72
+ additionalHoursCount: number;
73
+ selectedPickupLocationName?: string | null;
74
+ customPickupAddress?: string | null;
75
+ pickupLocationId: string | null;
76
+ draftItineraryDestinations: string[];
77
+ draftItineraryPlanningNotes: string;
78
+ childSafetySeatsCount: number;
79
+ foodRestrictions: string;
80
+ itineraryDisplayItems: PrivateShuttleCheckoutItineraryItem[];
81
+ }
82
+
83
+ function buildPrivateShuttleItineraryDisplayForStorage(
84
+ itineraryDisplayItems: PrivateShuttleCheckoutItineraryItem[]
85
+ ): ItineraryDisplayStep[] {
86
+ return itineraryDisplayItems.map((item, i) => {
87
+ if (item.isProposedStops) {
88
+ return {
89
+ stepType: ItineraryStepType.draft,
90
+ time: 'Proposed stops',
91
+ place: item.label,
92
+ };
93
+ }
94
+ const isPickup = i === 0;
95
+ return {
96
+ stepType: isPickup ? ItineraryStepType.pickup : ItineraryStepType.drop_off,
97
+ time: item.time ?? 'TBD',
98
+ place: item.clickableLabel ?? item.label,
99
+ };
100
+ });
101
+ }
102
+
103
+ export async function reservePrivateShuttleForCheckout({
104
+ selectedOption,
105
+ selectedDate,
106
+ selectedStartTime,
107
+ selectedAvailability,
108
+ companyTimezone,
109
+ productId,
110
+ currency,
111
+ bookingSourceAttribution,
112
+ partnerPortalBooking,
113
+ forceDashboardSource,
114
+ isAdmin,
115
+ isSpecialRequest,
116
+ passengerCount,
117
+ maxVacancies,
118
+ billableResourceCount,
119
+ resourceCount,
120
+ pricingConfig,
121
+ refreshSelectedDateDetailsForCheckout,
122
+ totalPrice,
123
+ activePromoCode,
124
+ cancellationPolicyId,
125
+ addOnSelections,
126
+ additionalHoursCount,
127
+ selectedPickupLocationName,
128
+ customPickupAddress,
129
+ pickupLocationId,
130
+ draftItineraryDestinations,
131
+ draftItineraryPlanningNotes,
132
+ childSafetySeatsCount,
133
+ foodRestrictions,
134
+ itineraryDisplayItems,
135
+ }: ReservePrivateShuttleForCheckoutParams): Promise<PrivateShuttleCheckoutReserveResult> {
136
+ if (!selectedOption) {
137
+ return { kind: 'blocked', message: 'No product option selected' };
138
+ }
139
+
140
+ const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
141
+ clientChannelSource: inferClientBookingSourceFromProductIds(productId, selectedOption),
142
+ forcePartnerPortalChannel: partnerPortalBooking,
143
+ forceDashboardSource,
144
+ });
145
+ const bookingItems: PrivateShuttleBookingItem[] = [
146
+ { category: 'RESOURCE', count: billableResourceCount },
147
+ ];
148
+ const [hours, minutes] = selectedStartTime.split(':').map(Number);
149
+ const startTimeISO = `${selectedDate}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00-06:00`;
150
+
151
+ try {
152
+ const refreshed = await refreshSelectedDateDetailsForCheckout();
153
+ const slot = findMergedPrivateShuttleAvailability(
154
+ refreshed.availabilities,
155
+ selectedAvailability,
156
+ selectedOption || null,
157
+ selectedDate || null,
158
+ companyTimezone
159
+ );
160
+ const passengersToReserve = isSpecialRequest
161
+ ? Math.min(passengerCount, maxVacancies)
162
+ : passengerCount;
163
+ if (!isAdmin && slot?.vacancies != null && (slot.vacancies === 0 || slot.vacancies < passengersToReserve)) {
164
+ return {
165
+ kind: 'blocked',
166
+ message: describePrivateShuttleCapacityConflictMessage({
167
+ passengersRequested: passengerCount,
168
+ vacancies: slot.vacancies,
169
+ }),
170
+ };
171
+ }
172
+ if (shouldReplacePricingConfig(pricingConfig, refreshed.pricingConfig)) {
173
+ return {
174
+ kind: 'blocked',
175
+ message: 'Pricing was refreshed. Please review the updated total and continue to payment again.',
176
+ };
177
+ }
178
+ } catch (refreshErr) {
179
+ console.warn('Fresh private shuttle availability check failed before reserve; falling back to reserve validation.', refreshErr);
180
+ }
181
+
182
+ const specialRequestNote = isSpecialRequest
183
+ ? `Special request: Customer requested ${passengerCount} passengers (${resourceCount} shuttles needed). We reserved ${billableResourceCount} at standard capacity. Team to add ${resourceCount - billableResourceCount} more shuttle(s) and verify fleet availability.`
184
+ : '';
185
+ const userPlanningNotes = draftItineraryPlanningNotes.trim();
186
+ const planningNotes = [specialRequestNote, userPlanningNotes].filter(Boolean).join('\n\n');
187
+ const selectedAvailabilityId = selectedAvailability?.availabilityId;
188
+ if (!selectedAvailabilityId) {
189
+ return {
190
+ kind: 'blocked',
191
+ message: 'Please choose an available date before continuing.',
192
+ };
193
+ }
194
+
195
+ const pricingV2Quote = await quotePublicNewBookingV2ForCheckout({
196
+ flow: 'private-shuttle',
197
+ clientProposedTotal: totalPrice,
198
+ request: {
199
+ productId,
200
+ optionId: selectedOption,
201
+ availabilityId: selectedAvailabilityId,
202
+ dateTime: selectedAvailability?.dateTime || selectedDate,
203
+ bookingItems,
204
+ currency,
205
+ promoCode: activePromoCode || null,
206
+ cancellationPolicyId: cancellationPolicyId || null,
207
+ addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
208
+ additionalHoursCount: isAdmin && additionalHoursCount > 0 ? additionalHoursCount : null,
209
+ ...(isAdmin ? { allowOverbook: true } : {}),
210
+ },
211
+ });
212
+
213
+ const reservation = await createReservation({
214
+ productId: selectedOption,
215
+ dateTime: selectedDate,
216
+ availabilityId: selectedAvailabilityId,
217
+ bookingItems,
218
+ currency,
219
+ startTime: startTimeISO,
220
+ passengerCount: isSpecialRequest ? Math.min(passengerCount, maxVacancies) : passengerCount,
221
+ requestedPassengerCount: isSpecialRequest ? passengerCount : undefined,
222
+ pickupLocationId: pickupLocationId || undefined,
223
+ cancellationPolicyId: cancellationPolicyId || undefined,
224
+ promoCode: activePromoCode || undefined,
225
+ travelerHotel: selectedPickupLocationName || customPickupAddress || undefined,
226
+ draftItinerary:
227
+ draftItineraryDestinations.length > 0 || planningNotes
228
+ ? {
229
+ destinations: draftItineraryDestinations,
230
+ planningNotes: planningNotes || undefined,
231
+ }
232
+ : undefined,
233
+ childSafetySeatsCount: childSafetySeatsCount > 0 ? childSafetySeatsCount : undefined,
234
+ foodRestrictions: foodRestrictions.trim() || undefined,
235
+ addOnSelections: addOnSelections.length > 0 ? addOnSelections : undefined,
236
+ additionalHoursCount: isAdmin && additionalHoursCount > 0 ? additionalHoursCount : undefined,
237
+ ...(isAdmin ? { allowOverbook: true } : {}),
238
+ ...(bookingSourceContext.sourceMetadata
239
+ ? {
240
+ source: bookingSourceContext.source,
241
+ sourceMetadata: bookingSourceContext.sourceMetadata,
242
+ source_metadata: bookingSourceContext.source_metadata,
243
+ }
244
+ : {}),
245
+ });
246
+
247
+ if (!reservation?.reservationReference) {
248
+ throw new Error('Invalid reservation response: missing reservationReference');
249
+ }
250
+
251
+ return {
252
+ kind: 'ready',
253
+ bookingItems,
254
+ bookingSourceContext,
255
+ startTimeISO,
256
+ pricingV2Quote,
257
+ reservation,
258
+ itineraryDisplayForStorage: buildPrivateShuttleItineraryDisplayForStorage(itineraryDisplayItems),
259
+ };
260
+ }