@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.128",
3
+ "version": "1.2.129",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -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
- subtotal !== totalFromSummary ||
507
- effectivePromoDiscountAmount > 0 ||
508
- addOnTotal > 0 ||
509
- (priceSummaryAugmentation?.subtotalAdjustment ?? 0) !== 0
510
- ? displaySubtotal
511
- : undefined;
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
- !isTaxIncludedInPrice &&
514
- (effectivePromoDiscountAmount > 0 ? effectiveTax : tax) > 0
515
- ? (effectivePromoDiscountAmount > 0 ? effectiveTax : tax)
516
- : 0;
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={displayCheckoutPriceSummaryLines}
867
- totalPrice={displayTotalPrice}
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 lines =
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
- quote?.netSubtotalBeforeTax ?? quote?.subtotal ?? quote?.grossSubtotal,
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',