@ticketboothapp/booking 1.2.123 → 1.2.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGE_BOOKING_BE_HANDOFF.md +15 -4
  2. package/package.json +1 -1
  3. package/src/components/booking/AdminChangeBookingContent.tsx +276 -0
  4. package/src/components/booking/AdminChangeBookingFlow.tsx +651 -6202
  5. package/src/components/booking/AdminChangeCheckoutDialogs.tsx +105 -0
  6. package/src/components/booking/AdminChangeCheckoutPanel.tsx +307 -0
  7. package/src/components/booking/AdminChangePricingPanel.tsx +244 -0
  8. package/src/components/booking/AdminChangeProductSwitcher.tsx +50 -0
  9. package/src/components/booking/AdminChangeReceiptComparison.tsx +167 -0
  10. package/src/components/booking/ChangeBookingCalendarPanel.tsx +126 -0
  11. package/src/components/booking/ChangeBookingCheckoutPanel.tsx +204 -0
  12. package/src/components/booking/ChangeBookingFlow.tsx +576 -4648
  13. package/src/components/booking/ChangeBookingItineraryPanel.tsx +115 -0
  14. package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +57 -0
  15. package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +157 -0
  16. package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +102 -0
  17. package/src/components/booking/CheckoutModal.tsx +68 -49
  18. package/src/components/booking/NewBookingFlow.tsx +485 -3505
  19. package/src/components/booking/PriceBreakdown.tsx +17 -3
  20. package/src/components/booking/PriceSummary.tsx +21 -3
  21. package/src/components/booking/PrivateShuttleAddOnsSection.tsx +102 -0
  22. package/src/components/booking/PrivateShuttleBookingFlow.tsx +407 -2788
  23. package/src/components/booking/PrivateShuttleCheckoutDialogs.tsx +106 -0
  24. package/src/components/booking/PrivateShuttleCheckoutSection.tsx +333 -0
  25. package/src/components/booking/PrivateShuttleItinerarySection.tsx +95 -0
  26. package/src/components/booking/PrivateShuttleMediaIntro.tsx +62 -0
  27. package/src/components/booking/PrivateShuttleOptionSection.tsx +81 -0
  28. package/src/components/booking/PrivateShuttlePassengerSection.tsx +148 -0
  29. package/src/components/booking/PrivateShuttlePickupSection.tsx +91 -0
  30. package/src/components/booking/PrivateShuttlePreferencesSection.tsx +75 -0
  31. package/src/components/booking/PrivateShuttleStartTimeSection.tsx +88 -0
  32. package/src/components/booking/StandardBookingCheckoutDialogs.tsx +106 -0
  33. package/src/components/booking/StandardBookingCheckoutSection.tsx +185 -0
  34. package/src/components/booking/StandardBookingItineraryPanel.tsx +125 -0
  35. package/src/components/booking/StandardBookingMediaIntro.tsx +69 -0
  36. package/src/components/booking/StandardBookingSelectionControlsPanel.tsx +236 -0
  37. package/src/components/booking/admin-change-customer-quote-runner.ts +119 -0
  38. package/src/components/booking/admin-change-flow-state-helpers.ts +124 -0
  39. package/src/components/booking/admin-change-payment-choice-runner.ts +172 -0
  40. package/src/components/booking/admin-change-provider-payload.ts +101 -0
  41. package/src/components/booking/change-booking-checkout-builders.ts +185 -0
  42. package/src/components/booking/change-booking-checkout-quote-runner.ts +207 -0
  43. package/src/components/booking/change-booking-flow-helpers.ts +706 -0
  44. package/src/components/booking/change-booking-payment-modal-builders.ts +151 -0
  45. package/src/components/booking/change-booking-quote-guards.ts +223 -0
  46. package/src/components/booking/change-booking-quote-state.ts +64 -0
  47. package/src/components/booking/pricing-v2-checkout-summary.ts +144 -0
  48. package/src/components/booking/private-shuttle-availability.ts +111 -0
  49. package/src/components/booking/private-shuttle-checkout-builders.ts +264 -0
  50. package/src/components/booking/private-shuttle-checkout-controller.ts +744 -0
  51. package/src/components/booking/private-shuttle-payment-choice-runner.ts +271 -0
  52. package/src/components/booking/private-shuttle-provider-change-runner.ts +73 -0
  53. package/src/components/booking/private-shuttle-reservation-runner.ts +260 -0
  54. package/src/components/booking/standard-booking-availability.ts +305 -0
  55. package/src/components/booking/standard-booking-checkout-builders.ts +289 -0
  56. package/src/components/booking/standard-booking-checkout-controller.ts +659 -0
  57. package/src/components/booking/standard-booking-payment-choice-runner.ts +274 -0
  58. package/src/components/booking/standard-booking-reservation-runner.ts +229 -0
  59. package/src/components/booking/use-private-shuttle-availability.ts +602 -0
  60. package/src/components/booking/use-standard-booking-availability.ts +806 -0
  61. package/src/components/booking/useActiveProductOptions.ts +26 -0
  62. package/src/components/booking/useAdminChangeCheckoutController.ts +806 -0
  63. package/src/components/booking/useAdminChangeOrderSummaryModel.ts +246 -0
  64. package/src/components/booking/useAdminChangePriceSummary.ts +416 -0
  65. package/src/components/booking/useAdminChangePricingDebugPanel.tsx +178 -0
  66. package/src/components/booking/useAdminChangeProductReset.ts +60 -0
  67. package/src/components/booking/useAdminChangeProducts.ts +102 -0
  68. package/src/components/booking/useAdminChangePromoDiscount.ts +235 -0
  69. package/src/components/booking/useAdminChangeProtectedPricing.ts +280 -0
  70. package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +437 -0
  71. package/src/components/booking/useAdminChangeQuotePreview.ts +506 -0
  72. package/src/components/booking/useAdminChangeReceiptDerivation.ts +203 -0
  73. package/src/components/booking/useAdminCustomReceiptLines.ts +57 -0
  74. package/src/components/booking/useAdminProviderPricingAdjustments.ts +146 -0
  75. package/src/components/booking/useBookingAvailabilityAddOns.ts +54 -0
  76. package/src/components/booking/useBookingPromoAndQuantityController.ts +163 -0
  77. package/src/components/booking/useBookingQuantityCapTrim.ts +56 -0
  78. package/src/components/booking/useBookingViewItemAnalytics.ts +30 -0
  79. package/src/components/booking/useChangeBookingAddOnFloorController.ts +109 -0
  80. package/src/components/booking/useChangeBookingAutoSelections.ts +148 -0
  81. package/src/components/booking/useChangeBookingAvailabilities.ts +692 -0
  82. package/src/components/booking/useChangeBookingCheckoutController.ts +461 -0
  83. package/src/components/booking/useChangeBookingInitialHydration.ts +204 -0
  84. package/src/components/booking/useChangeBookingItineraryUiState.ts +133 -0
  85. package/src/components/booking/useChangeBookingPickupItinerary.ts +472 -0
  86. package/src/components/booking/useChangeBookingPriceSummary.ts +199 -0
  87. package/src/components/booking/useChangeBookingPromoDiscount.ts +218 -0
  88. package/src/components/booking/useChangeBookingProtectedPricing.ts +256 -0
  89. package/src/components/booking/useChangeBookingQuotePreview.ts +442 -0
  90. package/src/components/booking/useChangeBookingReceiptDerivation.ts +128 -0
  91. package/src/components/booking/useChangeBookingReceiptFloorController.ts +449 -0
  92. package/src/components/booking/useChangeBookingSelection.ts +362 -0
  93. package/src/components/booking/useChangeBookingSelectionDetails.ts +200 -0
  94. package/src/components/booking/useChangeBookingSummaryLines.ts +169 -0
  95. package/src/components/booking/usePrivateShuttlePriceSummary.ts +513 -0
  96. package/src/components/booking/usePrivateShuttlePromoController.ts +143 -0
  97. package/src/components/booking/usePrivateShuttlePromoDiscount.ts +124 -0
  98. package/src/components/booking/useSeedPromoCodeInput.ts +13 -0
  99. package/src/components/booking/useStandardBookingAutoSelections.ts +224 -0
  100. package/src/components/booking/useStandardBookingPickupItinerary.ts +473 -0
  101. package/src/components/booking/useStandardBookingPriceSummary.ts +438 -0
  102. package/src/components/booking/useStandardBookingPromoController.ts +258 -0
  103. package/src/components/booking/useStandardBookingPromoDiscount.ts +203 -0
  104. package/src/data/products-config.json +1 -1
  105. package/src/lib/booking/change-booking-server-preview.ts +134 -20
  106. package/src/lib/booking/change-flow-pricing.ts +31 -11
  107. package/src/lib/booking/checkout-breakdown.ts +66 -1
  108. package/src/lib/booking/pricing-config.ts +10 -0
  109. package/src/lib/booking/source-metadata.ts +58 -0
  110. package/src/lib/booking-api.ts +155 -3
  111. package/src/lib/pricing-v2-shadow.ts +272 -0
  112. package/test/change-booking-helpers.test.ts +206 -0
@@ -0,0 +1,199 @@
1
+ import type {
2
+ AddOn,
3
+ Availability,
4
+ PricingConfig,
5
+ ReturnOption,
6
+ } from '../../lib/booking-api';
7
+ import {
8
+ resolveChangeFlowNewBookingTotal,
9
+ type ChangeFlowProtectedReceiptPricing,
10
+ } from '../../lib/booking/change-flow-pricing';
11
+ import type {
12
+ OrderSummaryFeeLine,
13
+ OrderSummaryTicketLine,
14
+ PriceBreakdown as PriceBreakdownData,
15
+ } from '../../lib/booking/pricing';
16
+ import type { ChangeBookingMergedQuoteState } from './change-booking-quote-state';
17
+ import type { ChangeBookingFlowProps } from './booking-flow-types';
18
+ import type { Currency } from './CurrencySwitcher';
19
+ import type { PricingRate } from './change-booking-flow-helpers';
20
+ import { useChangeBookingProtectedPricing } from './useChangeBookingProtectedPricing';
21
+ import { useChangeBookingPromoDiscount } from './useChangeBookingPromoDiscount';
22
+ import { useChangeBookingSummaryLines } from './useChangeBookingSummaryLines';
23
+
24
+ type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
25
+ type GetPriceBreakdown = (
26
+ category: string,
27
+ priceCAD: number,
28
+ baseInDisplayCurrency: number | undefined,
29
+ appliedAdjustments?: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }>,
30
+ ) => PriceBreakdownData | null;
31
+
32
+ interface UseChangeBookingPriceSummaryParams {
33
+ selectedAvailability: Availability | null;
34
+ selectedReturnOption: ReturnOption | null;
35
+ currency: Currency;
36
+ totalQuantity: number;
37
+ returnPriceAdjustment: number;
38
+ isCustomerSelfServeChange: boolean;
39
+ effectiveChangeFlowReturnUnitFloorPerPerson: number | null | undefined;
40
+ changeFlowInitialTicketCount: number;
41
+ changeFlowReturnPricingRuleA: boolean;
42
+ changeFlowApplyReceiptPaidFloors: boolean;
43
+ ticketLineItems: OrderSummaryTicketLine[];
44
+ changeFlowTicketBookedUnitPriceByCategory: Map<string, number>;
45
+ changeFlowInitialTicketQtyByCategory: Map<string, number>;
46
+ changeFlowProtectedReceiptPricing: ChangeFlowProtectedReceiptPricing;
47
+ subtotal: number;
48
+ feeLineItems: OrderSummaryFeeLine[];
49
+ changeFlowBookedFeeUnitByNormalizedLabel: Map<string, number>;
50
+ addOnSelections: AddOnSelection[];
51
+ addOns: AddOn[];
52
+ quantities: Record<string, number>;
53
+ pricing: PricingRate[];
54
+ getPriceBreakdown: GetPriceBreakdown;
55
+ cancellationPolicyFee: number;
56
+ hasSelectedCancellationPolicy: boolean;
57
+ effectiveCancellationPolicyLabel: string;
58
+ t: (key: string) => string;
59
+ appliedPromoCode: string | null;
60
+ productCompanyId?: string | null;
61
+ envCompanyId?: string | null;
62
+ productId: string;
63
+ lockedPromoCode: string | null;
64
+ originalReceipt: ChangeBookingFlowProps['originalReceipt'];
65
+ pricingConfig: PricingConfig | null;
66
+ isTaxIncludedInPrice: boolean;
67
+ suppressSelfServeCurrencyUi: boolean;
68
+ selfServePricingConfirmed: boolean;
69
+ latestChangeQuote: ChangeBookingMergedQuoteState | null;
70
+ }
71
+
72
+ export function useChangeBookingPriceSummary({
73
+ selectedAvailability,
74
+ selectedReturnOption,
75
+ currency,
76
+ totalQuantity,
77
+ returnPriceAdjustment,
78
+ isCustomerSelfServeChange,
79
+ effectiveChangeFlowReturnUnitFloorPerPerson,
80
+ changeFlowInitialTicketCount,
81
+ changeFlowReturnPricingRuleA,
82
+ changeFlowApplyReceiptPaidFloors,
83
+ ticketLineItems,
84
+ changeFlowTicketBookedUnitPriceByCategory,
85
+ changeFlowInitialTicketQtyByCategory,
86
+ changeFlowProtectedReceiptPricing,
87
+ subtotal,
88
+ feeLineItems,
89
+ changeFlowBookedFeeUnitByNormalizedLabel,
90
+ addOnSelections,
91
+ addOns,
92
+ quantities,
93
+ pricing,
94
+ getPriceBreakdown,
95
+ cancellationPolicyFee,
96
+ hasSelectedCancellationPolicy,
97
+ effectiveCancellationPolicyLabel,
98
+ t,
99
+ appliedPromoCode,
100
+ productCompanyId,
101
+ envCompanyId,
102
+ productId,
103
+ lockedPromoCode,
104
+ originalReceipt,
105
+ pricingConfig,
106
+ isTaxIncludedInPrice,
107
+ suppressSelfServeCurrencyUi,
108
+ selfServePricingConfirmed,
109
+ latestChangeQuote,
110
+ }: UseChangeBookingPriceSummaryParams) {
111
+ const {
112
+ checkoutReturnLineAmount,
113
+ ticketLineItemsForChangeFlowDisplay,
114
+ effectiveSubtotal,
115
+ feeLineItemsWithAddOns,
116
+ } = useChangeBookingProtectedPricing({
117
+ selectedAvailability,
118
+ selectedReturnOption,
119
+ currency,
120
+ totalQuantity,
121
+ returnPriceAdjustment,
122
+ isCustomerSelfServeChange,
123
+ effectiveChangeFlowReturnUnitFloorPerPerson,
124
+ changeFlowInitialTicketCount,
125
+ changeFlowReturnPricingRuleA,
126
+ changeFlowApplyReceiptPaidFloors,
127
+ ticketLineItems,
128
+ changeFlowTicketBookedUnitPriceByCategory,
129
+ changeFlowInitialTicketQtyByCategory,
130
+ changeFlowProtectedReceiptPricing,
131
+ subtotal,
132
+ feeLineItems,
133
+ changeFlowBookedFeeUnitByNormalizedLabel,
134
+ addOnSelections,
135
+ addOns,
136
+ });
137
+
138
+ const {
139
+ effectivePromoDiscountAmount,
140
+ effectiveTax,
141
+ totalPrice,
142
+ } = useChangeBookingPromoDiscount({
143
+ appliedPromoCode,
144
+ selectedAvailability,
145
+ totalQuantity,
146
+ productCompanyId,
147
+ envCompanyId,
148
+ productId,
149
+ currency,
150
+ quantities,
151
+ ticketLineItemsForChangeFlowDisplay,
152
+ feeLineItems,
153
+ effectiveSubtotal,
154
+ lockedPromoCode,
155
+ originalReceipt,
156
+ isTaxIncludedInPrice,
157
+ pricingConfig,
158
+ });
159
+
160
+ const {
161
+ checkoutPriceSummaryLinesForCheckout,
162
+ priceSummaryLinesIncludeTaxRow,
163
+ } = useChangeBookingSummaryLines({
164
+ selectedAvailability,
165
+ selectedReturnOption,
166
+ ticketLineItemsForChangeFlowDisplay,
167
+ pricing,
168
+ getPriceBreakdown,
169
+ checkoutReturnLineAmount,
170
+ totalQuantity,
171
+ t,
172
+ cancellationPolicyFee,
173
+ hasSelectedCancellationPolicy,
174
+ effectiveCancellationPolicyLabel,
175
+ feeLineItemsWithAddOns,
176
+ feeLineItems,
177
+ suppressSelfServeCurrencyUi,
178
+ selfServePricingConfirmed,
179
+ latestServerPriceSummaryLines: latestChangeQuote?.serverPreview?.priceSummaryLines,
180
+ quantities,
181
+ });
182
+
183
+ const changeFlowNewBookingTotal = resolveChangeFlowNewBookingTotal({
184
+ cartTotal: totalPrice,
185
+ originalReceiptTotal: originalReceipt?.total,
186
+ });
187
+
188
+ return {
189
+ checkoutReturnLineAmount,
190
+ effectiveSubtotal,
191
+ feeLineItemsWithAddOns,
192
+ checkoutPriceSummaryLinesForCheckout,
193
+ priceSummaryLinesIncludeTaxRow,
194
+ effectivePromoDiscountAmount,
195
+ effectiveTax,
196
+ totalPrice,
197
+ changeFlowNewBookingTotal,
198
+ };
199
+ }
@@ -0,0 +1,218 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ getPromoDiscount,
4
+ type Availability,
5
+ type PricingConfig,
6
+ } from '../../lib/booking-api';
7
+ import type { OrderSummaryFeeLine, OrderSummaryTicketLine } from '../../lib/booking/pricing';
8
+ import type { ChangeBookingFlowProps } from './booking-flow-types';
9
+ import type { Currency } from './CurrencySwitcher';
10
+ import { getAvailabilityOptionId } from './change-booking-flow-helpers';
11
+
12
+ interface UseChangeBookingPromoDiscountParams {
13
+ appliedPromoCode: string | null;
14
+ selectedAvailability: Availability | null;
15
+ totalQuantity: number;
16
+ productCompanyId?: string | null;
17
+ envCompanyId?: string | null;
18
+ productId: string;
19
+ currency: Currency;
20
+ quantities: Record<string, number>;
21
+ ticketLineItemsForChangeFlowDisplay: OrderSummaryTicketLine[];
22
+ feeLineItems: OrderSummaryFeeLine[];
23
+ effectiveSubtotal: number;
24
+ lockedPromoCode: string | null;
25
+ originalReceipt: ChangeBookingFlowProps['originalReceipt'];
26
+ isTaxIncludedInPrice: boolean;
27
+ pricingConfig: PricingConfig | null;
28
+ }
29
+
30
+ export function useChangeBookingPromoDiscount({
31
+ appliedPromoCode,
32
+ selectedAvailability,
33
+ totalQuantity,
34
+ productCompanyId,
35
+ envCompanyId,
36
+ productId,
37
+ currency,
38
+ quantities,
39
+ ticketLineItemsForChangeFlowDisplay,
40
+ feeLineItems,
41
+ effectiveSubtotal,
42
+ lockedPromoCode,
43
+ originalReceipt,
44
+ isTaxIncludedInPrice,
45
+ pricingConfig,
46
+ }: UseChangeBookingPromoDiscountParams) {
47
+ const quantitiesSignature = useMemo(
48
+ () =>
49
+ Object.entries(quantities)
50
+ .filter(([, n]) => (n ?? 0) > 0)
51
+ .sort(([a], [b]) => a.localeCompare(b))
52
+ .map(([c, n]) => `${c}:${n}`)
53
+ .join('|'),
54
+ [quantities],
55
+ );
56
+
57
+ const [promoDiscountAmount, setPromoDiscountAmount] = useState(0);
58
+ const [isGiftCard, setIsGiftCard] = useState(false);
59
+ const [isVoucher, setIsVoucher] = useState(false);
60
+ const promoDiscountFetchGenerationRef = useRef(0);
61
+ const promoDiscountParamsRef = useRef({
62
+ selectedAvailability: null as Availability | null,
63
+ ticketLineItems: [] as Array<{ category: string; qty: number; itemTotal: number }>,
64
+ effectiveSubtotal: 0,
65
+ displayedProductFeeTotal: 0,
66
+ appliedPromoCode: null as string | null,
67
+ changeBookingPromo: null as { priorAmount?: number } | null,
68
+ });
69
+
70
+ const selectedAvailabilityOptionId = useMemo(
71
+ () => (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''),
72
+ [selectedAvailability],
73
+ );
74
+
75
+ const promoDiscountFetchKey = useMemo(() => {
76
+ if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
77
+ const companyId = productCompanyId ?? envCompanyId;
78
+ if (!companyId) return '';
79
+ if (!selectedAvailabilityOptionId || !quantitiesSignature) return '';
80
+ return [
81
+ appliedPromoCode,
82
+ companyId,
83
+ productId,
84
+ selectedAvailabilityOptionId,
85
+ selectedAvailability.dateTime,
86
+ selectedAvailability.availabilityId ?? '',
87
+ currency,
88
+ quantitiesSignature,
89
+ ticketLineItemsForChangeFlowDisplay
90
+ .map((line) => `${line.category}:${line.qty}:${Math.round(line.itemTotal * 100)}`)
91
+ .sort()
92
+ .join('|'),
93
+ String(Math.round(feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0) * 100)),
94
+ String(Math.round(effectiveSubtotal * 100)),
95
+ lockedPromoCode ? String(Math.round((originalReceipt?.promoAmount ?? 0) * 100)) : '',
96
+ ].join('::');
97
+ }, [
98
+ appliedPromoCode,
99
+ selectedAvailability,
100
+ selectedAvailabilityOptionId,
101
+ totalQuantity,
102
+ productCompanyId,
103
+ envCompanyId,
104
+ productId,
105
+ currency,
106
+ quantitiesSignature,
107
+ ticketLineItemsForChangeFlowDisplay,
108
+ feeLineItems,
109
+ effectiveSubtotal,
110
+ lockedPromoCode,
111
+ originalReceipt?.promoAmount,
112
+ ]);
113
+
114
+ promoDiscountParamsRef.current = {
115
+ selectedAvailability,
116
+ ticketLineItems: ticketLineItemsForChangeFlowDisplay.map((line) => ({
117
+ category: line.category,
118
+ qty: line.qty,
119
+ itemTotal: line.itemTotal,
120
+ })),
121
+ effectiveSubtotal,
122
+ displayedProductFeeTotal: feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0),
123
+ appliedPromoCode,
124
+ changeBookingPromo: lockedPromoCode
125
+ ? { priorAmount: originalReceipt?.promoAmount }
126
+ : null,
127
+ };
128
+
129
+ useEffect(() => {
130
+ if (!promoDiscountFetchKey) {
131
+ setPromoDiscountAmount(0);
132
+ setIsGiftCard(false);
133
+ setIsVoucher(false);
134
+ return;
135
+ }
136
+
137
+ let cancelled = false;
138
+ const debounceMs = 120;
139
+ const timer = window.setTimeout(() => {
140
+ if (cancelled) return;
141
+ const {
142
+ selectedAvailability: sel,
143
+ ticketLineItems: lines,
144
+ effectiveSubtotal: sub,
145
+ displayedProductFeeTotal,
146
+ appliedPromoCode: code,
147
+ changeBookingPromo,
148
+ } = promoDiscountParamsRef.current;
149
+ if (!code || !sel) return;
150
+ const companyId = productCompanyId ?? envCompanyId;
151
+ const optionId = getAvailabilityOptionId(sel);
152
+ if (!companyId || !optionId) return;
153
+ const items = lines.map((line) => ({ category: line.category, qty: line.qty }));
154
+ if (items.length === 0) return;
155
+
156
+ const generation = ++promoDiscountFetchGenerationRef.current;
157
+ getPromoDiscount(
158
+ code,
159
+ companyId,
160
+ productId,
161
+ optionId,
162
+ currency,
163
+ items,
164
+ sel.dateTime,
165
+ sub,
166
+ changeBookingPromo
167
+ ? {
168
+ forBookingChange: true,
169
+ priorPromoDiscountAmount: changeBookingPromo.priorAmount,
170
+ displayedTicketLines: lines,
171
+ displayedProductFeeTotal,
172
+ }
173
+ : {
174
+ displayedTicketLines: lines,
175
+ displayedProductFeeTotal,
176
+ },
177
+ )
178
+ .then((res) => {
179
+ if (cancelled) return;
180
+ if (generation !== promoDiscountFetchGenerationRef.current) return;
181
+ setPromoDiscountAmount(res.discount ?? 0);
182
+ setIsGiftCard(res.isGiftCard ?? false);
183
+ setIsVoucher(res.isVoucher ?? false);
184
+ })
185
+ .catch(() => {
186
+ if (cancelled) return;
187
+ if (generation !== promoDiscountFetchGenerationRef.current) return;
188
+ setPromoDiscountAmount(0);
189
+ setIsGiftCard(false);
190
+ setIsVoucher(false);
191
+ });
192
+ }, debounceMs);
193
+
194
+ return () => {
195
+ cancelled = true;
196
+ window.clearTimeout(timer);
197
+ };
198
+ }, [promoDiscountFetchKey, productCompanyId, envCompanyId, productId, currency]);
199
+
200
+ const lockedPromoFallbackAmount =
201
+ lockedPromoCode
202
+ ? Math.max(0, originalReceipt?.promoAmount ?? 0)
203
+ : 0;
204
+ const effectivePromoDiscountAmount =
205
+ promoDiscountAmount > 0 ? promoDiscountAmount : lockedPromoFallbackAmount;
206
+ const taxOnSubtotal = isTaxIncludedInPrice ? 0 : effectiveSubtotal * (pricingConfig?.taxRate ?? 0);
207
+ const effectiveTax =
208
+ effectivePromoDiscountAmount > 0 && !isGiftCard && !isVoucher
209
+ ? (effectiveSubtotal - effectivePromoDiscountAmount) * (pricingConfig?.taxRate ?? 0)
210
+ : taxOnSubtotal;
211
+ const totalPrice = effectiveSubtotal + effectiveTax - effectivePromoDiscountAmount;
212
+
213
+ return {
214
+ effectivePromoDiscountAmount,
215
+ effectiveTax,
216
+ totalPrice,
217
+ };
218
+ }
@@ -0,0 +1,256 @@
1
+ import { useMemo } from 'react';
2
+ import type { AddOn, Availability, ReturnOption } from '../../lib/booking-api';
3
+ import {
4
+ changeFlowFeeLineTotalWithReceiptFloor,
5
+ changeFlowReturnLineTotalWithReceiptFloor,
6
+ changeFlowTicketLineTotalWithReceiptFloor,
7
+ type ChangeFlowProtectedReceiptPricing,
8
+ } from '../../lib/booking/change-flow-pricing';
9
+ import type {
10
+ OrderSummaryFeeLine,
11
+ OrderSummaryTicketLine,
12
+ } from '../../lib/booking/pricing';
13
+ import type { Currency } from './CurrencySwitcher';
14
+ import { normalizeLineLabelForCompare } from './change-booking-flow-helpers';
15
+
16
+ type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
17
+
18
+ interface UseChangeBookingProtectedPricingParams {
19
+ selectedAvailability: Availability | null;
20
+ selectedReturnOption: ReturnOption | null;
21
+ currency: Currency;
22
+ totalQuantity: number;
23
+ returnPriceAdjustment: number;
24
+ isCustomerSelfServeChange: boolean;
25
+ effectiveChangeFlowReturnUnitFloorPerPerson: number | null | undefined;
26
+ changeFlowInitialTicketCount: number;
27
+ changeFlowReturnPricingRuleA: boolean;
28
+ changeFlowApplyReceiptPaidFloors: boolean;
29
+ ticketLineItems: OrderSummaryTicketLine[];
30
+ changeFlowTicketBookedUnitPriceByCategory: Map<string, number>;
31
+ changeFlowInitialTicketQtyByCategory: Map<string, number>;
32
+ changeFlowProtectedReceiptPricing: ChangeFlowProtectedReceiptPricing;
33
+ subtotal: number;
34
+ feeLineItems: OrderSummaryFeeLine[];
35
+ changeFlowBookedFeeUnitByNormalizedLabel: Map<string, number>;
36
+ addOnSelections: AddOnSelection[];
37
+ addOns: AddOn[];
38
+ }
39
+
40
+ export function useChangeBookingProtectedPricing({
41
+ selectedAvailability,
42
+ selectedReturnOption,
43
+ currency,
44
+ totalQuantity,
45
+ returnPriceAdjustment,
46
+ isCustomerSelfServeChange,
47
+ effectiveChangeFlowReturnUnitFloorPerPerson,
48
+ changeFlowInitialTicketCount,
49
+ changeFlowReturnPricingRuleA,
50
+ changeFlowApplyReceiptPaidFloors,
51
+ ticketLineItems,
52
+ changeFlowTicketBookedUnitPriceByCategory,
53
+ changeFlowInitialTicketQtyByCategory,
54
+ changeFlowProtectedReceiptPricing,
55
+ subtotal,
56
+ feeLineItems,
57
+ changeFlowBookedFeeUnitByNormalizedLabel,
58
+ addOnSelections,
59
+ addOns,
60
+ }: UseChangeBookingProtectedPricingParams) {
61
+ const changeFlowProtectedTicketSubtotal = useMemo(() => {
62
+ const currentTicketSubtotal = ticketLineItems.reduce(
63
+ (sum, line) => sum + Math.max(0, Number(line.itemTotal) || 0),
64
+ 0,
65
+ );
66
+ if (!changeFlowApplyReceiptPaidFloors) return currentTicketSubtotal;
67
+ return ticketLineItems.reduce((sum, line) => {
68
+ const category = line.category?.trim().toUpperCase();
69
+ const qty = Math.max(0, Number(line.qty) || 0);
70
+ if (!category || qty <= 0) return sum;
71
+ return (
72
+ sum +
73
+ changeFlowTicketLineTotalWithReceiptFloor({
74
+ qty,
75
+ baselineQtyForCategory: changeFlowInitialTicketQtyByCategory.get(category) ?? 0,
76
+ receiptUnitFloor: changeFlowTicketBookedUnitPriceByCategory.get(category),
77
+ liveLineTotal: Number(line.itemTotal) || 0,
78
+ protectedReceiptPricing: changeFlowProtectedReceiptPricing,
79
+ })
80
+ );
81
+ }, 0);
82
+ }, [
83
+ changeFlowApplyReceiptPaidFloors,
84
+ ticketLineItems,
85
+ changeFlowTicketBookedUnitPriceByCategory,
86
+ changeFlowInitialTicketQtyByCategory,
87
+ changeFlowProtectedReceiptPricing,
88
+ ]);
89
+
90
+ const addOnTotal = useMemo(() => {
91
+ let sum = 0;
92
+ for (const sel of addOnSelections) {
93
+ const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
94
+ if (!addOn) continue;
95
+ const basePrice = addOn.price ?? 0;
96
+ const hasVariant =
97
+ (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
98
+ sel.variantId;
99
+ const variantAdjustment = hasVariant
100
+ ? (addOn.variants?.find((v) => v.id === sel.variantId)?.priceAdjustment ?? 0)
101
+ : 0;
102
+ sum += (basePrice + variantAdjustment) * (sel.quantity ?? 1);
103
+ }
104
+ return sum;
105
+ }, [addOnSelections, addOns]);
106
+
107
+ const currentTicketSubtotal = useMemo(
108
+ () => ticketLineItems.reduce((sum, line) => sum + Math.max(0, Number(line.itemTotal) || 0), 0),
109
+ [ticketLineItems],
110
+ );
111
+
112
+ const currentFeeSubtotal = useMemo(
113
+ () => feeLineItems.reduce((sum, line) => sum + Math.max(0, Number(line.totalAmount) || 0), 0),
114
+ [feeLineItems],
115
+ );
116
+
117
+ const changeFlowProtectedFeeSubtotal = useMemo(() => {
118
+ if (!changeFlowApplyReceiptPaidFloors || totalQuantity <= 0) return currentFeeSubtotal;
119
+ const initialParty = changeFlowInitialTicketCount;
120
+ return feeLineItems.reduce((sum, line) => {
121
+ const key = normalizeLineLabelForCompare(line.name || '');
122
+ const bookedFeePerPerson = key ? changeFlowBookedFeeUnitByNormalizedLabel.get(key) : undefined;
123
+ return (
124
+ sum +
125
+ changeFlowFeeLineTotalWithReceiptFloor({
126
+ totalQuantity,
127
+ initialTicketCount: initialParty,
128
+ bookedFeePerPerson,
129
+ liveFeeLineTotal: Number(line.totalAmount) || 0,
130
+ protectedReceiptPricing: changeFlowProtectedReceiptPricing,
131
+ })
132
+ );
133
+ }, 0);
134
+ }, [
135
+ changeFlowApplyReceiptPaidFloors,
136
+ totalQuantity,
137
+ currentFeeSubtotal,
138
+ feeLineItems,
139
+ changeFlowBookedFeeUnitByNormalizedLabel,
140
+ changeFlowInitialTicketCount,
141
+ changeFlowProtectedReceiptPricing,
142
+ ]);
143
+
144
+ const returnOptionCatalogPerPerson = useMemo(() => {
145
+ if (!selectedReturnOption?.returnAvailabilityId || !selectedAvailability?.returnOptions?.length) {
146
+ return null;
147
+ }
148
+ const raw = selectedAvailability.returnOptions.find(
149
+ (o) => o.returnAvailabilityId === selectedReturnOption.returnAvailabilityId,
150
+ );
151
+ const v = raw?.priceAdjustmentByCurrency?.[currency];
152
+ return typeof v === 'number' && Number.isFinite(v) ? v : null;
153
+ }, [selectedReturnOption?.returnAvailabilityId, selectedAvailability?.returnOptions, currency]);
154
+
155
+ const changeFlowProtectedReturnAdjustment = useMemo(() => {
156
+ if (totalQuantity <= 0) return returnPriceAdjustment;
157
+ if (effectiveChangeFlowReturnUnitFloorPerPerson == null) return returnPriceAdjustment;
158
+ const livePerPerson =
159
+ returnOptionCatalogPerPerson ?? (selectedReturnOption?.priceAdjustmentByCurrency?.[currency] ?? 0);
160
+ return changeFlowReturnLineTotalWithReceiptFloor({
161
+ totalPersons: totalQuantity,
162
+ baselineParty: changeFlowInitialTicketCount,
163
+ livePerPerson,
164
+ receiptFloorPerPerson: effectiveChangeFlowReturnUnitFloorPerPerson,
165
+ returnRuleA: changeFlowReturnPricingRuleA,
166
+ });
167
+ }, [
168
+ totalQuantity,
169
+ returnPriceAdjustment,
170
+ effectiveChangeFlowReturnUnitFloorPerPerson,
171
+ selectedReturnOption,
172
+ returnOptionCatalogPerPerson,
173
+ currency,
174
+ changeFlowInitialTicketCount,
175
+ changeFlowReturnPricingRuleA,
176
+ ]);
177
+
178
+ const checkoutReturnLineAmount = useMemo(() => {
179
+ if (isCustomerSelfServeChange) return changeFlowProtectedReturnAdjustment;
180
+ if (changeFlowApplyReceiptPaidFloors) return changeFlowProtectedReturnAdjustment;
181
+ return returnPriceAdjustment;
182
+ }, [
183
+ isCustomerSelfServeChange,
184
+ changeFlowApplyReceiptPaidFloors,
185
+ changeFlowProtectedReturnAdjustment,
186
+ returnPriceAdjustment,
187
+ ]);
188
+
189
+ const ticketLineItemsForChangeFlowDisplay = useMemo(() => {
190
+ if (!changeFlowApplyReceiptPaidFloors) return ticketLineItems;
191
+ return ticketLineItems.map((line) => {
192
+ const category = line.category?.trim().toUpperCase();
193
+ const qty = Math.max(0, Number(line.qty) || 0);
194
+ if (!category || qty <= 0) return line;
195
+ const newTotal = changeFlowTicketLineTotalWithReceiptFloor({
196
+ qty,
197
+ baselineQtyForCategory: changeFlowInitialTicketQtyByCategory.get(category) ?? 0,
198
+ receiptUnitFloor: changeFlowTicketBookedUnitPriceByCategory.get(category),
199
+ liveLineTotal: Number(line.itemTotal) || 0,
200
+ protectedReceiptPricing: changeFlowProtectedReceiptPricing,
201
+ });
202
+ return { ...line, itemTotal: newTotal };
203
+ });
204
+ }, [
205
+ changeFlowApplyReceiptPaidFloors,
206
+ ticketLineItems,
207
+ changeFlowTicketBookedUnitPriceByCategory,
208
+ changeFlowInitialTicketQtyByCategory,
209
+ changeFlowProtectedReceiptPricing,
210
+ ]);
211
+
212
+ const effectiveSubtotalBeforeAddOns =
213
+ changeFlowApplyReceiptPaidFloors
214
+ ? subtotal -
215
+ currentTicketSubtotal -
216
+ currentFeeSubtotal -
217
+ returnPriceAdjustment +
218
+ changeFlowProtectedTicketSubtotal +
219
+ changeFlowProtectedFeeSubtotal +
220
+ changeFlowProtectedReturnAdjustment
221
+ : subtotal;
222
+ const effectiveSubtotal = effectiveSubtotalBeforeAddOns + addOnTotal;
223
+
224
+ const feeLineItemsWithAddOns = useMemo(() => {
225
+ const addOnLines = addOnSelections
226
+ .map((sel) => {
227
+ const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
228
+ if (!addOn) return null;
229
+ const base = addOn.price ?? 0;
230
+ const hasVariant =
231
+ (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
232
+ sel.variantId;
233
+ const adj = hasVariant
234
+ ? (addOn.variants?.find((v) => v.id === sel.variantId)?.priceAdjustment ?? 0)
235
+ : 0;
236
+ const qty = sel.quantity ?? 1;
237
+ const amt = (base + adj) * qty;
238
+ const variantLabel = hasVariant
239
+ ? addOn.variants?.find((v) => v.id === sel.variantId)?.label
240
+ : null;
241
+ const name = variantLabel
242
+ ? `${addOn.name} (${variantLabel})${qty > 1 ? ` \u00d7 ${qty}` : ''}`
243
+ : addOn.name;
244
+ return { name, totalAmount: amt, description: addOn.description ?? undefined };
245
+ })
246
+ .filter((x): x is NonNullable<typeof x> => x != null);
247
+ return [...feeLineItems, ...addOnLines];
248
+ }, [feeLineItems, addOnSelections, addOns]);
249
+
250
+ return {
251
+ checkoutReturnLineAmount,
252
+ ticketLineItemsForChangeFlowDisplay,
253
+ effectiveSubtotal,
254
+ feeLineItemsWithAddOns,
255
+ };
256
+ }