@ticketboothapp/booking 1.2.127 → 1.2.129
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/package.json +1 -1
- package/src/components/booking/NewBookingFlow.tsx +101 -14
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +1 -0
- package/src/components/booking/StandardBookingCheckoutSection.tsx +3 -0
- package/src/components/booking/pricing-v2-checkout-summary.ts +41 -2
- package/src/components/booking/private-shuttle-checkout-controller.ts +3 -0
- package/src/components/booking/private-shuttle-reservation-runner.ts +4 -0
- package/src/components/booking/standard-booking-checkout-controller.ts +9 -0
- package/src/components/booking/standard-booking-reservation-runner.ts +8 -0
- package/src/lib/booking/checkout-breakdown.ts +2 -0
- package/src/lib/booking-api.ts +5 -0
- package/test/change-booking-helpers.test.ts +51 -0
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { parseISO } from 'date-fns';
|
|
|
5
5
|
import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
|
|
6
6
|
import {
|
|
7
7
|
type Availability,
|
|
8
|
+
type PricingV2QuoteSnapshot,
|
|
8
9
|
} from '../../lib/booking-api';
|
|
9
10
|
import {
|
|
10
11
|
EARLIEST_AVAILABILITY_DATE,
|
|
@@ -42,6 +43,7 @@ import { StandardBookingCheckoutDialogs } from './StandardBookingCheckoutDialogs
|
|
|
42
43
|
import { StandardBookingItineraryPanel } from './StandardBookingItineraryPanel';
|
|
43
44
|
import { StandardBookingMediaIntro } from './StandardBookingMediaIntro';
|
|
44
45
|
import { StandardBookingSelectionControlsPanel } from './StandardBookingSelectionControlsPanel';
|
|
46
|
+
import { buildCheckoutModalSummaryFromPricingV2Quote } from './pricing-v2-checkout-summary';
|
|
45
47
|
|
|
46
48
|
export function NewBookingFlow({
|
|
47
49
|
product,
|
|
@@ -99,6 +101,8 @@ export function NewBookingFlow({
|
|
|
99
101
|
const [phoneNumber, setPhoneNumber] = useState('');
|
|
100
102
|
const [promoCodeInput, setPromoCodeInput] = useState('');
|
|
101
103
|
const [appliedPromoCode, setAppliedPromoCode] = useState<string | null>(null);
|
|
104
|
+
const [checkoutPricingV2QuoteForDisplay, setCheckoutPricingV2QuoteForDisplay] =
|
|
105
|
+
useState<PricingV2QuoteSnapshot | null>(null);
|
|
102
106
|
const [pickupLocationId, setPickupLocationId] = useState<string | null>(null);
|
|
103
107
|
const [pickupLocationSkipped, setPickupLocationSkipped] = useState(false);
|
|
104
108
|
// Cancellation: change flow seeds from the booking so totals match the quote (server uses booking.cancellationPolicyId).
|
|
@@ -488,6 +492,75 @@ export function NewBookingFlow({
|
|
|
488
492
|
augmentPriceSummary,
|
|
489
493
|
});
|
|
490
494
|
|
|
495
|
+
const bookingItemsDisplayKey = useMemo(
|
|
496
|
+
() =>
|
|
497
|
+
Object.entries(quantities)
|
|
498
|
+
.filter(([, count]) => count > 0)
|
|
499
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
500
|
+
.map(([category, count]) => `${category}:${count}`)
|
|
501
|
+
.join('|'),
|
|
502
|
+
[quantities],
|
|
503
|
+
);
|
|
504
|
+
const addOnSelectionsDisplayKey = useMemo(
|
|
505
|
+
() =>
|
|
506
|
+
normalizeAddOnSelections(addOnSelections)
|
|
507
|
+
.map((selection) =>
|
|
508
|
+
[
|
|
509
|
+
selection.addOnId,
|
|
510
|
+
selection.variantId ?? '',
|
|
511
|
+
selection.quantity ?? 1,
|
|
512
|
+
].join(':'),
|
|
513
|
+
)
|
|
514
|
+
.join('|'),
|
|
515
|
+
[addOnSelections],
|
|
516
|
+
);
|
|
517
|
+
const dependentAddOnSelectionDisplayKey = useMemo(
|
|
518
|
+
() => JSON.stringify(dependentAddOnSelection ?? null),
|
|
519
|
+
[dependentAddOnSelection],
|
|
520
|
+
);
|
|
521
|
+
const checkoutPricingV2DisplayKey = useMemo(
|
|
522
|
+
() =>
|
|
523
|
+
[
|
|
524
|
+
product.productId,
|
|
525
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
526
|
+
selectedAvailability?.availabilityId ?? '',
|
|
527
|
+
selectedAvailability?.dateTime ?? '',
|
|
528
|
+
selectedReturnOption?.returnAvailabilityId ?? '',
|
|
529
|
+
bookingItemsDisplayKey,
|
|
530
|
+
currency,
|
|
531
|
+
appliedPromoCode ?? '',
|
|
532
|
+
cancellationPolicyId ?? '',
|
|
533
|
+
addOnSelectionsDisplayKey,
|
|
534
|
+
dependentAddOnSelectionDisplayKey,
|
|
535
|
+
pricingProfileIdForAvailabilities ?? '',
|
|
536
|
+
isAdmin ? 'admin' : 'public',
|
|
537
|
+
].join('::'),
|
|
538
|
+
[
|
|
539
|
+
addOnSelectionsDisplayKey,
|
|
540
|
+
appliedPromoCode,
|
|
541
|
+
bookingItemsDisplayKey,
|
|
542
|
+
cancellationPolicyId,
|
|
543
|
+
currency,
|
|
544
|
+
dependentAddOnSelectionDisplayKey,
|
|
545
|
+
isAdmin,
|
|
546
|
+
pricingProfileIdForAvailabilities,
|
|
547
|
+
product.productId,
|
|
548
|
+
selectedAvailability,
|
|
549
|
+
selectedReturnOption?.returnAvailabilityId,
|
|
550
|
+
],
|
|
551
|
+
);
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
setCheckoutPricingV2QuoteForDisplay(null);
|
|
554
|
+
}, [checkoutPricingV2DisplayKey]);
|
|
555
|
+
const checkoutPricingV2DisplaySummary = useMemo(
|
|
556
|
+
() =>
|
|
557
|
+
buildCheckoutModalSummaryFromPricingV2Quote(
|
|
558
|
+
checkoutPricingV2QuoteForDisplay,
|
|
559
|
+
t('booking.rounding') || 'Rounding',
|
|
560
|
+
),
|
|
561
|
+
[checkoutPricingV2QuoteForDisplay, t],
|
|
562
|
+
);
|
|
563
|
+
|
|
491
564
|
useBookingQuantityCapTrim({
|
|
492
565
|
isAdmin,
|
|
493
566
|
selectedAvailability,
|
|
@@ -502,18 +575,29 @@ export function NewBookingFlow({
|
|
|
502
575
|
: undefined;
|
|
503
576
|
|
|
504
577
|
const checkoutFormError = error || '';
|
|
578
|
+
const hasServerBackedCheckoutDisplay = checkoutPricingV2DisplaySummary != null;
|
|
579
|
+
const checkoutDisplayPriceSummaryLines =
|
|
580
|
+
checkoutPricingV2DisplaySummary?.lines ?? displayCheckoutPriceSummaryLines;
|
|
581
|
+
const checkoutDisplaySubtotal =
|
|
582
|
+
checkoutPricingV2DisplaySummary?.subtotal ?? displaySubtotal;
|
|
583
|
+
const checkoutDisplayTotalPrice =
|
|
584
|
+
checkoutPricingV2DisplaySummary?.fullTotalAmount ?? displayTotalPrice;
|
|
505
585
|
const checkoutFormSubtotal =
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
586
|
+
checkoutPricingV2DisplaySummary != null
|
|
587
|
+
? checkoutPricingV2DisplaySummary.subtotal
|
|
588
|
+
: subtotal !== totalFromSummary ||
|
|
589
|
+
effectivePromoDiscountAmount > 0 ||
|
|
590
|
+
addOnTotal > 0 ||
|
|
591
|
+
(priceSummaryAugmentation?.subtotalAdjustment ?? 0) !== 0
|
|
592
|
+
? displaySubtotal
|
|
593
|
+
: undefined;
|
|
512
594
|
const checkoutFormTaxAmount =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
595
|
+
checkoutPricingV2DisplaySummary != null
|
|
596
|
+
? checkoutPricingV2DisplaySummary.taxAmount ?? 0
|
|
597
|
+
: !isTaxIncludedInPrice &&
|
|
598
|
+
(effectivePromoDiscountAmount > 0 ? effectiveTax : tax) > 0
|
|
599
|
+
? (effectivePromoDiscountAmount > 0 ? effectiveTax : tax)
|
|
600
|
+
: 0;
|
|
517
601
|
|
|
518
602
|
const handleCheckoutPickupLocationSelect = useCallback((locationId: string | null) => {
|
|
519
603
|
setPickupLocationId(locationId);
|
|
@@ -644,13 +728,14 @@ export function NewBookingFlow({
|
|
|
644
728
|
isAdmin,
|
|
645
729
|
pricingConfig,
|
|
646
730
|
refreshSelectedDateDetailsForCheckout,
|
|
647
|
-
displayTotalPrice,
|
|
648
|
-
displaySubtotal,
|
|
731
|
+
displayTotalPrice: checkoutDisplayTotalPrice,
|
|
732
|
+
displaySubtotal: checkoutDisplaySubtotal,
|
|
649
733
|
totalPrice,
|
|
650
734
|
currency,
|
|
651
735
|
appliedPromoCode,
|
|
652
736
|
cancellationPolicyId,
|
|
653
737
|
addOnSelections,
|
|
738
|
+
pricingProfileIdForAvailabilities,
|
|
654
739
|
dependentAddOnSelection,
|
|
655
740
|
ticketLineItems,
|
|
656
741
|
pricing,
|
|
@@ -677,6 +762,7 @@ export function NewBookingFlow({
|
|
|
677
762
|
setError,
|
|
678
763
|
onSuccess,
|
|
679
764
|
onShowManage,
|
|
765
|
+
onPricingV2QuoteForDisplay: setCheckoutPricingV2QuoteForDisplay,
|
|
680
766
|
});
|
|
681
767
|
|
|
682
768
|
if (activeOptions.length === 0) {
|
|
@@ -862,8 +948,8 @@ export function NewBookingFlow({
|
|
|
862
948
|
{/* Total and Checkout — shared PriceSummary component */}
|
|
863
949
|
{!selectedBookingOptionsHydrating && selectedAvailability && (
|
|
864
950
|
<StandardBookingCheckoutSection
|
|
865
|
-
priceSummaryLines={
|
|
866
|
-
totalPrice={
|
|
951
|
+
priceSummaryLines={checkoutDisplayPriceSummaryLines}
|
|
952
|
+
totalPrice={checkoutDisplayTotalPrice}
|
|
867
953
|
subtotal={checkoutFormSubtotal}
|
|
868
954
|
taxAmount={checkoutFormTaxAmount}
|
|
869
955
|
taxRate={pricingConfig?.taxRate}
|
|
@@ -875,6 +961,7 @@ export function NewBookingFlow({
|
|
|
875
961
|
promoCodeError={promoCodeError}
|
|
876
962
|
promoCodeValidating={promoCodeValidating}
|
|
877
963
|
promoDiscountAmount={promoDiscountAmount}
|
|
964
|
+
hidePromoDiscountAmount={hasServerBackedCheckoutDisplay}
|
|
878
965
|
onPromoInputChange={handlePromoInputChange}
|
|
879
966
|
onPromoApply={handleApplyPromo}
|
|
880
967
|
onPromoRemove={handleRemovePromo}
|
|
@@ -21,6 +21,7 @@ export interface StandardBookingCheckoutSectionProps {
|
|
|
21
21
|
promoCodeError: string;
|
|
22
22
|
promoCodeValidating: boolean;
|
|
23
23
|
promoDiscountAmount: number;
|
|
24
|
+
hidePromoDiscountAmount?: boolean;
|
|
24
25
|
onPromoInputChange: (value: string) => void;
|
|
25
26
|
onPromoApply: () => void;
|
|
26
27
|
onPromoRemove: () => void;
|
|
@@ -76,6 +77,7 @@ export function StandardBookingCheckoutSection({
|
|
|
76
77
|
promoCodeError,
|
|
77
78
|
promoCodeValidating,
|
|
78
79
|
promoDiscountAmount,
|
|
80
|
+
hidePromoDiscountAmount = false,
|
|
79
81
|
onPromoInputChange,
|
|
80
82
|
onPromoApply,
|
|
81
83
|
onPromoRemove,
|
|
@@ -134,6 +136,7 @@ export function StandardBookingCheckoutSection({
|
|
|
134
136
|
promoCodeError={promoCodeError}
|
|
135
137
|
promoCodeValidating={promoCodeValidating}
|
|
136
138
|
promoDiscountAmount={promoDiscountAmount}
|
|
139
|
+
hideDiscountAmount={hidePromoDiscountAmount}
|
|
137
140
|
currency={currency}
|
|
138
141
|
locale={locale}
|
|
139
142
|
t={t}
|
|
@@ -43,6 +43,35 @@ function fallbackLabel(type: string): string {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function isPaymentCreditType(type?: string | null): boolean {
|
|
47
|
+
const normalized = normalizedLineType(type);
|
|
48
|
+
return normalized === "GIFT_CARD" || normalized === "PAYMENT_CREDIT";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isPaymentCreditSummaryLine(line: PriceSummaryLine): boolean {
|
|
52
|
+
return line.kind === "line" && isPaymentCreditType(line.type);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function movePaymentCreditLinesAfterTax(lines: PriceSummaryLine[]): PriceSummaryLine[] {
|
|
56
|
+
const paymentCreditLines = lines.filter(isPaymentCreditSummaryLine);
|
|
57
|
+
if (paymentCreditLines.length === 0) return lines;
|
|
58
|
+
|
|
59
|
+
const nonPaymentCreditLines = lines.filter((line) => !isPaymentCreditSummaryLine(line));
|
|
60
|
+
const taxIndex = nonPaymentCreditLines.findIndex(
|
|
61
|
+
(line) => line.kind === "line" && normalizedLineType(line.type) === "TAX",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (taxIndex < 0) {
|
|
65
|
+
return [...nonPaymentCreditLines, ...paymentCreditLines];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
...nonPaymentCreditLines.slice(0, taxIndex + 1),
|
|
70
|
+
...paymentCreditLines,
|
|
71
|
+
...nonPaymentCreditLines.slice(taxIndex + 1),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
function priceSummaryLineFromQuoteLine(
|
|
47
76
|
line: PricingV2QuoteLineSnapshot,
|
|
48
77
|
index: number,
|
|
@@ -83,10 +112,11 @@ export function buildCheckoutModalSummaryFromPricingV2Quote(
|
|
|
83
112
|
const total = pricingV2QuoteTotal(quote);
|
|
84
113
|
if (total == null) return undefined;
|
|
85
114
|
|
|
86
|
-
const
|
|
115
|
+
const mappedLines =
|
|
87
116
|
quote?.lines
|
|
88
117
|
?.map((line, index) => priceSummaryLineFromQuoteLine(line, index))
|
|
89
118
|
.filter((line): line is PriceSummaryLine => line != null) ?? [];
|
|
119
|
+
const lines = movePaymentCreditLinesAfterTax(mappedLines);
|
|
90
120
|
|
|
91
121
|
if (lines.length === 0) return undefined;
|
|
92
122
|
|
|
@@ -122,8 +152,17 @@ export function buildCheckoutModalSummaryFromPricingV2Quote(
|
|
|
122
152
|
type === "PAYMENT_CREDIT"
|
|
123
153
|
);
|
|
124
154
|
});
|
|
155
|
+
const paymentCreditTotal = Math.max(
|
|
156
|
+
0,
|
|
157
|
+
finiteNumber(quote?.paymentCreditTotal) ?? 0,
|
|
158
|
+
);
|
|
125
159
|
const subtotal = finiteNumber(
|
|
126
|
-
|
|
160
|
+
paymentCreditTotal > 0
|
|
161
|
+
? quote?.taxableSubtotal ??
|
|
162
|
+
quote?.grossSubtotal ??
|
|
163
|
+
quote?.netSubtotalBeforeTax ??
|
|
164
|
+
quote?.subtotal
|
|
165
|
+
: quote?.netSubtotalBeforeTax ?? quote?.subtotal ?? quote?.grossSubtotal,
|
|
127
166
|
);
|
|
128
167
|
const taxAmount = hasTaxLine
|
|
129
168
|
? 0
|
|
@@ -79,6 +79,7 @@ export interface UsePrivateShuttleCheckoutControllerParams {
|
|
|
79
79
|
pricingConfig: PricingConfig | null;
|
|
80
80
|
refreshSelectedDateDetailsForCheckout: ReservePrivateShuttleForCheckoutParams['refreshSelectedDateDetailsForCheckout'];
|
|
81
81
|
reloadAvailabilitiesAfterReserveConflict: () => Promise<Availability[]>;
|
|
82
|
+
pricingProfileIdForAvailabilities: string | null;
|
|
82
83
|
totalPrice: number;
|
|
83
84
|
subtotal: number;
|
|
84
85
|
activePromoCode: string | null;
|
|
@@ -151,6 +152,7 @@ export function usePrivateShuttleCheckoutController({
|
|
|
151
152
|
pricingConfig,
|
|
152
153
|
refreshSelectedDateDetailsForCheckout,
|
|
153
154
|
reloadAvailabilitiesAfterReserveConflict,
|
|
155
|
+
pricingProfileIdForAvailabilities,
|
|
154
156
|
totalPrice,
|
|
155
157
|
subtotal,
|
|
156
158
|
activePromoCode,
|
|
@@ -358,6 +360,7 @@ export function usePrivateShuttleCheckoutController({
|
|
|
358
360
|
childSafetySeatsCount,
|
|
359
361
|
foodRestrictions,
|
|
360
362
|
itineraryDisplayItems,
|
|
363
|
+
pricingProfileIdForAvailabilities,
|
|
361
364
|
});
|
|
362
365
|
if (reserveResult.kind === 'blocked') {
|
|
363
366
|
setError(reserveResult.message);
|
|
@@ -78,6 +78,7 @@ export interface ReservePrivateShuttleForCheckoutParams {
|
|
|
78
78
|
childSafetySeatsCount: number;
|
|
79
79
|
foodRestrictions: string;
|
|
80
80
|
itineraryDisplayItems: PrivateShuttleCheckoutItineraryItem[];
|
|
81
|
+
pricingProfileIdForAvailabilities: string | null;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
function buildPrivateShuttleItineraryDisplayForStorage(
|
|
@@ -132,6 +133,7 @@ export async function reservePrivateShuttleForCheckout({
|
|
|
132
133
|
childSafetySeatsCount,
|
|
133
134
|
foodRestrictions,
|
|
134
135
|
itineraryDisplayItems,
|
|
136
|
+
pricingProfileIdForAvailabilities,
|
|
135
137
|
}: ReservePrivateShuttleForCheckoutParams): Promise<PrivateShuttleCheckoutReserveResult> {
|
|
136
138
|
if (!selectedOption) {
|
|
137
139
|
return { kind: 'blocked', message: 'No product option selected' };
|
|
@@ -206,6 +208,7 @@ export async function reservePrivateShuttleForCheckout({
|
|
|
206
208
|
cancellationPolicyId: cancellationPolicyId || null,
|
|
207
209
|
addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
|
|
208
210
|
additionalHoursCount: isAdmin && additionalHoursCount > 0 ? additionalHoursCount : null,
|
|
211
|
+
pricingProfileId: pricingProfileIdForAvailabilities || null,
|
|
209
212
|
...(isAdmin ? { allowOverbook: true } : {}),
|
|
210
213
|
},
|
|
211
214
|
});
|
|
@@ -234,6 +237,7 @@ export async function reservePrivateShuttleForCheckout({
|
|
|
234
237
|
foodRestrictions: foodRestrictions.trim() || undefined,
|
|
235
238
|
addOnSelections: addOnSelections.length > 0 ? addOnSelections : undefined,
|
|
236
239
|
additionalHoursCount: isAdmin && additionalHoursCount > 0 ? additionalHoursCount : undefined,
|
|
240
|
+
pricingProfileId: pricingProfileIdForAvailabilities || undefined,
|
|
237
241
|
...(isAdmin ? { allowOverbook: true } : {}),
|
|
238
242
|
...(bookingSourceContext.sourceMetadata
|
|
239
243
|
? {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type Availability,
|
|
13
13
|
type ItineraryDisplayStep,
|
|
14
14
|
type PricingConfig,
|
|
15
|
+
type PricingV2QuoteSnapshot,
|
|
15
16
|
type Product,
|
|
16
17
|
type ReturnOption,
|
|
17
18
|
} from '../../lib/booking-api';
|
|
@@ -77,6 +78,7 @@ export interface UseStandardBookingCheckoutControllerParams {
|
|
|
77
78
|
cancellationPolicyId: string | null;
|
|
78
79
|
addOnSelections: StandardAddOnSelection[];
|
|
79
80
|
dependentAddOnSelection?: NewBookingFlowProps['dependentAddOnSelection'];
|
|
81
|
+
pricingProfileIdForAvailabilities: string | null;
|
|
80
82
|
ticketLineItems: OrderSummaryTicketLine[];
|
|
81
83
|
pricing: BuildStandardBookingCheckoutArtifactsParams['pricing'];
|
|
82
84
|
getPriceBreakdown: BuildStandardBookingCheckoutArtifactsParams['getPriceBreakdown'];
|
|
@@ -106,6 +108,7 @@ export interface UseStandardBookingCheckoutControllerParams {
|
|
|
106
108
|
setError: (error: string) => void;
|
|
107
109
|
onSuccess?: NewBookingFlowProps['onSuccess'];
|
|
108
110
|
onShowManage?: (params: { ref: string; lastName: string }) => void;
|
|
111
|
+
onPricingV2QuoteForDisplay?: (quote: PricingV2QuoteSnapshot | null) => void;
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
export function useStandardBookingCheckoutController({
|
|
@@ -143,6 +146,7 @@ export function useStandardBookingCheckoutController({
|
|
|
143
146
|
cancellationPolicyId,
|
|
144
147
|
addOnSelections,
|
|
145
148
|
dependentAddOnSelection,
|
|
149
|
+
pricingProfileIdForAvailabilities,
|
|
146
150
|
ticketLineItems,
|
|
147
151
|
pricing,
|
|
148
152
|
getPriceBreakdown,
|
|
@@ -168,6 +172,7 @@ export function useStandardBookingCheckoutController({
|
|
|
168
172
|
setError,
|
|
169
173
|
onSuccess,
|
|
170
174
|
onShowManage,
|
|
175
|
+
onPricingV2QuoteForDisplay,
|
|
171
176
|
}: UseStandardBookingCheckoutControllerParams) {
|
|
172
177
|
const [showCheckoutModal, setShowCheckoutModal] = useState(false);
|
|
173
178
|
const [checkoutClientSecret, setCheckoutClientSecret] = useState('');
|
|
@@ -267,6 +272,7 @@ export function useStandardBookingCheckoutController({
|
|
|
267
272
|
setLoading(true);
|
|
268
273
|
setError('');
|
|
269
274
|
paymentSubmitInFlightRef.current = false;
|
|
275
|
+
onPricingV2QuoteForDisplay?.(null);
|
|
270
276
|
|
|
271
277
|
try {
|
|
272
278
|
const reserveResult = await reserveStandardBookingForCheckout({
|
|
@@ -289,6 +295,8 @@ export function useStandardBookingCheckoutController({
|
|
|
289
295
|
cancellationPolicyId,
|
|
290
296
|
addOnSelections,
|
|
291
297
|
itineraryDisplay: computeItineraryDisplayForStorage() ?? computeItineraryDisplay(),
|
|
298
|
+
dependentAddOnSelection,
|
|
299
|
+
pricingProfileIdForAvailabilities,
|
|
292
300
|
});
|
|
293
301
|
if (reserveResult.kind === 'blocked') {
|
|
294
302
|
setError(reserveResult.message);
|
|
@@ -305,6 +313,7 @@ export function useStandardBookingCheckoutController({
|
|
|
305
313
|
timePart,
|
|
306
314
|
itineraryDisplay,
|
|
307
315
|
} = reserveResult;
|
|
316
|
+
onPricingV2QuoteForDisplay?.(pricingV2Quote?.quote ?? null);
|
|
308
317
|
pendingReservationRef.current = { reservationReference: reservation.reservationReference };
|
|
309
318
|
|
|
310
319
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
createReservation,
|
|
3
3
|
describeStandardTourCapacityConflictMessage,
|
|
4
4
|
type Availability,
|
|
5
|
+
type CheckoutDependentAddOnSelection,
|
|
5
6
|
type ItineraryDisplayStep,
|
|
6
7
|
type PricingConfig,
|
|
7
8
|
type Product,
|
|
@@ -65,6 +66,8 @@ export interface ReserveStandardBookingForCheckoutParams {
|
|
|
65
66
|
cancellationPolicyId: string | null;
|
|
66
67
|
addOnSelections: StandardAddOnSelection[];
|
|
67
68
|
itineraryDisplay: ItineraryDisplayStep[] | null;
|
|
69
|
+
dependentAddOnSelection?: CheckoutDependentAddOnSelection | null;
|
|
70
|
+
pricingProfileIdForAvailabilities: string | null;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
function updatePendingStandardBookingSession({
|
|
@@ -111,6 +114,8 @@ export async function reserveStandardBookingForCheckout({
|
|
|
111
114
|
cancellationPolicyId,
|
|
112
115
|
addOnSelections,
|
|
113
116
|
itineraryDisplay,
|
|
117
|
+
dependentAddOnSelection,
|
|
118
|
+
pricingProfileIdForAvailabilities,
|
|
114
119
|
}: ReserveStandardBookingForCheckoutParams): Promise<StandardCheckoutReserveResult> {
|
|
115
120
|
const bookingItems = Object.entries(quantities)
|
|
116
121
|
.filter(([, count]) => count > 0)
|
|
@@ -177,6 +182,8 @@ export async function reserveStandardBookingForCheckout({
|
|
|
177
182
|
promoCode: appliedPromoCode || null,
|
|
178
183
|
cancellationPolicyId: cancellationPolicyId || null,
|
|
179
184
|
addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
|
|
185
|
+
dependentAddOnSelection: dependentAddOnSelection || null,
|
|
186
|
+
pricingProfileId: pricingProfileIdForAvailabilities || null,
|
|
180
187
|
...(isAdmin ? { allowOverbook: true } : {}),
|
|
181
188
|
},
|
|
182
189
|
});
|
|
@@ -192,6 +199,7 @@ export async function reserveStandardBookingForCheckout({
|
|
|
192
199
|
promoCode: appliedPromoCode || undefined,
|
|
193
200
|
cancellationPolicyId: cancellationPolicyId || undefined,
|
|
194
201
|
addOnSelections: addOnSelections.length > 0 ? addOnSelections : undefined,
|
|
202
|
+
pricingProfileId: pricingProfileIdForAvailabilities || undefined,
|
|
195
203
|
...(isAdmin ? { allowOverbook: true } : {}),
|
|
196
204
|
travelerHotel: selectedPickupLocation?.name || undefined,
|
|
197
205
|
...(bookingSourceContext.sourceMetadata
|
|
@@ -26,6 +26,7 @@ export const CheckoutLineType = {
|
|
|
26
26
|
PROMO_CODE: 'PROMO_CODE',
|
|
27
27
|
ROUNDING: 'ROUNDING',
|
|
28
28
|
ADDITIONAL_HOURS: 'ADDITIONAL_HOURS', // Future: private shuttle extra hours line
|
|
29
|
+
DEPENDENT_ADD_ON: 'DEPENDENT_ADD_ON',
|
|
29
30
|
} as const;
|
|
30
31
|
|
|
31
32
|
export interface BuildCheckoutBreakdownParams {
|
|
@@ -82,6 +83,7 @@ function pricingV2LineTypeToCheckoutType(type?: string | null): string {
|
|
|
82
83
|
case 'CANCELLATION_UPGRADE':
|
|
83
84
|
case 'ROUNDING':
|
|
84
85
|
case 'BOOKING_CHANGE':
|
|
86
|
+
case 'DEPENDENT_ADD_ON':
|
|
85
87
|
return normalized;
|
|
86
88
|
default:
|
|
87
89
|
return 'FEE';
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -1853,6 +1853,8 @@ export interface ReserveRequest {
|
|
|
1853
1853
|
foodRestrictions?: string;
|
|
1854
1854
|
/** Admin only: additional hours add-on (extends duration) */
|
|
1855
1855
|
additionalHoursCount?: number;
|
|
1856
|
+
/** Optional B2B/partner pricing profile used for availability and server pricing. */
|
|
1857
|
+
pricingProfileId?: string | null;
|
|
1856
1858
|
/**
|
|
1857
1859
|
* Admin only: allow outbound/return capacity holds above vacancies. Server requires admin JWT
|
|
1858
1860
|
* and still validates permission; omit or false for public/partner flows.
|
|
@@ -1888,6 +1890,7 @@ export function summarizeReserveRequestForTelemetry(req: ReserveRequest): Record
|
|
|
1888
1890
|
promoPresent: Boolean(req.promoCode?.trim()),
|
|
1889
1891
|
additionalHoursCount: req.additionalHoursCount ?? null,
|
|
1890
1892
|
childSafetySeatsCount: req.childSafetySeatsCount ?? null,
|
|
1893
|
+
pricingProfileId: req.pricingProfileId ?? null,
|
|
1891
1894
|
allowOverbook: req.allowOverbook === true,
|
|
1892
1895
|
};
|
|
1893
1896
|
}
|
|
@@ -1908,6 +1911,8 @@ export interface PublicNewBookingQuoteV2Request {
|
|
|
1908
1911
|
cancellationPolicyId?: string | null;
|
|
1909
1912
|
addOnSelections?: Array<{ addOnId: string; quantity?: number; variantId?: string }> | null;
|
|
1910
1913
|
additionalHoursCount?: number | null;
|
|
1914
|
+
dependentAddOnSelection?: CheckoutDependentAddOnSelection | null;
|
|
1915
|
+
pricingProfileId?: string | null;
|
|
1911
1916
|
allowOverbook?: boolean | null;
|
|
1912
1917
|
}
|
|
1913
1918
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
buildChangeBookingPaymentSuccessUrl,
|
|
5
5
|
} from '../src/components/booking/change-booking-payment-modal-builders';
|
|
6
6
|
import { evaluateChangeBookingQuoteForCheckout } from '../src/components/booking/change-booking-quote-guards';
|
|
7
|
+
import { buildCheckoutModalSummaryFromPricingV2Quote } from '../src/components/booking/pricing-v2-checkout-summary';
|
|
7
8
|
import type { ChangeQuoteUiSlice } from '../src/lib/booking/change-flow-pricing';
|
|
8
9
|
import type { ChangeBookingQuoteResponse } from '../src/lib/booking-api';
|
|
9
10
|
|
|
@@ -167,6 +168,56 @@ test('delegates success URL construction to host when provider dashboard asks fo
|
|
|
167
168
|
assert.equal(url, 'provider://ABC-123/Smith/2026-07-08');
|
|
168
169
|
});
|
|
169
170
|
|
|
171
|
+
test('maps Pricing V2 gift cards as payment credits after tax', () => {
|
|
172
|
+
const summary = buildCheckoutModalSummaryFromPricingV2Quote(
|
|
173
|
+
{
|
|
174
|
+
currency: 'CAD',
|
|
175
|
+
grossSubtotal: 449.4,
|
|
176
|
+
netSubtotalBeforeTax: 350.46,
|
|
177
|
+
taxableSubtotal: 449.4,
|
|
178
|
+
taxAmount: 38.2,
|
|
179
|
+
paymentCreditTotal: 98.94,
|
|
180
|
+
payableTotal: 388.66,
|
|
181
|
+
amountToCharge: 388.66,
|
|
182
|
+
lines: [
|
|
183
|
+
{
|
|
184
|
+
lineId: 'ticket_1',
|
|
185
|
+
type: 'TICKET',
|
|
186
|
+
label: 'ADULT',
|
|
187
|
+
amount: 417.42,
|
|
188
|
+
quantity: 2,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
lineId: 'fee_1',
|
|
192
|
+
type: 'FEE',
|
|
193
|
+
label: 'Moraine Lake Road Access Fee',
|
|
194
|
+
amount: 31.98,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
lineId: 'gift_1',
|
|
198
|
+
type: 'GIFT_CARD',
|
|
199
|
+
label: 'Gift card comp',
|
|
200
|
+
amount: -98.94,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
lineId: 'tax_1',
|
|
204
|
+
type: 'TAX',
|
|
205
|
+
label: 'Taxes and fees',
|
|
206
|
+
amount: 38.2,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
'Rounding',
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
assert.equal(summary?.subtotal, 449.4);
|
|
214
|
+
assert.equal(summary?.fullTotalAmount, 388.66);
|
|
215
|
+
assert.deepEqual(
|
|
216
|
+
summary?.lines.map((line) => (line.kind === 'line' ? line.type : 'TICKET')),
|
|
217
|
+
['TICKET', 'FEE', 'TAX', 'GIFT_CARD'],
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
170
221
|
test('builds paid checkout modal data with trimmed customer and change totals', () => {
|
|
171
222
|
const modalData = buildChangeBookingPaidCheckoutModalData({
|
|
172
223
|
bookingReference: 'bookRef_ABC-123',
|