@ticketboothapp/booking 1.2.129 → 1.2.131

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.129",
3
+ "version": "1.2.131",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -48,27 +48,37 @@ function isPaymentCreditType(type?: string | null): boolean {
48
48
  return normalized === "GIFT_CARD" || normalized === "PAYMENT_CREDIT";
49
49
  }
50
50
 
51
- function isPaymentCreditSummaryLine(line: PriceSummaryLine): boolean {
52
- return line.kind === "line" && isPaymentCreditType(line.type);
51
+ function isPaymentCreditQuoteLine(line: PricingV2QuoteLineSnapshot): boolean {
52
+ return isPaymentCreditType(line.type);
53
53
  }
54
54
 
55
- function movePaymentCreditLinesAfterTax(lines: PriceSummaryLine[]): PriceSummaryLine[] {
56
- const paymentCreditLines = lines.filter(isPaymentCreditSummaryLine);
57
- if (paymentCreditLines.length === 0) return lines;
55
+ function isPostTaxDiscountQuoteLine(line: PricingV2QuoteLineSnapshot): boolean {
56
+ const discountBehavior = String(line.discountBehavior ?? "").trim().toUpperCase();
57
+ const source = String(line.source ?? "").trim().toUpperCase();
58
+ return discountBehavior === "POST_TAX_DISCOUNT" || source === "VOUCHER";
59
+ }
58
60
 
59
- const nonPaymentCreditLines = lines.filter((line) => !isPaymentCreditSummaryLine(line));
60
- const taxIndex = nonPaymentCreditLines.findIndex(
61
- (line) => line.kind === "line" && normalizedLineType(line.type) === "TAX",
61
+ function moveSettlementLinesAfterTax(
62
+ lines: PricingV2QuoteLineSnapshot[],
63
+ ): PricingV2QuoteLineSnapshot[] {
64
+ const settlementLines = lines.filter(
65
+ (line) => isPaymentCreditQuoteLine(line) || isPostTaxDiscountQuoteLine(line),
66
+ );
67
+ if (settlementLines.length === 0) return lines;
68
+
69
+ const nonSettlementLines = lines.filter(
70
+ (line) => !isPaymentCreditQuoteLine(line) && !isPostTaxDiscountQuoteLine(line),
62
71
  );
72
+ const taxIndex = nonSettlementLines.findIndex((line) => normalizedLineType(line.type) === "TAX");
63
73
 
64
74
  if (taxIndex < 0) {
65
- return [...nonPaymentCreditLines, ...paymentCreditLines];
75
+ return [...nonSettlementLines, ...settlementLines];
66
76
  }
67
77
 
68
78
  return [
69
- ...nonPaymentCreditLines.slice(0, taxIndex + 1),
70
- ...paymentCreditLines,
71
- ...nonPaymentCreditLines.slice(taxIndex + 1),
79
+ ...nonSettlementLines.slice(0, taxIndex + 1),
80
+ ...settlementLines,
81
+ ...nonSettlementLines.slice(taxIndex + 1),
72
82
  ];
73
83
  }
74
84
 
@@ -112,11 +122,10 @@ export function buildCheckoutModalSummaryFromPricingV2Quote(
112
122
  const total = pricingV2QuoteTotal(quote);
113
123
  if (total == null) return undefined;
114
124
 
115
- const mappedLines =
116
- quote?.lines
117
- ?.map((line, index) => priceSummaryLineFromQuoteLine(line, index))
118
- .filter((line): line is PriceSummaryLine => line != null) ?? [];
119
- const lines = movePaymentCreditLinesAfterTax(mappedLines);
125
+ const quoteLines = quote?.lines ?? [];
126
+ const lines = moveSettlementLinesAfterTax(quoteLines)
127
+ .map((line, index) => priceSummaryLineFromQuoteLine(line, index))
128
+ .filter((line): line is PriceSummaryLine => line != null);
120
129
 
121
130
  if (lines.length === 0) return undefined;
122
131
 
@@ -156,8 +165,9 @@ export function buildCheckoutModalSummaryFromPricingV2Quote(
156
165
  0,
157
166
  finiteNumber(quote?.paymentCreditTotal) ?? 0,
158
167
  );
168
+ const hasPostTaxDiscount = quoteLines.some(isPostTaxDiscountQuoteLine);
159
169
  const subtotal = finiteNumber(
160
- paymentCreditTotal > 0
170
+ paymentCreditTotal > 0 || hasPostTaxDiscount
161
171
  ? quote?.taxableSubtotal ??
162
172
  quote?.grossSubtotal ??
163
173
  quote?.netSubtotalBeforeTax ??
@@ -112,10 +112,21 @@ export function useStandardBookingPriceSummary({
112
112
  return Math.max(0, selectedReturnOption.vacancies ?? 0);
113
113
  }, [selectedReturnOption]);
114
114
 
115
+ const pricingAvailability = useMemo(() => {
116
+ if (!selectedAvailability || !selectedReturnOption?.rates?.length) return selectedAvailability;
117
+ const outboundVacancies = Math.max(0, selectedAvailability.vacancies ?? 0);
118
+ const returnVacancies = Math.max(0, selectedReturnOption.vacancies ?? 0);
119
+ return {
120
+ ...selectedAvailability,
121
+ vacancies: Math.min(outboundVacancies, returnVacancies),
122
+ rates: selectedReturnOption.rates,
123
+ };
124
+ }, [selectedAvailability, selectedReturnOption?.rates, selectedReturnOption?.vacancies]);
125
+
115
126
  const pricing = useMemo(
116
127
  () =>
117
128
  buildPricingFromAvailability(
118
- selectedAvailability,
129
+ pricingAvailability,
119
130
  activeOptions,
120
131
  precomputedPricesByOption,
121
132
  currency,
@@ -124,7 +135,7 @@ export function useStandardBookingPriceSummary({
124
135
  isSimplifiedPricingView,
125
136
  ),
126
137
  [
127
- selectedAvailability,
138
+ pricingAvailability,
128
139
  activeOptions,
129
140
  precomputedPricesByOption,
130
141
  currency,
@@ -145,7 +156,7 @@ export function useStandardBookingPriceSummary({
145
156
  const selectedOption = activeOptions.find(
146
157
  (option) =>
147
158
  option.optionId ===
148
- (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''),
159
+ (pricingAvailability ? getAvailabilityOptionId(pricingAvailability) : ''),
149
160
  );
150
161
  const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
151
162
  return computePriceBreakdown(
@@ -160,7 +171,7 @@ export function useStandardBookingPriceSummary({
160
171
  isSimplifiedPricingView,
161
172
  );
162
173
  },
163
- [pricingConfig, activeOptions, selectedAvailability, currency, hasFees, isSimplifiedPricingView],
174
+ [pricingConfig, activeOptions, pricingAvailability, currency, hasFees, isSimplifiedPricingView],
164
175
  );
165
176
 
166
177
  const orderSummary = useMemo(
@@ -352,7 +363,7 @@ export function useStandardBookingPriceSummary({
352
363
  totalPrice,
353
364
  } = useStandardBookingPromoDiscount({
354
365
  appliedPromoCode,
355
- selectedAvailability,
366
+ selectedAvailability: pricingAvailability,
356
367
  totalQuantity,
357
368
  productCompanyId,
358
369
  envCompanyId,
@@ -11,6 +11,8 @@ import type {
11
11
  import type { Currency } from './CurrencySwitcher';
12
12
  import { getAvailabilityOptionId } from './standard-booking-availability';
13
13
 
14
+ const normalizeMoneyZero = (value: number) => (Math.abs(value) < 0.005 ? 0 : value);
15
+
14
16
  interface UseStandardBookingPromoDiscountParams {
15
17
  appliedPromoCode: string | null;
16
18
  selectedAvailability: Availability | null;
@@ -188,9 +190,9 @@ export function useStandardBookingPromoDiscount({
188
190
  const taxOnSubtotal = isTaxIncludedInPrice ? 0 : effectiveSubtotal * (pricingConfig?.taxRate ?? 0);
189
191
  const effectiveTax =
190
192
  effectivePromoDiscountAmount > 0 && !isGiftCard && !isVoucher
191
- ? (effectiveSubtotal - effectivePromoDiscountAmount) * (pricingConfig?.taxRate ?? 0)
192
- : taxOnSubtotal;
193
- const totalPrice = effectiveSubtotal + effectiveTax - effectivePromoDiscountAmount;
193
+ ? normalizeMoneyZero((effectiveSubtotal - effectivePromoDiscountAmount) * (pricingConfig?.taxRate ?? 0))
194
+ : normalizeMoneyZero(taxOnSubtotal);
195
+ const totalPrice = normalizeMoneyZero(effectiveSubtotal + effectiveTax - effectivePromoDiscountAmount);
194
196
 
195
197
  return {
196
198
  promoDiscountAmount,
@@ -1669,6 +1669,8 @@ export interface ReturnOption {
1669
1669
  bookedCapacity?: number;
1670
1670
  pricesByCategory?: { retailPrices: Array<{ category: string; price: number }> };
1671
1671
  priceAdjustmentByCurrency?: Record<string, number>;
1672
+ /** Ticket rates priced using this specific return option's capacity floor. */
1673
+ rates?: AvailabilityRate[];
1672
1674
  returnLocation: string;
1673
1675
  mostPopular?: boolean;
1674
1676
  }
@@ -218,6 +218,59 @@ test('maps Pricing V2 gift cards as payment credits after tax', () => {
218
218
  );
219
219
  });
220
220
 
221
+ test('maps Pricing V2 post-tax vouchers after tax with full taxable subtotal', () => {
222
+ const summary = buildCheckoutModalSummaryFromPricingV2Quote(
223
+ {
224
+ currency: 'CAD',
225
+ grossSubtotal: 629.18,
226
+ discountTotal: -455.11,
227
+ paymentCreditTotal: 0,
228
+ netSubtotalBeforeTax: 174.07,
229
+ taxableSubtotal: 629.18,
230
+ taxAmount: 53.48,
231
+ payableTotal: 227.55,
232
+ amountToCharge: 227.55,
233
+ lines: [
234
+ {
235
+ lineId: 'ticket_1',
236
+ type: 'TICKET',
237
+ label: 'ADULT',
238
+ amount: 581.21,
239
+ quantity: 3,
240
+ },
241
+ {
242
+ lineId: 'fee_1',
243
+ type: 'FEE',
244
+ label: 'Moraine Lake Road Access Fee',
245
+ amount: 47.97,
246
+ },
247
+ {
248
+ lineId: 'voucher_1',
249
+ type: 'DISCOUNT',
250
+ label: "Les Clefs d'Or",
251
+ amount: -455.11,
252
+ discountBehavior: 'POST_TAX_DISCOUNT',
253
+ source: 'VOUCHER',
254
+ },
255
+ {
256
+ lineId: 'tax_1',
257
+ type: 'TAX',
258
+ label: 'Taxes and fees',
259
+ amount: 53.48,
260
+ },
261
+ ],
262
+ },
263
+ 'Rounding',
264
+ );
265
+
266
+ assert.equal(summary?.subtotal, 629.18);
267
+ assert.equal(summary?.fullTotalAmount, 227.55);
268
+ assert.deepEqual(
269
+ summary?.lines.map((line) => (line.kind === 'line' ? line.type : 'TICKET')),
270
+ ['TICKET', 'FEE', 'TAX', 'DISCOUNT'],
271
+ );
272
+ });
273
+
221
274
  test('builds paid checkout modal data with trimmed customer and change totals', () => {
222
275
  const modalData = buildChangeBookingPaidCheckoutModalData({
223
276
  bookingReference: 'bookRef_ABC-123',