@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,203 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
getPromoDiscount,
|
|
4
|
+
type Availability,
|
|
5
|
+
type PricingConfig,
|
|
6
|
+
} from '../../lib/booking-api';
|
|
7
|
+
import type {
|
|
8
|
+
OrderSummaryFeeLine,
|
|
9
|
+
OrderSummaryTicketLine,
|
|
10
|
+
} from '../../lib/booking/pricing';
|
|
11
|
+
import type { Currency } from './CurrencySwitcher';
|
|
12
|
+
import { getAvailabilityOptionId } from './standard-booking-availability';
|
|
13
|
+
|
|
14
|
+
interface UseStandardBookingPromoDiscountParams {
|
|
15
|
+
appliedPromoCode: string | null;
|
|
16
|
+
selectedAvailability: Availability | null;
|
|
17
|
+
totalQuantity: number;
|
|
18
|
+
productCompanyId?: string | null;
|
|
19
|
+
envCompanyId?: string | null;
|
|
20
|
+
productId: string;
|
|
21
|
+
currency: Currency;
|
|
22
|
+
quantities: Record<string, number>;
|
|
23
|
+
ticketLineItems: OrderSummaryTicketLine[];
|
|
24
|
+
feeLineItems: OrderSummaryFeeLine[];
|
|
25
|
+
effectiveSubtotal: number;
|
|
26
|
+
isTaxIncludedInPrice: boolean;
|
|
27
|
+
pricingConfig: PricingConfig | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useStandardBookingPromoDiscount({
|
|
31
|
+
appliedPromoCode,
|
|
32
|
+
selectedAvailability,
|
|
33
|
+
totalQuantity,
|
|
34
|
+
productCompanyId,
|
|
35
|
+
envCompanyId,
|
|
36
|
+
productId,
|
|
37
|
+
currency,
|
|
38
|
+
quantities,
|
|
39
|
+
ticketLineItems,
|
|
40
|
+
feeLineItems,
|
|
41
|
+
effectiveSubtotal,
|
|
42
|
+
isTaxIncludedInPrice,
|
|
43
|
+
pricingConfig,
|
|
44
|
+
}: UseStandardBookingPromoDiscountParams) {
|
|
45
|
+
const quantitiesSignature = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
Object.entries(quantities)
|
|
48
|
+
.filter(([, n]) => (n ?? 0) > 0)
|
|
49
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
50
|
+
.map(([c, n]) => `${c}:${n}`)
|
|
51
|
+
.join('|'),
|
|
52
|
+
[quantities],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const [promoDiscountAmount, setPromoDiscountAmount] = useState(0);
|
|
56
|
+
const [isGiftCard, setIsGiftCard] = useState(false);
|
|
57
|
+
const [isVoucher, setIsVoucher] = useState(false);
|
|
58
|
+
const promoDiscountFetchGenerationRef = useRef(0);
|
|
59
|
+
const promoDiscountParamsRef = useRef({
|
|
60
|
+
selectedAvailability: null as Availability | null,
|
|
61
|
+
ticketLineItems: [] as Array<{ category: string; qty: number; itemTotal: number }>,
|
|
62
|
+
effectiveSubtotal: 0,
|
|
63
|
+
displayedProductFeeTotal: 0,
|
|
64
|
+
appliedPromoCode: null as string | null,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const selectedAvailabilityOptionId = useMemo(
|
|
68
|
+
() => (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''),
|
|
69
|
+
[selectedAvailability],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const promoDiscountFetchKey = useMemo(() => {
|
|
73
|
+
if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
|
|
74
|
+
const companyId = productCompanyId ?? envCompanyId;
|
|
75
|
+
if (!companyId) return '';
|
|
76
|
+
if (!selectedAvailabilityOptionId || !quantitiesSignature) return '';
|
|
77
|
+
return [
|
|
78
|
+
appliedPromoCode,
|
|
79
|
+
companyId,
|
|
80
|
+
productId,
|
|
81
|
+
selectedAvailabilityOptionId,
|
|
82
|
+
selectedAvailability.dateTime,
|
|
83
|
+
selectedAvailability.availabilityId ?? '',
|
|
84
|
+
currency,
|
|
85
|
+
quantitiesSignature,
|
|
86
|
+
ticketLineItems
|
|
87
|
+
.map((line) => `${line.category}:${line.qty}:${Math.round(line.itemTotal * 100)}`)
|
|
88
|
+
.sort()
|
|
89
|
+
.join('|'),
|
|
90
|
+
String(Math.round(feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0) * 100)),
|
|
91
|
+
String(Math.round(effectiveSubtotal * 100)),
|
|
92
|
+
].join('::');
|
|
93
|
+
}, [
|
|
94
|
+
appliedPromoCode,
|
|
95
|
+
selectedAvailability,
|
|
96
|
+
selectedAvailabilityOptionId,
|
|
97
|
+
totalQuantity,
|
|
98
|
+
productCompanyId,
|
|
99
|
+
envCompanyId,
|
|
100
|
+
productId,
|
|
101
|
+
currency,
|
|
102
|
+
quantitiesSignature,
|
|
103
|
+
ticketLineItems,
|
|
104
|
+
feeLineItems,
|
|
105
|
+
effectiveSubtotal,
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
promoDiscountParamsRef.current = {
|
|
109
|
+
selectedAvailability,
|
|
110
|
+
ticketLineItems: ticketLineItems.map((line) => ({
|
|
111
|
+
category: line.category,
|
|
112
|
+
qty: line.qty,
|
|
113
|
+
itemTotal: line.itemTotal,
|
|
114
|
+
})),
|
|
115
|
+
effectiveSubtotal,
|
|
116
|
+
displayedProductFeeTotal: feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0),
|
|
117
|
+
appliedPromoCode,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!promoDiscountFetchKey) {
|
|
122
|
+
setPromoDiscountAmount(0);
|
|
123
|
+
setIsGiftCard(false);
|
|
124
|
+
setIsVoucher(false);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let cancelled = false;
|
|
129
|
+
const debounceMs = 120;
|
|
130
|
+
const timer = window.setTimeout(() => {
|
|
131
|
+
if (cancelled) return;
|
|
132
|
+
const {
|
|
133
|
+
selectedAvailability: sel,
|
|
134
|
+
ticketLineItems: lines,
|
|
135
|
+
effectiveSubtotal: sub,
|
|
136
|
+
displayedProductFeeTotal,
|
|
137
|
+
appliedPromoCode: code,
|
|
138
|
+
} = promoDiscountParamsRef.current;
|
|
139
|
+
if (!code || !sel) return;
|
|
140
|
+
const companyId = productCompanyId ?? envCompanyId;
|
|
141
|
+
const optionId = getAvailabilityOptionId(sel);
|
|
142
|
+
if (!companyId || !optionId) return;
|
|
143
|
+
const items = lines.map((line) => ({ category: line.category, qty: line.qty }));
|
|
144
|
+
if (items.length === 0) return;
|
|
145
|
+
|
|
146
|
+
const generation = ++promoDiscountFetchGenerationRef.current;
|
|
147
|
+
getPromoDiscount(
|
|
148
|
+
code,
|
|
149
|
+
companyId,
|
|
150
|
+
productId,
|
|
151
|
+
optionId,
|
|
152
|
+
currency,
|
|
153
|
+
items,
|
|
154
|
+
sel.dateTime,
|
|
155
|
+
sub,
|
|
156
|
+
{
|
|
157
|
+
availabilityCount: sel.vacancies,
|
|
158
|
+
totalCapacity: sel.totalCapacity,
|
|
159
|
+
displayedTicketLines: lines,
|
|
160
|
+
displayedProductFeeTotal,
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
.then((res) => {
|
|
164
|
+
if (cancelled) return;
|
|
165
|
+
if (generation !== promoDiscountFetchGenerationRef.current) return;
|
|
166
|
+
setPromoDiscountAmount(res.discount ?? 0);
|
|
167
|
+
setIsGiftCard(res.isGiftCard ?? false);
|
|
168
|
+
setIsVoucher(res.isVoucher ?? false);
|
|
169
|
+
})
|
|
170
|
+
.catch(() => {
|
|
171
|
+
if (cancelled) return;
|
|
172
|
+
if (generation !== promoDiscountFetchGenerationRef.current) return;
|
|
173
|
+
setPromoDiscountAmount(0);
|
|
174
|
+
setIsGiftCard(false);
|
|
175
|
+
setIsVoucher(false);
|
|
176
|
+
});
|
|
177
|
+
}, debounceMs);
|
|
178
|
+
|
|
179
|
+
return () => {
|
|
180
|
+
cancelled = true;
|
|
181
|
+
window.clearTimeout(timer);
|
|
182
|
+
};
|
|
183
|
+
}, [promoDiscountFetchKey, productCompanyId, envCompanyId, productId, currency]);
|
|
184
|
+
|
|
185
|
+
// Percentage/fixed promos: tax on discounted amount (promo before GST per CRA guidance).
|
|
186
|
+
// Vouchers and gift-card comps: tax on full subtotal (gift-card comp reduces amount due after tax).
|
|
187
|
+
const effectivePromoDiscountAmount = promoDiscountAmount > 0 ? promoDiscountAmount : 0;
|
|
188
|
+
const taxOnSubtotal = isTaxIncludedInPrice ? 0 : effectiveSubtotal * (pricingConfig?.taxRate ?? 0);
|
|
189
|
+
const effectiveTax =
|
|
190
|
+
effectivePromoDiscountAmount > 0 && !isGiftCard && !isVoucher
|
|
191
|
+
? (effectiveSubtotal - effectivePromoDiscountAmount) * (pricingConfig?.taxRate ?? 0)
|
|
192
|
+
: taxOnSubtotal;
|
|
193
|
+
const totalPrice = effectiveSubtotal + effectiveTax - effectivePromoDiscountAmount;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
promoDiscountAmount,
|
|
197
|
+
isGiftCard,
|
|
198
|
+
isVoucher,
|
|
199
|
+
effectivePromoDiscountAmount,
|
|
200
|
+
effectiveTax,
|
|
201
|
+
totalPrice,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"productId": "p_urmbvZepRegd",
|
|
114
114
|
"optionIds": ["po_uO9Ra6NHzwof", "po_44EQlZn24gXg", "po_vuzqpvGk6wVS", "po_nIToR7Qwau3E"],
|
|
115
115
|
"productType": "PRIVATE_SHUTTLE",
|
|
116
|
-
"bookingCutoffMinutes":
|
|
116
|
+
"bookingCutoffMinutes": 720,
|
|
117
117
|
"display": {
|
|
118
118
|
"path": "/private-shuttle",
|
|
119
119
|
"slug": "private-tour",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* See `CHANGE_BOOKING_BE_HANDOFF.md` in this package for fields the API should populate.
|
|
6
6
|
*/
|
|
7
7
|
import type { ChangeBookingQuoteResponse } from '../booking-api';
|
|
8
|
+
import type { PricingV2QuoteLineSnapshot } from '../booking-api';
|
|
8
9
|
import type { PriceSummaryLine } from '../../components/booking/PriceSummary';
|
|
9
10
|
import { roundMoney, serverTotalsFromChangeQuoteResponse } from './change-flow-pricing';
|
|
10
11
|
|
|
@@ -26,27 +27,123 @@ export interface ChangeBookingServerPreview {
|
|
|
26
27
|
ticketUnitPriceByCategory?: Record<string, number>;
|
|
27
28
|
cancellationPolicyFeeByPolicyId?: Record<string, number>;
|
|
28
29
|
returnOptionPriceByReturnAvailabilityId?: Record<string, number>;
|
|
30
|
+
paymentCreditTotal?: number;
|
|
31
|
+
quoteId?: string | null;
|
|
32
|
+
quoteStatus?: string | null;
|
|
33
|
+
quoteExpiresAt?: string | null;
|
|
34
|
+
pricingVersion?: string | null;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
type QuoteSummaryInputLine =
|
|
38
|
+
| PricingV2QuoteLineSnapshot
|
|
39
|
+
| {
|
|
40
|
+
label?: string;
|
|
41
|
+
amount?: number;
|
|
42
|
+
type?: string;
|
|
43
|
+
quantity?: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function lineItemsForSummary(quote: ChangeBookingQuoteResponse): QuoteSummaryInputLine[] | undefined {
|
|
47
|
+
const pricingQuote = quote.pricingQuote ?? quote.quote;
|
|
48
|
+
return pricingQuote?.lines ?? quote.newReceipt?.lineItems ?? quote.proposed?.lineItems ?? quote.original?.lineItems ?? undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizedUpper(value: unknown): string {
|
|
52
|
+
return typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isPricingV2Line(line: QuoteSummaryInputLine): line is PricingV2QuoteLineSnapshot {
|
|
56
|
+
return (
|
|
57
|
+
'billable' in line ||
|
|
58
|
+
'discountBehavior' in line ||
|
|
59
|
+
'taxBehavior' in line ||
|
|
60
|
+
'source' in line ||
|
|
61
|
+
'unitAmount' in line
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isReductionLine(line: QuoteSummaryInputLine): boolean {
|
|
66
|
+
const type = normalizedUpper(line.type);
|
|
67
|
+
const source = isPricingV2Line(line) ? normalizedUpper(line.source) : '';
|
|
68
|
+
const discountBehavior = isPricingV2Line(line) ? normalizedUpper(line.discountBehavior) : '';
|
|
69
|
+
return (
|
|
70
|
+
type === 'PROMO_CODE' ||
|
|
71
|
+
type === 'DISCOUNT' ||
|
|
72
|
+
type === 'GIFT_CARD' ||
|
|
73
|
+
type === 'VOUCHER' ||
|
|
74
|
+
type === 'PAYMENT_CREDIT' ||
|
|
75
|
+
source === 'PROMO' ||
|
|
76
|
+
source === 'GIFT_CARD' ||
|
|
77
|
+
source === 'VOUCHER' ||
|
|
78
|
+
discountBehavior === 'PAYMENT_CREDIT' ||
|
|
79
|
+
discountBehavior.endsWith('_DISCOUNT')
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function summaryTypeForLine(line: QuoteSummaryInputLine): string {
|
|
84
|
+
const type = normalizedUpper(line.type);
|
|
85
|
+
if (isPricingV2Line(line)) {
|
|
86
|
+
const taxBehavior = normalizedUpper(line.taxBehavior);
|
|
87
|
+
const discountBehavior = normalizedUpper(line.discountBehavior);
|
|
88
|
+
const source = normalizedUpper(line.source);
|
|
89
|
+
if (taxBehavior === 'TAX' || type === 'TAX' || source === 'TAX') return 'TAX';
|
|
90
|
+
if (discountBehavior === 'PAYMENT_CREDIT') return source === 'GIFT_CARD' ? 'GIFT_CARD' : 'PAYMENT_CREDIT';
|
|
91
|
+
if (source === 'GIFT_CARD') return 'GIFT_CARD';
|
|
92
|
+
if (source === 'VOUCHER') return 'VOUCHER';
|
|
93
|
+
if (source === 'PROMO' || discountBehavior.endsWith('_DISCOUNT')) return type || 'DISCOUNT';
|
|
94
|
+
}
|
|
95
|
+
if (type === 'RETURNOPTION') return 'return';
|
|
96
|
+
if (type === 'RETURN_OPTION') return 'return';
|
|
97
|
+
return type || 'FEE';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function amountForDisplay(line: QuoteSummaryInputLine): number {
|
|
101
|
+
const amount = Number(line.amount) || 0;
|
|
102
|
+
return isReductionLine(line) && amount > 0 ? -amount : amount;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasPaymentCreditLine(lines: PriceSummaryLine[]): boolean {
|
|
106
|
+
return lines.some((line) => {
|
|
107
|
+
if (line.kind !== 'line') return false;
|
|
108
|
+
const type = normalizedUpper(line.type);
|
|
109
|
+
const label = line.label.toLowerCase();
|
|
110
|
+
return (
|
|
111
|
+
type === 'PAYMENT_CREDIT' ||
|
|
112
|
+
type === 'GIFT_CARD' ||
|
|
113
|
+
label.includes('payment credit') ||
|
|
114
|
+
label.includes('comp credit') ||
|
|
115
|
+
label.includes('gift card')
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function withPaymentCreditSummaryLine(
|
|
121
|
+
lines: PriceSummaryLine[],
|
|
122
|
+
paymentCreditTotal: number | null | undefined,
|
|
123
|
+
): PriceSummaryLine[] {
|
|
124
|
+
if (paymentCreditTotal == null || !Number.isFinite(paymentCreditTotal) || paymentCreditTotal <= 0) return lines;
|
|
125
|
+
if (hasPaymentCreditLine(lines)) return lines;
|
|
126
|
+
return [
|
|
127
|
+
...lines,
|
|
128
|
+
{
|
|
129
|
+
kind: 'line',
|
|
130
|
+
label: 'Gift card comp',
|
|
131
|
+
amount: -roundMoney(paymentCreditTotal),
|
|
132
|
+
type: 'PAYMENT_CREDIT',
|
|
133
|
+
},
|
|
134
|
+
];
|
|
35
135
|
}
|
|
36
136
|
|
|
37
137
|
/** Map receipt-style quote line items to checkout {@link PriceSummaryLine} rows (shared with price-check drift UI). */
|
|
38
138
|
export function mapQuoteLineItemsToPriceSummaryLines(
|
|
39
|
-
items:
|
|
40
|
-
| NonNullable<NonNullable<ChangeBookingQuoteResponse['newReceipt']>['lineItems']>
|
|
41
|
-
| Array<{ label?: string; amount?: number; type?: string; quantity?: number }>
|
|
42
|
-
| undefined
|
|
43
|
-
| null,
|
|
139
|
+
items: QuoteSummaryInputLine[] | undefined | null,
|
|
44
140
|
): PriceSummaryLine[] {
|
|
45
141
|
if (!items?.length) return [];
|
|
46
142
|
const out: PriceSummaryLine[] = [];
|
|
47
143
|
for (const item of items) {
|
|
48
|
-
|
|
49
|
-
const
|
|
144
|
+
if (isPricingV2Line(item) && item.billable === false) continue;
|
|
145
|
+
const amount = amountForDisplay(item);
|
|
146
|
+
const type = summaryTypeForLine(item);
|
|
50
147
|
const qty = Math.max(0, Number(item.quantity) || 0);
|
|
51
148
|
const label = (item.label ?? '').trim();
|
|
52
149
|
if (
|
|
@@ -90,19 +187,29 @@ export function mapQuoteLineItemsToPriceSummaryLines(
|
|
|
90
187
|
}
|
|
91
188
|
|
|
92
189
|
/** Align with checkout drift keys (`ChangeBookingFlow` uses `type: 'return'`). */
|
|
93
|
-
const summaryType =
|
|
94
|
-
type === 'RETURN_OPTION' || type === 'RETURNOPTION' ? 'return' : (item.type ?? 'fee');
|
|
95
190
|
out.push({
|
|
96
191
|
kind: 'line',
|
|
97
192
|
label: label || type || 'Line',
|
|
98
193
|
amount,
|
|
99
|
-
type
|
|
194
|
+
type,
|
|
100
195
|
quantity: qty > 0 ? qty : null,
|
|
101
196
|
});
|
|
102
197
|
}
|
|
103
198
|
return out;
|
|
104
199
|
}
|
|
105
200
|
|
|
201
|
+
function signedAmountDueFromQuote(quote: ChangeBookingQuoteResponse): number {
|
|
202
|
+
const pricingQuote = quote.pricingQuote ?? quote.quote;
|
|
203
|
+
if (quote.balanceDeltaMajorUnits != null) return quote.balanceDeltaMajorUnits;
|
|
204
|
+
if (quote.balanceDelta != null) return quote.balanceDelta;
|
|
205
|
+
if (pricingQuote?.balanceDelta != null) return pricingQuote.balanceDelta;
|
|
206
|
+
const charge = pricingQuote?.amountToCharge ?? quote.amountToCharge;
|
|
207
|
+
const refund = pricingQuote?.refundCandidate ?? quote.refundCandidate;
|
|
208
|
+
if (charge != null || refund != null) return (charge ?? 0) - (refund ?? 0);
|
|
209
|
+
if (quote.amountDueCents != null) return quote.amountDueCents / 100;
|
|
210
|
+
return quote.priceDiff ?? 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
106
213
|
/**
|
|
107
214
|
* Single builder: quote JSON → preview consumed by ChangeBookingFlow (no parallel FE “display math”).
|
|
108
215
|
*/
|
|
@@ -114,16 +221,18 @@ export function buildChangeBookingServerPreview(
|
|
|
114
221
|
const totals = serverTotalsFromChangeQuoteResponse(quote, fallbackCart);
|
|
115
222
|
if (!totals) return null;
|
|
116
223
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
224
|
+
const pricingQuote = quote.pricingQuote ?? quote.quote;
|
|
225
|
+
const currency = (pricingQuote?.currency ?? quote.currency ?? currencyFallback).trim() || currencyFallback;
|
|
226
|
+
const amountDue = signedAmountDueFromQuote(quote);
|
|
227
|
+
const paymentCreditTotal = pricingQuote?.paymentCreditTotal ?? quote.paymentCreditTotal;
|
|
120
228
|
|
|
121
229
|
const rawLines = lineItemsForSummary(quote);
|
|
122
230
|
const priceSummaryLines =
|
|
123
|
-
rawLines && rawLines.length > 0
|
|
231
|
+
rawLines && rawLines.length > 0
|
|
232
|
+
? withPaymentCreditSummaryLine(mapQuoteLineItemsToPriceSummaryLines(rawLines), paymentCreditTotal)
|
|
233
|
+
: withPaymentCreditSummaryLine([], paymentCreditTotal);
|
|
124
234
|
|
|
125
|
-
|
|
126
|
-
const completeness: ChangeBookingPreviewCompleteness = 'full';
|
|
235
|
+
const completeness: ChangeBookingPreviewCompleteness = priceSummaryLines.length > 0 ? 'full' : 'totals_only';
|
|
127
236
|
|
|
128
237
|
return {
|
|
129
238
|
currency,
|
|
@@ -136,5 +245,10 @@ export function buildChangeBookingServerPreview(
|
|
|
136
245
|
ticketUnitPriceByCategory: quote.ticketUnitPriceByCategory,
|
|
137
246
|
cancellationPolicyFeeByPolicyId: quote.cancellationPolicyFeeByPolicyId,
|
|
138
247
|
returnOptionPriceByReturnAvailabilityId: quote.returnOptionPriceByReturnAvailabilityId,
|
|
248
|
+
paymentCreditTotal,
|
|
249
|
+
quoteId: pricingQuote?.quoteId ?? quote.pricingQuoteId ?? null,
|
|
250
|
+
quoteStatus: pricingQuote?.status ?? null,
|
|
251
|
+
quoteExpiresAt: pricingQuote?.expiresAt ?? quote.expiresAt ?? null,
|
|
252
|
+
pricingVersion: pricingQuote?.pricingVersion ?? null,
|
|
139
253
|
};
|
|
140
254
|
}
|
|
@@ -196,19 +196,22 @@ export function changeFlowBalanceVsOriginal(input: {
|
|
|
196
196
|
* `fallbackCart` only when the API omits breakdown (scaled to match server total when needed).
|
|
197
197
|
*/
|
|
198
198
|
export function serverTotalsFromChangeQuoteResponse(
|
|
199
|
-
quote: Pick<ChangeBookingQuoteResponse, 'newReceipt' | 'proposed' | 'newTotalCents'>,
|
|
199
|
+
quote: Pick<ChangeBookingQuoteResponse, 'newReceipt' | 'proposed' | 'newTotalCents' | 'pricingQuote' | 'quote'>,
|
|
200
200
|
fallbackCart: { total: number; subtotal: number; tax: number }
|
|
201
201
|
): { total: number; subtotal: number; tax: number } | null {
|
|
202
202
|
const nr = quote.newReceipt;
|
|
203
203
|
const prop = quote.proposed;
|
|
204
|
+
const pricingQuote = quote.pricingQuote ?? quote.quote;
|
|
204
205
|
const total =
|
|
206
|
+
pricingQuote?.payableTotal ??
|
|
207
|
+
pricingQuote?.totalAmount ??
|
|
205
208
|
nr?.total ??
|
|
206
209
|
prop?.total ??
|
|
207
210
|
(quote.newTotalCents != null ? quote.newTotalCents / 100 : undefined);
|
|
208
211
|
if (total == null || !Number.isFinite(total)) return null;
|
|
209
212
|
|
|
210
|
-
let subtotal = nr?.subtotal;
|
|
211
|
-
let tax = nr?.tax;
|
|
213
|
+
let subtotal = pricingQuote?.netSubtotalBeforeTax ?? pricingQuote?.subtotal ?? nr?.subtotal;
|
|
214
|
+
let tax = pricingQuote?.taxAmount ?? pricingQuote?.tax ?? nr?.tax;
|
|
212
215
|
if (subtotal != null && tax == null) {
|
|
213
216
|
tax = Math.max(0, roundMoney(total - subtotal));
|
|
214
217
|
} else if (tax != null && subtotal == null) {
|
|
@@ -246,7 +249,13 @@ export interface ChangeQuoteUiSlice {
|
|
|
246
249
|
canProceed: boolean;
|
|
247
250
|
reasonIfBlocked?: string;
|
|
248
251
|
changeIntentId?: string;
|
|
252
|
+
quoteId?: string;
|
|
253
|
+
pricingQuoteInputsHash?: string;
|
|
254
|
+
quoteStatus?: string;
|
|
255
|
+
quoteExpiresAt?: string;
|
|
256
|
+
pricingVersion?: string;
|
|
249
257
|
quotedTotal?: number;
|
|
258
|
+
paymentCreditTotal?: number;
|
|
250
259
|
serverDisplay?: { total: number; subtotal: number; tax: number };
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -256,21 +265,32 @@ export function sliceChangeQuoteForUi(
|
|
|
256
265
|
fallbackCart: { total: number; subtotal: number; tax: number },
|
|
257
266
|
currencyFallback: string
|
|
258
267
|
): ChangeQuoteUiSlice {
|
|
268
|
+
const pricingQuote = quote.pricingQuote ?? quote.quote;
|
|
259
269
|
/** Staff JWT on quote: signed new − previous (refund owed negative). Otherwise same as legacy nonnegative amount-due. */
|
|
260
|
-
const priceDiff =
|
|
261
|
-
quote.balanceDeltaMajorUnits != null
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
270
|
+
const priceDiff = (() => {
|
|
271
|
+
if (quote.balanceDeltaMajorUnits != null) return quote.balanceDeltaMajorUnits;
|
|
272
|
+
if (quote.balanceDelta != null) return quote.balanceDelta;
|
|
273
|
+
if (pricingQuote?.balanceDelta != null) return pricingQuote.balanceDelta;
|
|
274
|
+
if (quote.amountToCharge != null || quote.refundCandidate != null) {
|
|
275
|
+
return (quote.amountToCharge ?? 0) - (quote.refundCandidate ?? 0);
|
|
276
|
+
}
|
|
277
|
+
if (quote.amountDueCents != null) return quote.amountDueCents / 100;
|
|
278
|
+
return quote.priceDiff ?? 0;
|
|
279
|
+
})();
|
|
266
280
|
const serverDisplay = serverTotalsFromChangeQuoteResponse(quote, fallbackCart);
|
|
267
281
|
return {
|
|
268
282
|
priceDiff,
|
|
269
|
-
currency: quote.currency ?? currencyFallback,
|
|
283
|
+
currency: pricingQuote?.currency ?? quote.currency ?? currencyFallback,
|
|
270
284
|
canProceed: quote.canProceed !== false,
|
|
271
285
|
reasonIfBlocked: quote.reasonIfBlocked,
|
|
272
286
|
changeIntentId: quote.changeIntentId,
|
|
273
|
-
|
|
287
|
+
quoteId: pricingQuote?.quoteId ?? quote.pricingQuoteId,
|
|
288
|
+
pricingQuoteInputsHash: pricingQuote?.inputsHash ?? quote.pricingQuoteInputsHash,
|
|
289
|
+
quoteStatus: pricingQuote?.status ?? undefined,
|
|
290
|
+
quoteExpiresAt: pricingQuote?.expiresAt ?? quote.expiresAt,
|
|
291
|
+
pricingVersion: pricingQuote?.pricingVersion ?? undefined,
|
|
292
|
+
quotedTotal: pricingQuote?.payableTotal ?? pricingQuote?.totalAmount ?? quote.proposed?.total ?? quote.newReceipt?.total,
|
|
293
|
+
paymentCreditTotal: pricingQuote?.paymentCreditTotal ?? quote.paymentCreditTotal,
|
|
274
294
|
...(serverDisplay ? { serverDisplay } : {}),
|
|
275
295
|
};
|
|
276
296
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckoutBreakdown, CheckoutReceiptLine } from '../booking-api';
|
|
1
|
+
import type { CheckoutBreakdown, CheckoutReceiptLine, PricingV2QuoteSnapshot } from '../booking-api';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Round to 2 decimal places. Used so breakdown amounts and totals are stable
|
|
@@ -67,3 +67,68 @@ export function buildCheckoutBreakdown(params: BuildCheckoutBreakdownParams): Ch
|
|
|
67
67
|
currency,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
function pricingV2LineTypeToCheckoutType(type?: string | null): string {
|
|
72
|
+
const normalized = (type ?? '').trim().toUpperCase();
|
|
73
|
+
switch (normalized) {
|
|
74
|
+
case 'TICKET':
|
|
75
|
+
case 'FEE':
|
|
76
|
+
case 'TAX':
|
|
77
|
+
case 'PROMO_CODE':
|
|
78
|
+
case 'GIFT_CARD':
|
|
79
|
+
case 'DEAL':
|
|
80
|
+
case 'DISCOUNT':
|
|
81
|
+
case 'RETURN_OPTION':
|
|
82
|
+
case 'CANCELLATION_UPGRADE':
|
|
83
|
+
case 'ROUNDING':
|
|
84
|
+
case 'BOOKING_CHANGE':
|
|
85
|
+
return normalized;
|
|
86
|
+
default:
|
|
87
|
+
return 'FEE';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function pricingV2QuoteTotal(quote?: PricingV2QuoteSnapshot | null): number | null {
|
|
92
|
+
const total = quote?.amountToCharge ?? quote?.payableTotal ?? quote?.totalAmount;
|
|
93
|
+
return typeof total === 'number' && Number.isFinite(total) ? round2(total) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildCheckoutBreakdownFromPricingV2Quote(
|
|
97
|
+
quote: PricingV2QuoteSnapshot | null | undefined,
|
|
98
|
+
roundingLabel: string
|
|
99
|
+
): CheckoutBreakdown | null {
|
|
100
|
+
const totalAmount = pricingV2QuoteTotal(quote);
|
|
101
|
+
const currency = typeof quote?.currency === 'string' && quote.currency.trim() ? quote.currency.trim() : null;
|
|
102
|
+
if (totalAmount == null || currency == null) return null;
|
|
103
|
+
|
|
104
|
+
const lines =
|
|
105
|
+
quote?.lines
|
|
106
|
+
?.filter((line) => line.billable !== false)
|
|
107
|
+
.flatMap((line) => {
|
|
108
|
+
const amount = line.amount;
|
|
109
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount)) return [];
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
label: line.label?.trim() || pricingV2LineTypeToCheckoutType(line.type),
|
|
113
|
+
amount,
|
|
114
|
+
type: pricingV2LineTypeToCheckoutType(line.type),
|
|
115
|
+
quantity: line.quantity ?? undefined,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}) ?? [];
|
|
119
|
+
|
|
120
|
+
if (lines.length === 0) {
|
|
121
|
+
return {
|
|
122
|
+
lineItems: [{ label: 'Total', amount: totalAmount, type: 'FEE' }],
|
|
123
|
+
totalAmount,
|
|
124
|
+
currency,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return buildCheckoutBreakdown({
|
|
129
|
+
lines,
|
|
130
|
+
totalAmount,
|
|
131
|
+
currency,
|
|
132
|
+
roundingLabel,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PricingConfig } from '../booking-api';
|
|
2
|
+
|
|
3
|
+
export function shouldReplacePricingConfig(
|
|
4
|
+
current: PricingConfig | null,
|
|
5
|
+
next: PricingConfig | null | undefined
|
|
6
|
+
): next is PricingConfig {
|
|
7
|
+
if (!next) return false;
|
|
8
|
+
if (!current) return true;
|
|
9
|
+
return JSON.stringify(current) !== JSON.stringify(next);
|
|
10
|
+
}
|
|
@@ -21,6 +21,8 @@ export interface BookingSourceMetadata {
|
|
|
21
21
|
gclid?: string;
|
|
22
22
|
fbclid?: string;
|
|
23
23
|
msclkid?: string;
|
|
24
|
+
gaClientId?: string;
|
|
25
|
+
gaSessionId?: string;
|
|
24
26
|
partnerId?: string;
|
|
25
27
|
partnerSlug?: string;
|
|
26
28
|
agentId?: string;
|
|
@@ -47,6 +49,59 @@ export function isDedicatedPartnerBookingPortalHost(hostname: string): boolean {
|
|
|
47
49
|
return h === 'booking.viaviamorainelake.com' || h === 'staging.booking.viaviamorainelake.com';
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
function parseCookieMap(cookieString: string): Map<string, string> {
|
|
53
|
+
const decodeCookieValue = (value: string): string => {
|
|
54
|
+
try {
|
|
55
|
+
return decodeURIComponent(value);
|
|
56
|
+
} catch {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const map = new Map<string, string>();
|
|
61
|
+
cookieString
|
|
62
|
+
.split(';')
|
|
63
|
+
.map((part) => part.trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.forEach((part) => {
|
|
66
|
+
const eq = part.indexOf('=');
|
|
67
|
+
if (eq <= 0) return;
|
|
68
|
+
const name = part.slice(0, eq).trim();
|
|
69
|
+
const value = part.slice(eq + 1).trim();
|
|
70
|
+
if (name) map.set(name, decodeCookieValue(value));
|
|
71
|
+
});
|
|
72
|
+
return map;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function gaClientIdFromCookie(cookieValue: string | undefined): string | undefined {
|
|
76
|
+
if (!cookieValue) return undefined;
|
|
77
|
+
const parts = cookieValue.split('.');
|
|
78
|
+
if (parts.length >= 4 && /^GA\d+$/.test(parts[0])) {
|
|
79
|
+
return parts.slice(-2).join('.');
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function gaSessionIdFromCookie(cookieValue: string | undefined): string | undefined {
|
|
85
|
+
if (!cookieValue) return undefined;
|
|
86
|
+
const parts = cookieValue.split('.');
|
|
87
|
+
if (parts.length >= 3 && /^GS\d+$/.test(parts[0]) && /^\d+$/.test(parts[2])) {
|
|
88
|
+
return parts[2];
|
|
89
|
+
}
|
|
90
|
+
const gs2Match = cookieValue.match(/(?:^|[.$])s(\d+)(?:[$.]|$)/);
|
|
91
|
+
return gs2Match?.[1];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readGoogleAnalyticsCookieIds(): Pick<BookingSourceMetadata, 'gaClientId' | 'gaSessionId'> {
|
|
95
|
+
if (typeof document === 'undefined') return {};
|
|
96
|
+
const cookies = parseCookieMap(document.cookie || '');
|
|
97
|
+
const gaClientId = gaClientIdFromCookie(cookies.get('_ga'));
|
|
98
|
+
const sessionCookie = Array.from(cookies.entries()).find(
|
|
99
|
+
([name, value]) => name.startsWith('_ga_') && value.startsWith('GS'),
|
|
100
|
+
)?.[1];
|
|
101
|
+
const gaSessionId = gaSessionIdFromCookie(sessionCookie);
|
|
102
|
+
return withDefinedValues({ gaClientId, gaSessionId });
|
|
103
|
+
}
|
|
104
|
+
|
|
50
105
|
/**
|
|
51
106
|
* Reads the **current browser URL** (and referrer) into attribution fields. Supplements
|
|
52
107
|
* `mergeBookingSourceMetadata` with `canonicalAttribution` from the route so identity does
|
|
@@ -74,6 +129,7 @@ export function buildBookingSourceMetadataFromLocation(): Partial<BookingSourceM
|
|
|
74
129
|
const partnerSlugFromPath = partnerMatch?.[1];
|
|
75
130
|
const agentIdFromQuery = params.get('agentId') || undefined;
|
|
76
131
|
const agentNameFromQuery = params.get('agentName') || undefined;
|
|
132
|
+
const gaCookieIds = readGoogleAnalyticsCookieIds();
|
|
77
133
|
|
|
78
134
|
return withDefinedValues<BookingSourceMetadata>({
|
|
79
135
|
pageUrl: currentUrl.href,
|
|
@@ -88,6 +144,8 @@ export function buildBookingSourceMetadataFromLocation(): Partial<BookingSourceM
|
|
|
88
144
|
gclid: params.get('gclid') || undefined,
|
|
89
145
|
fbclid: params.get('fbclid') || undefined,
|
|
90
146
|
msclkid: params.get('msclkid') || undefined,
|
|
147
|
+
gaClientId: gaCookieIds.gaClientId,
|
|
148
|
+
gaSessionId: gaCookieIds.gaSessionId,
|
|
91
149
|
partnerId: partnerFromQuery,
|
|
92
150
|
partnerSlug: partnerSlugFromPath,
|
|
93
151
|
agentId: agentIdFromQuery,
|