@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.
- package/CHANGE_BOOKING_BE_HANDOFF.md +15 -4
- package/package.json +1 -1
- package/src/components/booking/AdminChangeBookingContent.tsx +276 -0
- package/src/components/booking/AdminChangeBookingFlow.tsx +651 -6202
- package/src/components/booking/AdminChangeCheckoutDialogs.tsx +105 -0
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +307 -0
- package/src/components/booking/AdminChangePricingPanel.tsx +244 -0
- package/src/components/booking/AdminChangeProductSwitcher.tsx +50 -0
- package/src/components/booking/AdminChangeReceiptComparison.tsx +167 -0
- package/src/components/booking/ChangeBookingCalendarPanel.tsx +126 -0
- package/src/components/booking/ChangeBookingCheckoutPanel.tsx +204 -0
- package/src/components/booking/ChangeBookingFlow.tsx +576 -4648
- package/src/components/booking/ChangeBookingItineraryPanel.tsx +115 -0
- package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +57 -0
- package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +157 -0
- package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +102 -0
- package/src/components/booking/CheckoutModal.tsx +68 -49
- package/src/components/booking/NewBookingFlow.tsx +485 -3505
- package/src/components/booking/PriceBreakdown.tsx +17 -3
- package/src/components/booking/PriceSummary.tsx +21 -3
- package/src/components/booking/PrivateShuttleAddOnsSection.tsx +102 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +407 -2788
- package/src/components/booking/PrivateShuttleCheckoutDialogs.tsx +106 -0
- package/src/components/booking/PrivateShuttleCheckoutSection.tsx +333 -0
- package/src/components/booking/PrivateShuttleItinerarySection.tsx +95 -0
- package/src/components/booking/PrivateShuttleMediaIntro.tsx +62 -0
- package/src/components/booking/PrivateShuttleOptionSection.tsx +81 -0
- package/src/components/booking/PrivateShuttlePassengerSection.tsx +148 -0
- package/src/components/booking/PrivateShuttlePickupSection.tsx +91 -0
- package/src/components/booking/PrivateShuttlePreferencesSection.tsx +75 -0
- package/src/components/booking/PrivateShuttleStartTimeSection.tsx +88 -0
- package/src/components/booking/StandardBookingCheckoutDialogs.tsx +106 -0
- package/src/components/booking/StandardBookingCheckoutSection.tsx +185 -0
- package/src/components/booking/StandardBookingItineraryPanel.tsx +125 -0
- package/src/components/booking/StandardBookingMediaIntro.tsx +69 -0
- package/src/components/booking/StandardBookingSelectionControlsPanel.tsx +236 -0
- package/src/components/booking/admin-change-customer-quote-runner.ts +119 -0
- package/src/components/booking/admin-change-flow-state-helpers.ts +124 -0
- package/src/components/booking/admin-change-payment-choice-runner.ts +172 -0
- package/src/components/booking/admin-change-provider-payload.ts +101 -0
- package/src/components/booking/change-booking-checkout-builders.ts +185 -0
- package/src/components/booking/change-booking-checkout-quote-runner.ts +207 -0
- package/src/components/booking/change-booking-flow-helpers.ts +706 -0
- package/src/components/booking/change-booking-payment-modal-builders.ts +151 -0
- package/src/components/booking/change-booking-quote-guards.ts +223 -0
- package/src/components/booking/change-booking-quote-state.ts +64 -0
- package/src/components/booking/pricing-v2-checkout-summary.ts +144 -0
- package/src/components/booking/private-shuttle-availability.ts +111 -0
- package/src/components/booking/private-shuttle-checkout-builders.ts +264 -0
- package/src/components/booking/private-shuttle-checkout-controller.ts +744 -0
- package/src/components/booking/private-shuttle-payment-choice-runner.ts +271 -0
- package/src/components/booking/private-shuttle-provider-change-runner.ts +73 -0
- package/src/components/booking/private-shuttle-reservation-runner.ts +260 -0
- package/src/components/booking/standard-booking-availability.ts +305 -0
- package/src/components/booking/standard-booking-checkout-builders.ts +289 -0
- package/src/components/booking/standard-booking-checkout-controller.ts +659 -0
- package/src/components/booking/standard-booking-payment-choice-runner.ts +274 -0
- package/src/components/booking/standard-booking-reservation-runner.ts +229 -0
- package/src/components/booking/use-private-shuttle-availability.ts +602 -0
- package/src/components/booking/use-standard-booking-availability.ts +806 -0
- package/src/components/booking/useActiveProductOptions.ts +26 -0
- package/src/components/booking/useAdminChangeCheckoutController.ts +806 -0
- package/src/components/booking/useAdminChangeOrderSummaryModel.ts +246 -0
- package/src/components/booking/useAdminChangePriceSummary.ts +416 -0
- package/src/components/booking/useAdminChangePricingDebugPanel.tsx +178 -0
- package/src/components/booking/useAdminChangeProductReset.ts +60 -0
- package/src/components/booking/useAdminChangeProducts.ts +102 -0
- package/src/components/booking/useAdminChangePromoDiscount.ts +235 -0
- package/src/components/booking/useAdminChangeProtectedPricing.ts +280 -0
- package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +437 -0
- package/src/components/booking/useAdminChangeQuotePreview.ts +506 -0
- package/src/components/booking/useAdminChangeReceiptDerivation.ts +203 -0
- package/src/components/booking/useAdminCustomReceiptLines.ts +57 -0
- package/src/components/booking/useAdminProviderPricingAdjustments.ts +146 -0
- package/src/components/booking/useBookingAvailabilityAddOns.ts +54 -0
- package/src/components/booking/useBookingPromoAndQuantityController.ts +163 -0
- package/src/components/booking/useBookingQuantityCapTrim.ts +56 -0
- package/src/components/booking/useBookingViewItemAnalytics.ts +30 -0
- package/src/components/booking/useChangeBookingAddOnFloorController.ts +109 -0
- package/src/components/booking/useChangeBookingAutoSelections.ts +148 -0
- package/src/components/booking/useChangeBookingAvailabilities.ts +692 -0
- package/src/components/booking/useChangeBookingCheckoutController.ts +461 -0
- package/src/components/booking/useChangeBookingInitialHydration.ts +204 -0
- package/src/components/booking/useChangeBookingItineraryUiState.ts +133 -0
- package/src/components/booking/useChangeBookingPickupItinerary.ts +472 -0
- package/src/components/booking/useChangeBookingPriceSummary.ts +199 -0
- package/src/components/booking/useChangeBookingPromoDiscount.ts +218 -0
- package/src/components/booking/useChangeBookingProtectedPricing.ts +256 -0
- package/src/components/booking/useChangeBookingQuotePreview.ts +442 -0
- package/src/components/booking/useChangeBookingReceiptDerivation.ts +128 -0
- package/src/components/booking/useChangeBookingReceiptFloorController.ts +449 -0
- package/src/components/booking/useChangeBookingSelection.ts +362 -0
- package/src/components/booking/useChangeBookingSelectionDetails.ts +200 -0
- package/src/components/booking/useChangeBookingSummaryLines.ts +169 -0
- package/src/components/booking/usePrivateShuttlePriceSummary.ts +513 -0
- package/src/components/booking/usePrivateShuttlePromoController.ts +143 -0
- package/src/components/booking/usePrivateShuttlePromoDiscount.ts +124 -0
- package/src/components/booking/useSeedPromoCodeInput.ts +13 -0
- package/src/components/booking/useStandardBookingAutoSelections.ts +224 -0
- package/src/components/booking/useStandardBookingPickupItinerary.ts +473 -0
- package/src/components/booking/useStandardBookingPriceSummary.ts +438 -0
- package/src/components/booking/useStandardBookingPromoController.ts +258 -0
- package/src/components/booking/useStandardBookingPromoDiscount.ts +203 -0
- package/src/data/products-config.json +1 -1
- package/src/lib/booking/change-booking-server-preview.ts +134 -20
- package/src/lib/booking/change-flow-pricing.ts +31 -11
- package/src/lib/booking/checkout-breakdown.ts +66 -1
- package/src/lib/booking/pricing-config.ts +10 -0
- package/src/lib/booking/source-metadata.ts +58 -0
- package/src/lib/booking-api.ts +154 -3
- package/src/lib/pricing-v2-shadow.ts +272 -0
- 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
|
+
}
|