@ticketboothapp/booking 1.2.123 → 1.2.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +155 -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> {
|
|
@@ -1629,6 +1675,7 @@ export interface ReturnOption {
|
|
|
1629
1675
|
|
|
1630
1676
|
export interface Availability {
|
|
1631
1677
|
dateTime: string;
|
|
1678
|
+
canonicalDateTime?: string | null;
|
|
1632
1679
|
productId?: string;
|
|
1633
1680
|
vacancies: number;
|
|
1634
1681
|
totalCapacity?: number;
|
|
@@ -1849,6 +1896,103 @@ export function summarizeReserveRequestForTelemetry(req: ReserveRequest): Record
|
|
|
1849
1896
|
* `/1/reserve` success payload (`ReserveResponseData` on the API).
|
|
1850
1897
|
* Hold expiry is serialized as `reservationExpiration` (not `expiresAt`).
|
|
1851
1898
|
*/
|
|
1899
|
+
export interface PublicNewBookingQuoteV2Request {
|
|
1900
|
+
productId: string;
|
|
1901
|
+
optionId: string;
|
|
1902
|
+
availabilityId: string;
|
|
1903
|
+
dateTime: string;
|
|
1904
|
+
bookingItems: Array<{ category: string; count: number }>;
|
|
1905
|
+
currency?: string | null;
|
|
1906
|
+
returnAvailabilityId?: string | null;
|
|
1907
|
+
promoCode?: string | null;
|
|
1908
|
+
cancellationPolicyId?: string | null;
|
|
1909
|
+
addOnSelections?: Array<{ addOnId: string; quantity?: number; variantId?: string }> | null;
|
|
1910
|
+
additionalHoursCount?: number | null;
|
|
1911
|
+
allowOverbook?: boolean | null;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
export interface PricingV2QuoteLineSnapshot {
|
|
1915
|
+
lineId?: string | null;
|
|
1916
|
+
type?: string | null;
|
|
1917
|
+
label?: string | null;
|
|
1918
|
+
amount?: number | null;
|
|
1919
|
+
quantity?: number | null;
|
|
1920
|
+
unitAmount?: number | null;
|
|
1921
|
+
billable?: boolean | null;
|
|
1922
|
+
currency?: string | null;
|
|
1923
|
+
taxBehavior?: string | null;
|
|
1924
|
+
discountBehavior?: string | null;
|
|
1925
|
+
source?: string | null;
|
|
1926
|
+
metadata?: Record<string, string> | null;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
export interface PricingV2QuoteSnapshot {
|
|
1930
|
+
quoteId?: string | null;
|
|
1931
|
+
pricingVersion?: string | null;
|
|
1932
|
+
companyId?: string | null;
|
|
1933
|
+
context?: string | null;
|
|
1934
|
+
status?: string | null;
|
|
1935
|
+
currency?: string | null;
|
|
1936
|
+
grossSubtotal?: number | null;
|
|
1937
|
+
discountTotal?: number | null;
|
|
1938
|
+
paymentCreditTotal?: number | null;
|
|
1939
|
+
netSubtotalBeforeTax?: number | null;
|
|
1940
|
+
taxableSubtotal?: number | null;
|
|
1941
|
+
taxAmount?: number | null;
|
|
1942
|
+
payableTotal?: number | null;
|
|
1943
|
+
amountPreviouslyPaid?: number | null;
|
|
1944
|
+
balanceDelta?: number | null;
|
|
1945
|
+
amountToCharge?: number | null;
|
|
1946
|
+
refundCandidate?: number | null;
|
|
1947
|
+
totalAmount?: number | null;
|
|
1948
|
+
subtotal?: number | null;
|
|
1949
|
+
tax?: number | null;
|
|
1950
|
+
lines?: PricingV2QuoteLineSnapshot[] | null;
|
|
1951
|
+
inputsHash?: string | null;
|
|
1952
|
+
oldReceiptHash?: string | null;
|
|
1953
|
+
expiresAt?: string | null;
|
|
1954
|
+
bookingReference?: string | null;
|
|
1955
|
+
reservationReference?: string | null;
|
|
1956
|
+
relatedChangeIntentId?: string | null;
|
|
1957
|
+
metadata?: Record<string, string> | null;
|
|
1958
|
+
[key: string]: unknown;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
export interface PublicNewBookingQuoteV2Data {
|
|
1962
|
+
quote?: PricingV2QuoteSnapshot | null;
|
|
1963
|
+
canBook: boolean;
|
|
1964
|
+
reasonIfBlocked?: string | null;
|
|
1965
|
+
unsupportedFeatures?: string[];
|
|
1966
|
+
pricingV2Enabled?: boolean | null;
|
|
1967
|
+
pricingV2QuoteRequired?: boolean | null;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
export async function quotePublicNewBookingV2(
|
|
1971
|
+
request: PublicNewBookingQuoteV2Request,
|
|
1972
|
+
signal?: AbortSignal
|
|
1973
|
+
): Promise<PublicNewBookingQuoteV2Data> {
|
|
1974
|
+
const endpoint = '/1/public/booking-quotes';
|
|
1975
|
+
const res = await fetch(`${API_BASE}${endpoint}`, {
|
|
1976
|
+
method: 'POST',
|
|
1977
|
+
headers: getAuthHeaders(),
|
|
1978
|
+
body: JSON.stringify(request),
|
|
1979
|
+
signal,
|
|
1980
|
+
});
|
|
1981
|
+
const payload = await parseJsonSafely(res);
|
|
1982
|
+
if (!res.ok) {
|
|
1983
|
+
const message = isApiErrorPayload(payload)
|
|
1984
|
+
? payload.errorMessage || payload.error || `HTTP error ${res.status}`
|
|
1985
|
+
: `HTTP error ${res.status}`;
|
|
1986
|
+
throw new Error(message);
|
|
1987
|
+
}
|
|
1988
|
+
const outer = (payload as { data?: unknown } | null)?.data;
|
|
1989
|
+
const inner = (outer as { data?: PublicNewBookingQuoteV2Data } | null)?.data ?? outer;
|
|
1990
|
+
if (!inner || typeof inner !== 'object') {
|
|
1991
|
+
throw new Error('Invalid response: missing Pricing V2 quote data');
|
|
1992
|
+
}
|
|
1993
|
+
return inner as PublicNewBookingQuoteV2Data;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1852
1996
|
export interface ReserveResponse {
|
|
1853
1997
|
reservationReference: string;
|
|
1854
1998
|
/** ISO 8601 instant when the hold expires (API field name). */
|
|
@@ -2038,6 +2182,9 @@ export interface CreatePaymentIntentRequest {
|
|
|
2038
2182
|
checkoutBreakdown?: CheckoutBreakdown;
|
|
2039
2183
|
dependentAddOnSelection?: CheckoutDependentAddOnSelection;
|
|
2040
2184
|
itineraryDisplay?: ItineraryDisplayStep[];
|
|
2185
|
+
pricingQuoteId?: string;
|
|
2186
|
+
pricingQuoteInputsHash?: string;
|
|
2187
|
+
idempotencyKey?: string;
|
|
2041
2188
|
skipConfirmationCommunications?: boolean;
|
|
2042
2189
|
disableAutoCommunications?: boolean;
|
|
2043
2190
|
/** Private Shuttle deposit: pay deposit now, balance charged before trip */
|
|
@@ -2139,6 +2286,9 @@ export interface ConfirmFreeBookingRequest {
|
|
|
2139
2286
|
dependentAddOnSelection?: CheckoutDependentAddOnSelection;
|
|
2140
2287
|
skipConfirmationCommunications?: boolean;
|
|
2141
2288
|
disableAutoCommunications?: boolean;
|
|
2289
|
+
/** Pricing V2 checkout quote id; the backend also falls back to the quote stored on the reservation. */
|
|
2290
|
+
pricingQuoteId?: string;
|
|
2291
|
+
pricingQuoteInputsHash?: string;
|
|
2142
2292
|
source?: string;
|
|
2143
2293
|
sourceMetadata?: BookingSourceMetadata;
|
|
2144
2294
|
source_metadata?: BookingSourceMetadata;
|
|
@@ -2224,6 +2374,8 @@ export interface ConfirmBookingWithoutPaymentRequest {
|
|
|
2224
2374
|
balanceAmount: number;
|
|
2225
2375
|
totalAmount: number;
|
|
2226
2376
|
balanceChargeDaysBefore?: number;
|
|
2377
|
+
pricingQuoteId?: string;
|
|
2378
|
+
pricingQuoteInputsHash?: string;
|
|
2227
2379
|
source?: string;
|
|
2228
2380
|
sourceMetadata?: BookingSourceMetadata;
|
|
2229
2381
|
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
|
+
}
|