@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.127",
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,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={displayCheckoutPriceSummaryLines}
866
- totalPrice={displayTotalPrice}
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}
@@ -669,6 +669,7 @@ export function PrivateShuttleBookingFlow({
669
669
  pricingConfig,
670
670
  refreshSelectedDateDetailsForCheckout,
671
671
  reloadAvailabilitiesAfterReserveConflict,
672
+ pricingProfileIdForAvailabilities,
672
673
  totalPrice,
673
674
  subtotal,
674
675
  activePromoCode,
@@ -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
@@ -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';
@@ -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',