@ticketboothapp/booking 1.2.128 → 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 +100 -14
- package/src/components/booking/StandardBookingCheckoutSection.tsx +3 -0
- package/src/components/booking/pricing-v2-checkout-summary.ts +41 -2
- package/src/components/booking/standard-booking-checkout-controller.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,8 +728,8 @@ 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,
|
|
@@ -678,6 +762,7 @@ export function NewBookingFlow({
|
|
|
678
762
|
setError,
|
|
679
763
|
onSuccess,
|
|
680
764
|
onShowManage,
|
|
765
|
+
onPricingV2QuoteForDisplay: setCheckoutPricingV2QuoteForDisplay,
|
|
681
766
|
});
|
|
682
767
|
|
|
683
768
|
if (activeOptions.length === 0) {
|
|
@@ -863,8 +948,8 @@ export function NewBookingFlow({
|
|
|
863
948
|
{/* Total and Checkout — shared PriceSummary component */}
|
|
864
949
|
{!selectedBookingOptionsHydrating && selectedAvailability && (
|
|
865
950
|
<StandardBookingCheckoutSection
|
|
866
|
-
priceSummaryLines={
|
|
867
|
-
totalPrice={
|
|
951
|
+
priceSummaryLines={checkoutDisplayPriceSummaryLines}
|
|
952
|
+
totalPrice={checkoutDisplayTotalPrice}
|
|
868
953
|
subtotal={checkoutFormSubtotal}
|
|
869
954
|
taxAmount={checkoutFormTaxAmount}
|
|
870
955
|
taxRate={pricingConfig?.taxRate}
|
|
@@ -876,6 +961,7 @@ export function NewBookingFlow({
|
|
|
876
961
|
promoCodeError={promoCodeError}
|
|
877
962
|
promoCodeValidating={promoCodeValidating}
|
|
878
963
|
promoDiscountAmount={promoDiscountAmount}
|
|
964
|
+
hidePromoDiscountAmount={hasServerBackedCheckoutDisplay}
|
|
879
965
|
onPromoInputChange={handlePromoInputChange}
|
|
880
966
|
onPromoApply={handleApplyPromo}
|
|
881
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
|
|
@@ -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';
|
|
@@ -107,6 +108,7 @@ export interface UseStandardBookingCheckoutControllerParams {
|
|
|
107
108
|
setError: (error: string) => void;
|
|
108
109
|
onSuccess?: NewBookingFlowProps['onSuccess'];
|
|
109
110
|
onShowManage?: (params: { ref: string; lastName: string }) => void;
|
|
111
|
+
onPricingV2QuoteForDisplay?: (quote: PricingV2QuoteSnapshot | null) => void;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
export function useStandardBookingCheckoutController({
|
|
@@ -170,6 +172,7 @@ export function useStandardBookingCheckoutController({
|
|
|
170
172
|
setError,
|
|
171
173
|
onSuccess,
|
|
172
174
|
onShowManage,
|
|
175
|
+
onPricingV2QuoteForDisplay,
|
|
173
176
|
}: UseStandardBookingCheckoutControllerParams) {
|
|
174
177
|
const [showCheckoutModal, setShowCheckoutModal] = useState(false);
|
|
175
178
|
const [checkoutClientSecret, setCheckoutClientSecret] = useState('');
|
|
@@ -269,6 +272,7 @@ export function useStandardBookingCheckoutController({
|
|
|
269
272
|
setLoading(true);
|
|
270
273
|
setError('');
|
|
271
274
|
paymentSubmitInFlightRef.current = false;
|
|
275
|
+
onPricingV2QuoteForDisplay?.(null);
|
|
272
276
|
|
|
273
277
|
try {
|
|
274
278
|
const reserveResult = await reserveStandardBookingForCheckout({
|
|
@@ -309,6 +313,7 @@ export function useStandardBookingCheckoutController({
|
|
|
309
313
|
timePart,
|
|
310
314
|
itineraryDisplay,
|
|
311
315
|
} = reserveResult;
|
|
316
|
+
onPricingV2QuoteForDisplay?.(pricingV2Quote?.quote ?? null);
|
|
312
317
|
pendingReservationRef.current = { reservationReference: reservation.reservationReference };
|
|
313
318
|
|
|
314
319
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
@@ -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',
|