@ticketboothapp/booking 1.2.122 → 1.2.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +647 -6157
- 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
package/src/lib/booking-api.ts
CHANGED
|
@@ -317,6 +317,8 @@ function logBookingSourceDebug(
|
|
|
317
317
|
utmSource?: string;
|
|
318
318
|
utmMedium?: string;
|
|
319
319
|
utmCampaign?: string;
|
|
320
|
+
gaClientId?: string;
|
|
321
|
+
gaSessionId?: string;
|
|
320
322
|
};
|
|
321
323
|
source_metadata?: {
|
|
322
324
|
pagePath?: string;
|
|
@@ -325,12 +327,14 @@ function logBookingSourceDebug(
|
|
|
325
327
|
utmSource?: string;
|
|
326
328
|
utmMedium?: string;
|
|
327
329
|
utmCampaign?: string;
|
|
330
|
+
gaClientId?: string;
|
|
331
|
+
gaSessionId?: string;
|
|
328
332
|
};
|
|
329
333
|
};
|
|
330
334
|
const md = maybe.sourceMetadata ?? maybe.source_metadata;
|
|
331
335
|
if (md) {
|
|
332
336
|
console.info(
|
|
333
|
-
`${prefix} summary source=${maybe.source ?? 'n/a'} path=${md.pagePath ?? 'n/a'} partner=${md.partnerId ?? 'n/a'} utm_source=${md.utmSource ?? 'n/a'} utm_medium=${md.utmMedium ?? 'n/a'} utm_campaign=${md.utmCampaign ?? 'n/a'}`
|
|
337
|
+
`${prefix} summary source=${maybe.source ?? 'n/a'} path=${md.pagePath ?? 'n/a'} partner=${md.partnerId ?? 'n/a'} utm_source=${md.utmSource ?? 'n/a'} utm_medium=${md.utmMedium ?? 'n/a'} utm_campaign=${md.utmCampaign ?? 'n/a'} ga_client_id=${md.gaClientId ? 'present' : 'n/a'} ga_session_id=${md.gaSessionId ? 'present' : 'n/a'}`
|
|
334
338
|
);
|
|
335
339
|
}
|
|
336
340
|
}
|
|
@@ -1186,6 +1190,8 @@ export interface CreatePaymentIntentResponse {
|
|
|
1186
1190
|
clientSecret: string;
|
|
1187
1191
|
amount: number;
|
|
1188
1192
|
currency: string;
|
|
1193
|
+
pricingQuoteId?: string | null;
|
|
1194
|
+
pricingQuoteInputsHash?: string | null;
|
|
1189
1195
|
}
|
|
1190
1196
|
|
|
1191
1197
|
export interface ChangeBookingQuoteRequest {
|
|
@@ -1338,6 +1344,15 @@ export interface ChangeBookingQuoteResponse {
|
|
|
1338
1344
|
newTotalCents?: number;
|
|
1339
1345
|
amountDueCents?: number;
|
|
1340
1346
|
expiresAt?: string;
|
|
1347
|
+
pricingQuoteId?: string;
|
|
1348
|
+
pricingQuoteInputsHash?: string;
|
|
1349
|
+
/** Optional full Pricing V2 quote snapshot when BE prices the change through Pricing V2. */
|
|
1350
|
+
pricingQuote?: PricingV2QuoteSnapshot | null;
|
|
1351
|
+
quote?: PricingV2QuoteSnapshot | null;
|
|
1352
|
+
paymentCreditTotal?: number;
|
|
1353
|
+
balanceDelta?: number;
|
|
1354
|
+
amountToCharge?: number;
|
|
1355
|
+
refundCandidate?: number;
|
|
1341
1356
|
/**
|
|
1342
1357
|
* Optional (customer change-quote UI): per-category ticket unit prices in **major units**, display currency.
|
|
1343
1358
|
* Uppercase keys preferred (ADULT, CHILD, …). Enables full self-serve preview without catalog math.
|
|
@@ -1442,12 +1457,18 @@ export async function cancelBookingPublic(
|
|
|
1442
1457
|
|
|
1443
1458
|
export async function createBalancePaymentIntent(
|
|
1444
1459
|
bookingReference: string,
|
|
1445
|
-
lastName: string
|
|
1460
|
+
lastName: string,
|
|
1461
|
+
pricingQuote?: { pricingQuoteId?: string | null; pricingQuoteInputsHash?: string | null }
|
|
1446
1462
|
): Promise<CreatePaymentIntentResponse> {
|
|
1447
1463
|
const res = await fetch(`${API_BASE}/checkout/balance-payment-intent`, {
|
|
1448
1464
|
method: 'POST',
|
|
1449
1465
|
headers: withBookingOutboundHeaders({ 'Content-Type': 'application/json' }),
|
|
1450
|
-
body: JSON.stringify({
|
|
1466
|
+
body: JSON.stringify({
|
|
1467
|
+
bookingReference,
|
|
1468
|
+
lastName,
|
|
1469
|
+
pricingQuoteId: pricingQuote?.pricingQuoteId,
|
|
1470
|
+
pricingQuoteInputsHash: pricingQuote?.pricingQuoteInputsHash,
|
|
1471
|
+
}),
|
|
1451
1472
|
});
|
|
1452
1473
|
if (!res.ok) {
|
|
1453
1474
|
const err = await res.json();
|
|
@@ -1457,6 +1478,31 @@ export async function createBalancePaymentIntent(
|
|
|
1457
1478
|
return (data.data ?? data) as CreatePaymentIntentResponse;
|
|
1458
1479
|
}
|
|
1459
1480
|
|
|
1481
|
+
export interface CreateBalancePaymentQuoteResponse {
|
|
1482
|
+
amount: number;
|
|
1483
|
+
currency: string;
|
|
1484
|
+
pricingQuoteId: string;
|
|
1485
|
+
pricingQuoteInputsHash: string;
|
|
1486
|
+
expiresAt: string;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
export async function createBalancePaymentQuote(
|
|
1490
|
+
bookingReference: string,
|
|
1491
|
+
lastName: string
|
|
1492
|
+
): Promise<CreateBalancePaymentQuoteResponse> {
|
|
1493
|
+
const res = await fetch(`${API_BASE}/checkout/balance-payment-quote`, {
|
|
1494
|
+
method: 'POST',
|
|
1495
|
+
headers: withBookingOutboundHeaders({ 'Content-Type': 'application/json' }),
|
|
1496
|
+
body: JSON.stringify({ bookingReference, lastName }),
|
|
1497
|
+
});
|
|
1498
|
+
if (!res.ok) {
|
|
1499
|
+
const err = await res.json();
|
|
1500
|
+
throw new Error(err.errorMessage || err.error || 'Failed to price payment');
|
|
1501
|
+
}
|
|
1502
|
+
const data = await res.json();
|
|
1503
|
+
return (data.data ?? data) as CreateBalancePaymentQuoteResponse;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1460
1506
|
export async function quoteChangeBooking(
|
|
1461
1507
|
request: ChangeBookingQuoteRequest
|
|
1462
1508
|
): Promise<ChangeBookingQuoteResponse> {
|
|
@@ -1849,6 +1895,103 @@ export function summarizeReserveRequestForTelemetry(req: ReserveRequest): Record
|
|
|
1849
1895
|
* `/1/reserve` success payload (`ReserveResponseData` on the API).
|
|
1850
1896
|
* Hold expiry is serialized as `reservationExpiration` (not `expiresAt`).
|
|
1851
1897
|
*/
|
|
1898
|
+
export interface PublicNewBookingQuoteV2Request {
|
|
1899
|
+
productId: string;
|
|
1900
|
+
optionId: string;
|
|
1901
|
+
availabilityId: string;
|
|
1902
|
+
dateTime: string;
|
|
1903
|
+
bookingItems: Array<{ category: string; count: number }>;
|
|
1904
|
+
currency?: string | null;
|
|
1905
|
+
returnAvailabilityId?: string | null;
|
|
1906
|
+
promoCode?: string | null;
|
|
1907
|
+
cancellationPolicyId?: string | null;
|
|
1908
|
+
addOnSelections?: Array<{ addOnId: string; quantity?: number; variantId?: string }> | null;
|
|
1909
|
+
additionalHoursCount?: number | null;
|
|
1910
|
+
allowOverbook?: boolean | null;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
export interface PricingV2QuoteLineSnapshot {
|
|
1914
|
+
lineId?: string | null;
|
|
1915
|
+
type?: string | null;
|
|
1916
|
+
label?: string | null;
|
|
1917
|
+
amount?: number | null;
|
|
1918
|
+
quantity?: number | null;
|
|
1919
|
+
unitAmount?: number | null;
|
|
1920
|
+
billable?: boolean | null;
|
|
1921
|
+
currency?: string | null;
|
|
1922
|
+
taxBehavior?: string | null;
|
|
1923
|
+
discountBehavior?: string | null;
|
|
1924
|
+
source?: string | null;
|
|
1925
|
+
metadata?: Record<string, string> | null;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
export interface PricingV2QuoteSnapshot {
|
|
1929
|
+
quoteId?: string | null;
|
|
1930
|
+
pricingVersion?: string | null;
|
|
1931
|
+
companyId?: string | null;
|
|
1932
|
+
context?: string | null;
|
|
1933
|
+
status?: string | null;
|
|
1934
|
+
currency?: string | null;
|
|
1935
|
+
grossSubtotal?: number | null;
|
|
1936
|
+
discountTotal?: number | null;
|
|
1937
|
+
paymentCreditTotal?: number | null;
|
|
1938
|
+
netSubtotalBeforeTax?: number | null;
|
|
1939
|
+
taxableSubtotal?: number | null;
|
|
1940
|
+
taxAmount?: number | null;
|
|
1941
|
+
payableTotal?: number | null;
|
|
1942
|
+
amountPreviouslyPaid?: number | null;
|
|
1943
|
+
balanceDelta?: number | null;
|
|
1944
|
+
amountToCharge?: number | null;
|
|
1945
|
+
refundCandidate?: number | null;
|
|
1946
|
+
totalAmount?: number | null;
|
|
1947
|
+
subtotal?: number | null;
|
|
1948
|
+
tax?: number | null;
|
|
1949
|
+
lines?: PricingV2QuoteLineSnapshot[] | null;
|
|
1950
|
+
inputsHash?: string | null;
|
|
1951
|
+
oldReceiptHash?: string | null;
|
|
1952
|
+
expiresAt?: string | null;
|
|
1953
|
+
bookingReference?: string | null;
|
|
1954
|
+
reservationReference?: string | null;
|
|
1955
|
+
relatedChangeIntentId?: string | null;
|
|
1956
|
+
metadata?: Record<string, string> | null;
|
|
1957
|
+
[key: string]: unknown;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
export interface PublicNewBookingQuoteV2Data {
|
|
1961
|
+
quote?: PricingV2QuoteSnapshot | null;
|
|
1962
|
+
canBook: boolean;
|
|
1963
|
+
reasonIfBlocked?: string | null;
|
|
1964
|
+
unsupportedFeatures?: string[];
|
|
1965
|
+
pricingV2Enabled?: boolean | null;
|
|
1966
|
+
pricingV2QuoteRequired?: boolean | null;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
export async function quotePublicNewBookingV2(
|
|
1970
|
+
request: PublicNewBookingQuoteV2Request,
|
|
1971
|
+
signal?: AbortSignal
|
|
1972
|
+
): Promise<PublicNewBookingQuoteV2Data> {
|
|
1973
|
+
const endpoint = '/1/public/booking-quotes';
|
|
1974
|
+
const res = await fetch(`${API_BASE}${endpoint}`, {
|
|
1975
|
+
method: 'POST',
|
|
1976
|
+
headers: getAuthHeaders(),
|
|
1977
|
+
body: JSON.stringify(request),
|
|
1978
|
+
signal,
|
|
1979
|
+
});
|
|
1980
|
+
const payload = await parseJsonSafely(res);
|
|
1981
|
+
if (!res.ok) {
|
|
1982
|
+
const message = isApiErrorPayload(payload)
|
|
1983
|
+
? payload.errorMessage || payload.error || `HTTP error ${res.status}`
|
|
1984
|
+
: `HTTP error ${res.status}`;
|
|
1985
|
+
throw new Error(message);
|
|
1986
|
+
}
|
|
1987
|
+
const outer = (payload as { data?: unknown } | null)?.data;
|
|
1988
|
+
const inner = (outer as { data?: PublicNewBookingQuoteV2Data } | null)?.data ?? outer;
|
|
1989
|
+
if (!inner || typeof inner !== 'object') {
|
|
1990
|
+
throw new Error('Invalid response: missing Pricing V2 quote data');
|
|
1991
|
+
}
|
|
1992
|
+
return inner as PublicNewBookingQuoteV2Data;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1852
1995
|
export interface ReserveResponse {
|
|
1853
1996
|
reservationReference: string;
|
|
1854
1997
|
/** ISO 8601 instant when the hold expires (API field name). */
|
|
@@ -2038,6 +2181,9 @@ export interface CreatePaymentIntentRequest {
|
|
|
2038
2181
|
checkoutBreakdown?: CheckoutBreakdown;
|
|
2039
2182
|
dependentAddOnSelection?: CheckoutDependentAddOnSelection;
|
|
2040
2183
|
itineraryDisplay?: ItineraryDisplayStep[];
|
|
2184
|
+
pricingQuoteId?: string;
|
|
2185
|
+
pricingQuoteInputsHash?: string;
|
|
2186
|
+
idempotencyKey?: string;
|
|
2041
2187
|
skipConfirmationCommunications?: boolean;
|
|
2042
2188
|
disableAutoCommunications?: boolean;
|
|
2043
2189
|
/** Private Shuttle deposit: pay deposit now, balance charged before trip */
|
|
@@ -2139,6 +2285,9 @@ export interface ConfirmFreeBookingRequest {
|
|
|
2139
2285
|
dependentAddOnSelection?: CheckoutDependentAddOnSelection;
|
|
2140
2286
|
skipConfirmationCommunications?: boolean;
|
|
2141
2287
|
disableAutoCommunications?: boolean;
|
|
2288
|
+
/** Pricing V2 checkout quote id; the backend also falls back to the quote stored on the reservation. */
|
|
2289
|
+
pricingQuoteId?: string;
|
|
2290
|
+
pricingQuoteInputsHash?: string;
|
|
2142
2291
|
source?: string;
|
|
2143
2292
|
sourceMetadata?: BookingSourceMetadata;
|
|
2144
2293
|
source_metadata?: BookingSourceMetadata;
|
|
@@ -2224,6 +2373,8 @@ export interface ConfirmBookingWithoutPaymentRequest {
|
|
|
2224
2373
|
balanceAmount: number;
|
|
2225
2374
|
totalAmount: number;
|
|
2226
2375
|
balanceChargeDaysBefore?: number;
|
|
2376
|
+
pricingQuoteId?: string;
|
|
2377
|
+
pricingQuoteInputsHash?: string;
|
|
2227
2378
|
source?: string;
|
|
2228
2379
|
sourceMetadata?: BookingSourceMetadata;
|
|
2229
2380
|
source_metadata?: BookingSourceMetadata;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
quotePublicNewBookingV2,
|
|
3
|
+
type PublicNewBookingQuoteV2Data,
|
|
4
|
+
type PublicNewBookingQuoteV2Request,
|
|
5
|
+
} from './booking-api';
|
|
6
|
+
|
|
7
|
+
export const PRICING_V2_SHADOW_NEW_BOOKING_QUOTE_EVENT = 'ticketbooth:pricing-v2-shadow-new-booking-quote';
|
|
8
|
+
export const PRICING_V2_SHADOW_NEW_BOOKING_QUOTE_STORAGE_KEY = 'ticketbooth:pricing-v2-shadow-new-booking-quote:last';
|
|
9
|
+
|
|
10
|
+
export type PricingV2ShadowNewBookingStatus = 'running' | 'matched' | 'mismatch' | 'blocked' | 'failed' | 'timeout';
|
|
11
|
+
|
|
12
|
+
export interface PricingV2ShadowNewBookingQuoteDetail {
|
|
13
|
+
status: PricingV2ShadowNewBookingStatus;
|
|
14
|
+
flow: 'standard' | 'private-shuttle';
|
|
15
|
+
productId: string;
|
|
16
|
+
optionId: string;
|
|
17
|
+
availabilityId: string;
|
|
18
|
+
dateTime: string;
|
|
19
|
+
quoteId?: string | null;
|
|
20
|
+
canBook?: boolean | null;
|
|
21
|
+
reasonIfBlocked?: string | null;
|
|
22
|
+
unsupportedFeatures?: string[];
|
|
23
|
+
clientProposedTotal?: number | null;
|
|
24
|
+
pricingV2Total?: number | null;
|
|
25
|
+
totalDelta?: number | null;
|
|
26
|
+
paymentCreditTotal?: number | null;
|
|
27
|
+
pricingV2LineCount?: number | null;
|
|
28
|
+
pricingV2Status?: string | null;
|
|
29
|
+
pricingV2Enabled?: boolean | null;
|
|
30
|
+
pricingV2QuoteRequired?: boolean | null;
|
|
31
|
+
currency?: string | null;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PRICING_V2_SHADOW_NEW_BOOKING_TIMEOUT_MS = 1500;
|
|
37
|
+
export const PRICING_V2_TOTAL_TOLERANCE = 0.02;
|
|
38
|
+
const PRICING_V2_CHECKOUT_QUOTE_UNAVAILABLE_MESSAGE =
|
|
39
|
+
'Unable to confirm the current price. Please try again.';
|
|
40
|
+
|
|
41
|
+
function roundMoney(value: number): number {
|
|
42
|
+
return Math.round(value * 100) / 100;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function quoteTotal(data: PublicNewBookingQuoteV2Data): number | null {
|
|
46
|
+
const value = data.quote?.amountToCharge ?? data.quote?.payableTotal ?? data.quote?.totalAmount;
|
|
47
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function quoteLineCount(data: PublicNewBookingQuoteV2Data): number | null {
|
|
51
|
+
const lines = data.quote?.lines;
|
|
52
|
+
return Array.isArray(lines) ? lines.length : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function publishPricingV2ShadowNewBookingQuote(detail: PricingV2ShadowNewBookingQuoteDetail): void {
|
|
56
|
+
if (typeof window === 'undefined') return;
|
|
57
|
+
try {
|
|
58
|
+
window.sessionStorage.setItem(PRICING_V2_SHADOW_NEW_BOOKING_QUOTE_STORAGE_KEY, JSON.stringify(detail));
|
|
59
|
+
} catch {
|
|
60
|
+
// Shadow diagnostics should never affect booking checkout.
|
|
61
|
+
}
|
|
62
|
+
window.dispatchEvent(new CustomEvent<PricingV2ShadowNewBookingQuoteDetail>(PRICING_V2_SHADOW_NEW_BOOKING_QUOTE_EVENT, { detail }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isPricingV2CheckoutQuoteEnabled(data: PublicNewBookingQuoteV2Data): boolean {
|
|
66
|
+
return data.pricingV2Enabled === true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPricingV2CheckoutQuoteRequiredByClient(): boolean {
|
|
70
|
+
return process.env.NEXT_PUBLIC_PRICING_V2_CHECKOUT_REQUIRED?.trim().toLowerCase() === 'true';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isPricingV2CheckoutQuoteRequired(data: PublicNewBookingQuoteV2Data): boolean {
|
|
74
|
+
return data.pricingV2QuoteRequired === true || isPricingV2CheckoutQuoteRequiredByClient();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function shadowQuotePublicNewBookingV2({
|
|
78
|
+
flow,
|
|
79
|
+
request,
|
|
80
|
+
clientProposedTotal,
|
|
81
|
+
}: {
|
|
82
|
+
flow: PricingV2ShadowNewBookingQuoteDetail['flow'];
|
|
83
|
+
request: PublicNewBookingQuoteV2Request;
|
|
84
|
+
clientProposedTotal?: number | null;
|
|
85
|
+
}): Promise<void> {
|
|
86
|
+
if (typeof window === 'undefined') return;
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timeoutId = window.setTimeout(() => controller.abort(), PRICING_V2_SHADOW_NEW_BOOKING_TIMEOUT_MS);
|
|
89
|
+
const roundedClientTotal = typeof clientProposedTotal === 'number' && Number.isFinite(clientProposedTotal)
|
|
90
|
+
? roundMoney(clientProposedTotal)
|
|
91
|
+
: null;
|
|
92
|
+
|
|
93
|
+
publishPricingV2ShadowNewBookingQuote({
|
|
94
|
+
status: 'running',
|
|
95
|
+
flow,
|
|
96
|
+
productId: request.productId,
|
|
97
|
+
optionId: request.optionId,
|
|
98
|
+
availabilityId: request.availabilityId,
|
|
99
|
+
dateTime: request.dateTime,
|
|
100
|
+
clientProposedTotal: roundedClientTotal,
|
|
101
|
+
currency: request.currency ?? null,
|
|
102
|
+
timestamp: new Date().toISOString(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const quote = await quotePublicNewBookingV2(request, controller.signal);
|
|
107
|
+
const pricingV2QuoteRequired = isPricingV2CheckoutQuoteRequired(quote);
|
|
108
|
+
const v2Total = quoteTotal(quote);
|
|
109
|
+
const totalDelta = v2Total != null && roundedClientTotal != null
|
|
110
|
+
? roundMoney(v2Total - roundedClientTotal)
|
|
111
|
+
: null;
|
|
112
|
+
const status: PricingV2ShadowNewBookingStatus = !quote.canBook
|
|
113
|
+
? 'blocked'
|
|
114
|
+
: totalDelta != null && Math.abs(totalDelta) > PRICING_V2_TOTAL_TOLERANCE
|
|
115
|
+
? 'mismatch'
|
|
116
|
+
: 'matched';
|
|
117
|
+
const detail: PricingV2ShadowNewBookingQuoteDetail = {
|
|
118
|
+
status,
|
|
119
|
+
flow,
|
|
120
|
+
productId: request.productId,
|
|
121
|
+
optionId: request.optionId,
|
|
122
|
+
availabilityId: request.availabilityId,
|
|
123
|
+
dateTime: request.dateTime,
|
|
124
|
+
quoteId: quote.quote?.quoteId ?? null,
|
|
125
|
+
canBook: quote.canBook,
|
|
126
|
+
reasonIfBlocked: quote.reasonIfBlocked ?? null,
|
|
127
|
+
unsupportedFeatures: quote.unsupportedFeatures ?? [],
|
|
128
|
+
clientProposedTotal: roundedClientTotal,
|
|
129
|
+
pricingV2Total: v2Total,
|
|
130
|
+
totalDelta,
|
|
131
|
+
paymentCreditTotal: quote.quote?.paymentCreditTotal ?? null,
|
|
132
|
+
pricingV2LineCount: quoteLineCount(quote),
|
|
133
|
+
pricingV2Status: quote.quote?.status ?? null,
|
|
134
|
+
pricingV2Enabled: quote.pricingV2Enabled ?? null,
|
|
135
|
+
pricingV2QuoteRequired,
|
|
136
|
+
currency: quote.quote?.currency ?? request.currency ?? null,
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
publishPricingV2ShadowNewBookingQuote(detail);
|
|
140
|
+
if (status === 'matched') {
|
|
141
|
+
console.info('[PricingV2 shadow] new booking quote matched', detail);
|
|
142
|
+
} else {
|
|
143
|
+
console.warn('[PricingV2 shadow] new booking quote mismatch', detail);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const aborted = error instanceof Error && error.name === 'AbortError';
|
|
147
|
+
const pricingV2QuoteRequired = isPricingV2CheckoutQuoteRequiredByClient();
|
|
148
|
+
const detail: PricingV2ShadowNewBookingQuoteDetail = {
|
|
149
|
+
status: aborted ? 'timeout' : 'failed',
|
|
150
|
+
flow,
|
|
151
|
+
productId: request.productId,
|
|
152
|
+
optionId: request.optionId,
|
|
153
|
+
availabilityId: request.availabilityId,
|
|
154
|
+
dateTime: request.dateTime,
|
|
155
|
+
clientProposedTotal: roundedClientTotal,
|
|
156
|
+
pricingV2QuoteRequired,
|
|
157
|
+
currency: request.currency ?? null,
|
|
158
|
+
error: aborted ? 'Quote timed out after 1.5s' : error instanceof Error ? error.message : 'Unknown quote error',
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
};
|
|
161
|
+
publishPricingV2ShadowNewBookingQuote(detail);
|
|
162
|
+
console.warn('[PricingV2 shadow] new booking quote skipped', detail);
|
|
163
|
+
} finally {
|
|
164
|
+
window.clearTimeout(timeoutId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
export async function quotePublicNewBookingV2ForCheckout({
|
|
170
|
+
flow,
|
|
171
|
+
request,
|
|
172
|
+
clientProposedTotal,
|
|
173
|
+
}: {
|
|
174
|
+
flow: PricingV2ShadowNewBookingQuoteDetail['flow'];
|
|
175
|
+
request: PublicNewBookingQuoteV2Request;
|
|
176
|
+
clientProposedTotal?: number | null;
|
|
177
|
+
}): Promise<PublicNewBookingQuoteV2Data | null> {
|
|
178
|
+
const roundedClientTotal = typeof clientProposedTotal === 'number' && Number.isFinite(clientProposedTotal)
|
|
179
|
+
? roundMoney(clientProposedTotal)
|
|
180
|
+
: null;
|
|
181
|
+
|
|
182
|
+
publishPricingV2ShadowNewBookingQuote({
|
|
183
|
+
status: 'running',
|
|
184
|
+
flow,
|
|
185
|
+
productId: request.productId,
|
|
186
|
+
optionId: request.optionId,
|
|
187
|
+
availabilityId: request.availabilityId,
|
|
188
|
+
dateTime: request.dateTime,
|
|
189
|
+
clientProposedTotal: roundedClientTotal,
|
|
190
|
+
currency: request.currency ?? null,
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
let quote: PublicNewBookingQuoteV2Data;
|
|
195
|
+
try {
|
|
196
|
+
quote = await quotePublicNewBookingV2(request);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const pricingV2QuoteRequired = isPricingV2CheckoutQuoteRequiredByClient();
|
|
199
|
+
const detail: PricingV2ShadowNewBookingQuoteDetail = {
|
|
200
|
+
status: pricingV2QuoteRequired ? 'blocked' : 'failed',
|
|
201
|
+
flow,
|
|
202
|
+
productId: request.productId,
|
|
203
|
+
optionId: request.optionId,
|
|
204
|
+
availabilityId: request.availabilityId,
|
|
205
|
+
dateTime: request.dateTime,
|
|
206
|
+
clientProposedTotal: roundedClientTotal,
|
|
207
|
+
pricingV2QuoteRequired,
|
|
208
|
+
currency: request.currency ?? null,
|
|
209
|
+
error: error instanceof Error ? error.message : 'Unknown quote error',
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
211
|
+
};
|
|
212
|
+
publishPricingV2ShadowNewBookingQuote(detail);
|
|
213
|
+
if (pricingV2QuoteRequired) {
|
|
214
|
+
console.warn('[PricingV2 checkout] required new booking quote unavailable', detail);
|
|
215
|
+
throw new Error(PRICING_V2_CHECKOUT_QUOTE_UNAVAILABLE_MESSAGE);
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const v2Total = quoteTotal(quote);
|
|
221
|
+
const totalDelta = v2Total != null && roundedClientTotal != null
|
|
222
|
+
? roundMoney(v2Total - roundedClientTotal)
|
|
223
|
+
: null;
|
|
224
|
+
const hasRequiredQuoteFields = Boolean(quote.quote?.quoteId) && v2Total != null;
|
|
225
|
+
const pricingV2QuoteRequired = isPricingV2CheckoutQuoteRequired(quote);
|
|
226
|
+
const pricingV2QuoteEnabled = isPricingV2CheckoutQuoteEnabled(quote) || pricingV2QuoteRequired;
|
|
227
|
+
const status: PricingV2ShadowNewBookingStatus = !quote.canBook || !hasRequiredQuoteFields
|
|
228
|
+
? 'blocked'
|
|
229
|
+
: totalDelta != null && Math.abs(totalDelta) > PRICING_V2_TOTAL_TOLERANCE
|
|
230
|
+
? 'mismatch'
|
|
231
|
+
: 'matched';
|
|
232
|
+
const detail: PricingV2ShadowNewBookingQuoteDetail = {
|
|
233
|
+
status,
|
|
234
|
+
flow,
|
|
235
|
+
productId: request.productId,
|
|
236
|
+
optionId: request.optionId,
|
|
237
|
+
availabilityId: request.availabilityId,
|
|
238
|
+
dateTime: request.dateTime,
|
|
239
|
+
quoteId: quote.quote?.quoteId ?? null,
|
|
240
|
+
canBook: quote.canBook,
|
|
241
|
+
reasonIfBlocked: quote.reasonIfBlocked ?? null,
|
|
242
|
+
unsupportedFeatures: quote.unsupportedFeatures ?? [],
|
|
243
|
+
clientProposedTotal: roundedClientTotal,
|
|
244
|
+
pricingV2Total: v2Total,
|
|
245
|
+
totalDelta,
|
|
246
|
+
paymentCreditTotal: quote.quote?.paymentCreditTotal ?? null,
|
|
247
|
+
pricingV2LineCount: quoteLineCount(quote),
|
|
248
|
+
pricingV2Status: quote.quote?.status ?? null,
|
|
249
|
+
pricingV2Enabled: quote.pricingV2Enabled ?? null,
|
|
250
|
+
pricingV2QuoteRequired,
|
|
251
|
+
currency: quote.quote?.currency ?? request.currency ?? null,
|
|
252
|
+
timestamp: new Date().toISOString(),
|
|
253
|
+
};
|
|
254
|
+
publishPricingV2ShadowNewBookingQuote(detail);
|
|
255
|
+
|
|
256
|
+
if (!pricingV2QuoteEnabled) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (status === 'blocked') {
|
|
261
|
+
if (pricingV2QuoteRequired && !hasRequiredQuoteFields) {
|
|
262
|
+
throw new Error(PRICING_V2_CHECKOUT_QUOTE_UNAVAILABLE_MESSAGE);
|
|
263
|
+
}
|
|
264
|
+
if (!pricingV2QuoteRequired) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const reason = quote.reasonIfBlocked ?? 'quote_not_applicable';
|
|
268
|
+
throw new Error(`Pricing quote blocked: ${reason}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return quote;
|
|
272
|
+
}
|