@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,305 @@
1
+ import { addWeeks, parseISO } from 'date-fns';
2
+ import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
3
+
4
+ import type {
5
+ Availability,
6
+ GetAvailabilitiesResponse,
7
+ PrecomputedPricesByCategory,
8
+ PricingConfig,
9
+ Product,
10
+ ReturnOption,
11
+ } from '../../lib/booking-api';
12
+ import {
13
+ EARLIEST_AVAILABILITY_DATE,
14
+ INITIAL_FETCH_WEEKS,
15
+ LATEST_AVAILABILITY_DATE,
16
+ VISIBLE_RANGE_BUFFER_WEEKS,
17
+ } from '../../lib/booking-constants';
18
+ import {
19
+ computePriceBreakdown,
20
+ getDisplayPriceFromBaseInDisplayCurrency,
21
+ } from '../../lib/booking/pricing';
22
+ import type { Currency } from './CurrencySwitcher';
23
+
24
+ export function findMergedAvailabilityForSelection(
25
+ merged: Availability[],
26
+ selected: Availability | null
27
+ ): Availability | undefined {
28
+ if (!selected) return undefined;
29
+ const optId = getAvailabilityOptionId(selected) || undefined;
30
+ const dt = selected.dateTime;
31
+ const availId = selected.availabilityId?.trim();
32
+ if (availId && optId) {
33
+ const exact = merged.find(
34
+ (a) =>
35
+ a.availabilityId === availId &&
36
+ getAvailabilityOptionId(a) === optId
37
+ );
38
+ if (exact) return exact;
39
+ }
40
+ return merged.find((a) => a.dateTime === dt && getAvailabilityOptionId(a) === optId);
41
+ }
42
+
43
+ export function getAvailabilityOptionId(availability: Availability, fallbackOptionId?: string): string {
44
+ const productOptionId = availability.productOptionId?.trim();
45
+ if (productOptionId) return productOptionId;
46
+ const productId = availability.productId?.trim();
47
+ if (productId?.startsWith('po_')) return productId;
48
+ return fallbackOptionId?.trim() ?? '';
49
+ }
50
+
51
+ export function availabilityWithOptionId(availability: Availability, fallbackOptionId?: string): Availability {
52
+ const productOptionId = getAvailabilityOptionId(availability, fallbackOptionId);
53
+ return productOptionId ? { ...availability, productOptionId } : availability;
54
+ }
55
+
56
+ /** Prefer full availability rows over calendar summary rows when merging fetches. */
57
+ export function mergeAvailabilityRecord(
58
+ existing: Availability | undefined,
59
+ incoming: Availability,
60
+ ): Availability {
61
+ if (!existing) return incoming;
62
+ const existingIsSummary = existing.isSummary === true;
63
+ const incomingIsSummary = incoming.isSummary === true;
64
+ if (existingIsSummary && !incomingIsSummary) return incoming;
65
+ if (!existingIsSummary && incomingIsSummary) return existing;
66
+ return incoming;
67
+ }
68
+
69
+ export function mergeAvailabilitiesIntoMap(
70
+ map: Map<string, Availability>,
71
+ availabilities: Availability[],
72
+ ): void {
73
+ for (const availability of availabilities) {
74
+ const key = getAvailabilityCacheKey(availability);
75
+ map.set(key, mergeAvailabilityRecord(map.get(key), availability));
76
+ }
77
+ }
78
+
79
+ export function getAvailabilityCacheKey(availability: Availability): string {
80
+ return `${availability.dateTime}-${getAvailabilityOptionId(availability)}`;
81
+ }
82
+
83
+ export function pickDefaultAvailabilityForTimes(
84
+ times: Availability[],
85
+ activeOptions: Product['options'],
86
+ ): Availability | null {
87
+ if (times.length === 0) return null;
88
+ const mostPopularOption = activeOptions.find((opt) => opt.mostPopular);
89
+ const candidate = mostPopularOption
90
+ ? times.find(
91
+ (avail) => getAvailabilityOptionId(avail) === mostPopularOption.optionId && avail.vacancies > 0,
92
+ )
93
+ : null;
94
+ const fallback = times.find((avail) => avail.vacancies > 0);
95
+ return candidate ?? fallback ?? null;
96
+ }
97
+
98
+ export function availabilityMatchesSelectedDate(
99
+ availability: Availability,
100
+ selectedDate: string,
101
+ companyTimezone: string,
102
+ ): boolean {
103
+ try {
104
+ const dateInCompanyTz = formatInTimeZone(
105
+ parseAvailabilityDateTime(availability.dateTime),
106
+ companyTimezone,
107
+ 'yyyy-MM-dd',
108
+ );
109
+ return dateInCompanyTz === selectedDate;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ export function shouldSyncSelectedAvailability(
116
+ current: Availability,
117
+ updated: Availability,
118
+ ): boolean {
119
+ return (
120
+ current !== updated &&
121
+ (current.isSummary !== updated.isSummary ||
122
+ current.vacancies !== updated.vacancies ||
123
+ current.availabilityId !== updated.availabilityId ||
124
+ (current.returnOptions?.length ?? 0) !== (updated.returnOptions?.length ?? 0))
125
+ );
126
+ }
127
+
128
+ export function mergePrecomputedPricesFromAvailabilityResult(
129
+ next: Record<string, PrecomputedPricesByCategory>,
130
+ result: GetAvailabilitiesResponse,
131
+ fallbackOptionId?: string,
132
+ ) {
133
+ if (result.precomputedPricesByOption) {
134
+ Object.entries(result.precomputedPricesByOption).forEach(([optionId, prices]) => {
135
+ if (prices && Object.keys(prices).length > 0) next[optionId] = prices;
136
+ });
137
+ }
138
+ if (fallbackOptionId && result.precomputedPrices && Object.keys(result.precomputedPrices).length > 0) {
139
+ next[fallbackOptionId] = result.precomputedPrices;
140
+ }
141
+ }
142
+
143
+ /** Ticket rates for one availability row - mirrors main cart pricing so baseline slots match BE. */
144
+ export function buildPricingFromAvailability(
145
+ selectedAvailability: Availability | null,
146
+ activeOptions: Product['options'],
147
+ precomputedPricesByOption: Record<string, PrecomputedPricesByCategory> | null | undefined,
148
+ currency: Currency,
149
+ pricingConfig: PricingConfig | null,
150
+ hasFees: boolean,
151
+ isSimplifiedPricingView: boolean,
152
+ ) {
153
+ if (!selectedAvailability || !pricingConfig) return [];
154
+ const optionId = getAvailabilityOptionId(selectedAvailability);
155
+ const selectedOption = activeOptions.find((opt) => opt.optionId === optionId);
156
+ const precomputed = optionId ? precomputedPricesByOption?.[optionId] : undefined;
157
+ const rateToDisplayPrice = (backendInDisplayCurrency: number) =>
158
+ getDisplayPriceFromBaseInDisplayCurrency(backendInDisplayCurrency, currency, pricingConfig, hasFees);
159
+ const getBaseInDisplayCurrency = (category: string) => {
160
+ const fromPrecomputed = precomputed?.[category]?.[currency];
161
+ if (fromPrecomputed != null) return fromPrecomputed;
162
+ return 0;
163
+ };
164
+ const buildRate = (
165
+ category: string,
166
+ backendPriceCAD: number,
167
+ backendInDisplayCurrency: number,
168
+ baseInDisplayCurrency: number,
169
+ appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }>,
170
+ ) => {
171
+ const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
172
+ const hasOptionScopedPrice = baseInDisplayCurrency > 0;
173
+ const priceCADForBreakdown =
174
+ hasOptionScopedPrice && currency === 'CAD' ? baseInDisplayCurrency : backendPriceCAD;
175
+ const displayCurrencyPriceForFallback = hasOptionScopedPrice
176
+ ? baseInDisplayCurrency
177
+ : backendInDisplayCurrency;
178
+ const isPublicMode = isSimplifiedPricingView;
179
+ const breakdown = computePriceBreakdown(
180
+ pricingConfig,
181
+ currency,
182
+ priceCADForBreakdown,
183
+ basePriceCAD,
184
+ hasFees,
185
+ appliedAdjustments,
186
+ undefined,
187
+ baseInDisplayCurrency,
188
+ isPublicMode,
189
+ );
190
+ const price = breakdown?.finalPrice ?? rateToDisplayPrice(displayCurrencyPriceForFallback);
191
+ return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: priceCADForBreakdown };
192
+ };
193
+ return (
194
+ selectedAvailability.rates?.map((rate) => {
195
+ const backendPriceCAD = rate.price ?? 0;
196
+ const backendInDisplayCurrency =
197
+ rate.priceByCurrency?.[currency] ?? (currency === 'CAD' ? backendPriceCAD : 0);
198
+ const baseInDisplayCurrency = getBaseInDisplayCurrency(rate.category);
199
+ const built = buildRate(
200
+ rate.category,
201
+ backendPriceCAD,
202
+ backendInDisplayCurrency,
203
+ baseInDisplayCurrency,
204
+ rate.appliedAdjustments ?? rate.applied_adjustments ?? [],
205
+ );
206
+ return {
207
+ category: rate.category,
208
+ rateId: rate.rateId || rate.category,
209
+ available: rate.available,
210
+ price: built.price,
211
+ priceCAD: built.priceCAD,
212
+ baseInDisplayCurrency: built.baseInDisplayCurrency,
213
+ appliedAdjustments: built.appliedAdjustments,
214
+ };
215
+ }) ||
216
+ selectedAvailability.pricesByCategory?.retailPrices?.map((p) => {
217
+ const priceCADFromApi = p.price / 100;
218
+ const baseInDisplayCurrency = getBaseInDisplayCurrency(p.category);
219
+ const backendInDisplayCurrency = currency === 'CAD' ? priceCADFromApi : 0;
220
+ const built = buildRate(p.category, priceCADFromApi, backendInDisplayCurrency, baseInDisplayCurrency, []);
221
+ return {
222
+ category: p.category,
223
+ rateId: p.category,
224
+ available: selectedAvailability.vacancies,
225
+ price: built.price,
226
+ priceCAD: built.priceCAD,
227
+ baseInDisplayCurrency: built.baseInDisplayCurrency,
228
+ appliedAdjustments: built.appliedAdjustments,
229
+ };
230
+ }) ||
231
+ []
232
+ );
233
+ }
234
+
235
+ export function findMergedReturnVacancies(
236
+ outbound: Availability | undefined,
237
+ selectedReturn: ReturnOption | null | undefined
238
+ ): number | null {
239
+ if (!selectedReturn?.returnAvailabilityId || !outbound?.returnOptions?.length) return null;
240
+ const ro = outbound.returnOptions.find(
241
+ (r) => r.returnAvailabilityId === selectedReturn.returnAvailabilityId
242
+ );
243
+ return ro ? ro.vacancies : null;
244
+ }
245
+
246
+ export function normalizeAddOnSelections(
247
+ selections: Array<{ addOnId: string; variantId?: string; quantity?: number }>
248
+ ): Array<{ addOnId: string; variantId?: string; quantity?: number }> {
249
+ const qtyByKey = new Map<string, number>();
250
+ for (const sel of selections) {
251
+ const addOnId = sel.addOnId?.trim();
252
+ if (!addOnId) continue;
253
+ const variantId = sel.variantId?.trim() || '';
254
+ const key = `${addOnId}::${variantId}`;
255
+ qtyByKey.set(key, (qtyByKey.get(key) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
256
+ }
257
+ return Array.from(qtyByKey.entries()).map(([key, qty]) => {
258
+ const [addOnId, variantIdRaw] = key.split('::');
259
+ const variantId = variantIdRaw?.trim() || undefined;
260
+ return { addOnId, variantId, quantity: qty };
261
+ });
262
+ }
263
+
264
+ export function parseAvailabilityDateTime(value: string): Date {
265
+ // If API omits timezone offset, treat it as UTC to prevent user-local day shifts.
266
+ const hasExplicitOffset = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(value);
267
+ return parseISO(hasExplicitOffset ? value : `${value}Z`);
268
+ }
269
+
270
+ export function getStartOfCurrentDay(companyTimezone: string): Date {
271
+ const todayStr = formatInTimeZone(new Date(), companyTimezone, 'yyyy-MM-dd');
272
+ return fromZonedTime(parseISO(`${todayStr}T00:00:00.000`), companyTimezone);
273
+ }
274
+
275
+ export function getInitialAvailabilityRangeStart(companyTimezone: string): Date {
276
+ const todayStr = formatInTimeZone(new Date(), companyTimezone, 'yyyy-MM-dd');
277
+ const seasonStartStr = formatInTimeZone(EARLIEST_AVAILABILITY_DATE, companyTimezone, 'yyyy-MM-dd');
278
+ const seasonEndStr = formatInTimeZone(LATEST_AVAILABILITY_DATE, companyTimezone, 'yyyy-MM-dd');
279
+ const initialDateStr =
280
+ todayStr < seasonStartStr
281
+ ? seasonStartStr
282
+ : todayStr > seasonEndStr
283
+ ? seasonStartStr
284
+ : todayStr;
285
+
286
+ return fromZonedTime(parseISO(`${initialDateStr}T00:00:00.000`), companyTimezone);
287
+ }
288
+
289
+ export function getInitialAvailabilityRangeEnd(initialStart: Date): Date {
290
+ return addWeeks(initialStart, INITIAL_FETCH_WEEKS + VISIBLE_RANGE_BUFFER_WEEKS);
291
+ }
292
+
293
+ export function deriveDefaultQuantitiesFromAvailabilities(
294
+ availabilities: Availability[]
295
+ ): Record<string, number> | null {
296
+ const firstWithRates = availabilities.find((avail) => avail.rates && avail.rates.length > 0);
297
+ if (!firstWithRates?.rates?.length) return null;
298
+
299
+ const initialQuantities: Record<string, number> = {};
300
+ firstWithRates.rates.forEach((rate) => {
301
+ initialQuantities[rate.category] = rate.category === 'ADULT' ? 1 : 0;
302
+ });
303
+
304
+ return Object.keys(initialQuantities).length > 0 ? initialQuantities : null;
305
+ }
@@ -0,0 +1,289 @@
1
+ import type {
2
+ AddOn,
3
+ CheckoutBreakdown,
4
+ PricingV2QuoteSnapshot,
5
+ } from '../../lib/booking-api';
6
+ import type { CheckoutLineInput } from '../../lib/booking/checkout-breakdown';
7
+ import {
8
+ buildCheckoutBreakdown,
9
+ buildCheckoutBreakdownFromPricingV2Quote,
10
+ } from '../../lib/booking/checkout-breakdown';
11
+ import type {
12
+ OrderSummaryFeeLine,
13
+ OrderSummaryTicketLine,
14
+ PriceBreakdown as PriceBreakdownData,
15
+ } from '../../lib/booking/pricing';
16
+ import type { CheckoutModalLineItem, CheckoutModalSummaryOverride } from './CheckoutModal';
17
+ import type { PriceSummaryLine } from './PriceSummary';
18
+ import { buildCheckoutModalSummaryFromPricingV2Quote } from './pricing-v2-checkout-summary';
19
+
20
+ type AppliedAdjustment = {
21
+ type: string;
22
+ id: string;
23
+ name: string;
24
+ changeByCurrency?: Record<string, number>;
25
+ };
26
+
27
+ export type StandardBookingAddOnSelection = {
28
+ addOnId: string;
29
+ variantId?: string;
30
+ quantity?: number;
31
+ };
32
+
33
+ export type StandardBookingPricingLine = {
34
+ category: string;
35
+ priceCAD: number;
36
+ baseInDisplayCurrency?: number;
37
+ appliedAdjustments?: AppliedAdjustment[];
38
+ };
39
+
40
+ export interface BuildStandardBookingCheckoutArtifactsParams {
41
+ ticketLineItems: OrderSummaryTicketLine[];
42
+ pricing: StandardBookingPricingLine[];
43
+ getPriceBreakdown: (
44
+ category: string,
45
+ priceCAD: number,
46
+ baseInDisplayCurrency: number | undefined,
47
+ appliedAdjustments?: AppliedAdjustment[]
48
+ ) => PriceBreakdownData | null;
49
+ totalQuantity: number;
50
+ checkoutReturnLineAmount: number;
51
+ returnOptionLabel: string;
52
+ personLabel: string;
53
+ peopleLabel: string;
54
+ cancellationPolicyFee: number;
55
+ cancellationPolicyLabel: string;
56
+ addOnSelections: StandardBookingAddOnSelection[];
57
+ addOns: AddOn[];
58
+ feeLineItems: OrderSummaryFeeLine[];
59
+ feeLineItemsWithAddOns: OrderSummaryFeeLine[];
60
+ priceSummaryLines?: PriceSummaryLine[];
61
+ checkoutPriceSummaryLineCount: number;
62
+ isTaxIncludedInPrice: boolean;
63
+ taxForBreakdown: number;
64
+ taxLabel: string;
65
+ effectivePromoDiscountAmount: number;
66
+ appliedPromoCode?: string | null;
67
+ discountLabel: string;
68
+ promoLineType: 'GIFT_CARD' | 'PROMO_CODE';
69
+ displayTotalPrice: number;
70
+ currency: string;
71
+ pricingV2Quote?: PricingV2QuoteSnapshot | null;
72
+ roundingLabel: string;
73
+ }
74
+
75
+ export interface StandardBookingCheckoutArtifacts {
76
+ checkoutBreakdown: CheckoutBreakdown;
77
+ pricingV2SummaryOverride: CheckoutModalSummaryOverride | undefined;
78
+ amountDueForCheckout: number;
79
+ ticketLinesForModal: CheckoutModalLineItem[];
80
+ feeLineItemsForModal: OrderSummaryFeeLine[];
81
+ }
82
+
83
+ function buildStandardBookingAddOnLineItems(
84
+ addOnSelections: StandardBookingAddOnSelection[],
85
+ addOns: AddOn[]
86
+ ): {
87
+ checkoutLines: CheckoutLineInput[];
88
+ modalFeeLineItems: OrderSummaryFeeLine[];
89
+ } {
90
+ const checkoutLines: CheckoutLineInput[] = [];
91
+ const modalFeeLineItems: OrderSummaryFeeLine[] = [];
92
+
93
+ for (const selection of addOnSelections) {
94
+ const addOn = addOns.find((candidate) => candidate.addOnId === selection.addOnId);
95
+ if (!addOn) continue;
96
+
97
+ const hasVariant =
98
+ (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
99
+ selection.variantId;
100
+ const variant = hasVariant
101
+ ? addOn.variants?.find((candidate) => candidate.id === selection.variantId)
102
+ : null;
103
+ const quantity = selection.quantity ?? 1;
104
+ const amount = ((addOn.price ?? 0) + (variant?.priceAdjustment ?? 0)) * quantity;
105
+ const label = variant?.label
106
+ ? `${addOn.name} (${variant.label})${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`
107
+ : addOn.name;
108
+
109
+ checkoutLines.push({
110
+ label,
111
+ amount,
112
+ type: 'FEE',
113
+ quantity,
114
+ });
115
+ modalFeeLineItems.push({
116
+ name: label,
117
+ totalAmount: amount,
118
+ description: addOn.description ?? undefined,
119
+ });
120
+ }
121
+
122
+ return { checkoutLines, modalFeeLineItems };
123
+ }
124
+
125
+ function externalPriceSummaryLineItems(
126
+ priceSummaryLines: PriceSummaryLine[] | undefined,
127
+ checkoutPriceSummaryLineCount: number
128
+ ): Extract<PriceSummaryLine, { kind: 'line' }>[] {
129
+ return (
130
+ priceSummaryLines
131
+ ?.slice(checkoutPriceSummaryLineCount)
132
+ .filter((line): line is Extract<PriceSummaryLine, { kind: 'line' }> => line.kind === 'line') ??
133
+ []
134
+ );
135
+ }
136
+
137
+ function buildStandardBookingTicketLinesForModal({
138
+ ticketLineItems,
139
+ pricing,
140
+ getPriceBreakdown,
141
+ }: Pick<
142
+ BuildStandardBookingCheckoutArtifactsParams,
143
+ 'ticketLineItems' | 'pricing' | 'getPriceBreakdown'
144
+ >): CheckoutModalLineItem[] {
145
+ return ticketLineItems.map((line) => {
146
+ const rate = pricing.find((candidate) => candidate.category === line.category);
147
+ const breakdown = getPriceBreakdown(
148
+ line.category,
149
+ rate?.priceCAD ?? 0,
150
+ rate?.baseInDisplayCurrency,
151
+ rate?.appliedAdjustments ?? []
152
+ );
153
+ return { line, breakdown };
154
+ });
155
+ }
156
+
157
+ export function buildStandardBookingCheckoutArtifacts(
158
+ params: BuildStandardBookingCheckoutArtifactsParams
159
+ ): StandardBookingCheckoutArtifacts {
160
+ const {
161
+ ticketLineItems,
162
+ pricing,
163
+ getPriceBreakdown,
164
+ totalQuantity,
165
+ checkoutReturnLineAmount,
166
+ returnOptionLabel,
167
+ personLabel,
168
+ peopleLabel,
169
+ cancellationPolicyFee,
170
+ cancellationPolicyLabel,
171
+ addOnSelections,
172
+ addOns,
173
+ feeLineItems,
174
+ feeLineItemsWithAddOns,
175
+ priceSummaryLines,
176
+ checkoutPriceSummaryLineCount,
177
+ isTaxIncludedInPrice,
178
+ taxForBreakdown,
179
+ taxLabel,
180
+ effectivePromoDiscountAmount,
181
+ appliedPromoCode,
182
+ discountLabel,
183
+ promoLineType,
184
+ displayTotalPrice,
185
+ currency,
186
+ pricingV2Quote,
187
+ roundingLabel,
188
+ } = params;
189
+
190
+ const personCountLabel = totalQuantity === 1 ? personLabel : peopleLabel;
191
+ const externalSummaryLines = externalPriceSummaryLineItems(
192
+ priceSummaryLines,
193
+ checkoutPriceSummaryLineCount
194
+ );
195
+ const { checkoutLines: addOnCheckoutLines } = buildStandardBookingAddOnLineItems(
196
+ addOnSelections,
197
+ addOns
198
+ );
199
+ const externalCheckoutLines: CheckoutLineInput[] = externalSummaryLines.map((line) => ({
200
+ label: line.label,
201
+ amount: line.amount,
202
+ type: line.type ?? 'FEE',
203
+ quantity: line.quantity ?? undefined,
204
+ }));
205
+ const lines: CheckoutLineInput[] = [
206
+ ...ticketLineItems.map((line) => ({
207
+ label: line.category,
208
+ amount: line.itemTotal,
209
+ type: 'TICKET',
210
+ quantity: line.qty,
211
+ })),
212
+ ...(checkoutReturnLineAmount !== 0
213
+ ? [
214
+ {
215
+ label: `${returnOptionLabel} (${totalQuantity} ${personCountLabel})`,
216
+ amount: checkoutReturnLineAmount,
217
+ type: 'RETURN_OPTION',
218
+ quantity: totalQuantity,
219
+ },
220
+ ]
221
+ : []),
222
+ ...(cancellationPolicyFee > 0
223
+ ? [
224
+ {
225
+ label: cancellationPolicyLabel,
226
+ amount: cancellationPolicyFee,
227
+ type: 'CANCELLATION_UPGRADE',
228
+ },
229
+ ]
230
+ : []),
231
+ ...addOnCheckoutLines,
232
+ ...externalCheckoutLines,
233
+ ...feeLineItems.map((fee) => ({
234
+ label: `${fee.name} (${totalQuantity} ${personCountLabel})`,
235
+ amount: fee.totalAmount,
236
+ type: 'FEE',
237
+ quantity: totalQuantity,
238
+ })),
239
+ ...(!isTaxIncludedInPrice && taxForBreakdown > 0
240
+ ? [
241
+ {
242
+ label: taxLabel,
243
+ amount: taxForBreakdown,
244
+ type: 'TAX',
245
+ },
246
+ ]
247
+ : []),
248
+ ...(effectivePromoDiscountAmount > 0
249
+ ? [
250
+ {
251
+ label: appliedPromoCode ? `Promo: ${appliedPromoCode}` : discountLabel,
252
+ amount: -effectivePromoDiscountAmount,
253
+ type: promoLineType,
254
+ },
255
+ ]
256
+ : []),
257
+ ];
258
+ const legacyCheckoutBreakdown = buildCheckoutBreakdown({
259
+ lines,
260
+ totalAmount: displayTotalPrice,
261
+ currency,
262
+ roundingLabel,
263
+ });
264
+ const checkoutBreakdown =
265
+ buildCheckoutBreakdownFromPricingV2Quote(pricingV2Quote, roundingLabel) ??
266
+ legacyCheckoutBreakdown;
267
+ const pricingV2SummaryOverride = buildCheckoutModalSummaryFromPricingV2Quote(
268
+ pricingV2Quote,
269
+ roundingLabel
270
+ );
271
+ const ticketLinesForModal = buildStandardBookingTicketLinesForModal({
272
+ ticketLineItems,
273
+ pricing,
274
+ getPriceBreakdown,
275
+ });
276
+ const externalModalFeeLineItems: OrderSummaryFeeLine[] = externalSummaryLines.map((line) => ({
277
+ name: line.label,
278
+ totalAmount: line.amount,
279
+ showQuantityLabel: false,
280
+ }));
281
+
282
+ return {
283
+ checkoutBreakdown,
284
+ pricingV2SummaryOverride,
285
+ amountDueForCheckout: checkoutBreakdown.totalAmount,
286
+ ticketLinesForModal,
287
+ feeLineItemsForModal: [...feeLineItemsWithAddOns, ...externalModalFeeLineItems],
288
+ };
289
+ }