@ticketboothapp/booking 1.2.81 → 1.2.83

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.81",
3
+ "version": "1.2.83",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -61,6 +61,7 @@ import {
61
61
  changeFlowTicketLineTotalWithReceiptFloor,
62
62
  changeFlowFeeLineTotalWithReceiptFloor,
63
63
  changeFlowReturnLineTotalWithReceiptFloor,
64
+ reconcileTaxIncludedChangeFlowTicketLines,
64
65
  type ChangeFlowProtectedReceiptPricing,
65
66
  roundMoney,
66
67
  } from '../../lib/booking/change-flow-pricing';
@@ -3827,11 +3828,30 @@ export function AdminChangeBookingFlow({
3827
3828
  const displayChangeFlowProposedTotalWithEditableLines = roundMoney(
3828
3829
  displayChangeFlowProposedTotal + editableSummaryPreSubtotalDelta
3829
3830
  );
3831
+ /** Tax-included currencies: ticket row amounts match authoritative new-booking total (quote/display), not raw catalog/floor rollup. */
3832
+ const taxIncludedReconciledCheckoutPriceSummaryLines = useMemo(() => {
3833
+ if (!isTaxIncludedInPrice || !originalReceipt || showProviderPricingInlineEditor) {
3834
+ return editableCheckoutPriceSummaryLines;
3835
+ }
3836
+ return reconcileTaxIncludedChangeFlowTicketLines(
3837
+ editableCheckoutPriceSummaryLines,
3838
+ displayChangeFlowProposedTotalWithEditableLines,
3839
+ effectivePromoDiscountAmount,
3840
+ );
3841
+ }, [
3842
+ isTaxIncludedInPrice,
3843
+ originalReceipt,
3844
+ showProviderPricingInlineEditor,
3845
+ editableCheckoutPriceSummaryLines,
3846
+ displayChangeFlowProposedTotalWithEditableLines,
3847
+ effectivePromoDiscountAmount,
3848
+ ]);
3849
+
3830
3850
  const adminFeAuthoritativeReceipt = useMemo((): AdminFeAuthoritativeReceipt => {
3831
- const hasTaxLine = editableCheckoutPriceSummaryLines.some(
3851
+ const hasTaxLine = taxIncludedReconciledCheckoutPriceSummaryLines.some(
3832
3852
  (line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
3833
3853
  );
3834
- const lineItems = mapSummaryLinesToFeReceiptLineItems(editableCheckoutPriceSummaryLines);
3854
+ const lineItems = mapSummaryLinesToFeReceiptLineItems(taxIncludedReconciledCheckoutPriceSummaryLines);
3835
3855
  if (!hasTaxLine && Math.abs(displayChangeFlowTax) >= 0.0005) {
3836
3856
  lineItems.push({
3837
3857
  label: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
@@ -3854,7 +3874,7 @@ export function AdminChangeBookingFlow({
3854
3874
  lineItems,
3855
3875
  };
3856
3876
  }, [
3857
- editableCheckoutPriceSummaryLines,
3877
+ taxIncludedReconciledCheckoutPriceSummaryLines,
3858
3878
  displayChangeFlowTax,
3859
3879
  displayChangeFlowSubtotal,
3860
3880
  editableSummaryPreSubtotalDelta,
@@ -5014,8 +5034,11 @@ export function AdminChangeBookingFlow({
5014
5034
  }),
5015
5035
  )
5016
5036
  : totalPrice;
5037
+ const ticketRowsForCheckoutBreakdown = taxIncludedReconciledCheckoutPriceSummaryLines.filter(
5038
+ (l): l is Extract<PriceSummaryLine, { kind: 'ticket' }> => l.kind === 'ticket',
5039
+ );
5017
5040
  const lines = [
5018
- ...ticketLineItemsForChangeFlowDisplay.map((line) => ({
5041
+ ...ticketRowsForCheckoutBreakdown.map((line) => ({
5019
5042
  label: line.category,
5020
5043
  amount: line.itemTotal,
5021
5044
  type: 'TICKET' as const,
@@ -5158,7 +5181,7 @@ export function AdminChangeBookingFlow({
5158
5181
  availabilityProductOptionId,
5159
5182
  itineraryDisplay: itineraryDisplay ?? undefined,
5160
5183
  clientSecret: paymentIntent.clientSecret ?? '',
5161
- ticketLinesForModal: ticketLineItemsForChangeFlowDisplay.map((line) => {
5184
+ ticketLinesForModal: ticketRowsForCheckoutBreakdown.map((line) => {
5162
5185
  const rate = pricing.find((r) => r.category === line.category);
5163
5186
  const breakdown = getPriceBreakdown(
5164
5187
  line.category,
@@ -5166,7 +5189,16 @@ export function AdminChangeBookingFlow({
5166
5189
  rate?.baseInDisplayCurrency,
5167
5190
  rate?.appliedAdjustments ?? []
5168
5191
  );
5169
- return { line, breakdown };
5192
+ const qty = Math.max(0, line.qty);
5193
+ return {
5194
+ line: {
5195
+ category: line.category,
5196
+ qty,
5197
+ itemTotal: line.itemTotal,
5198
+ pricePerUnit: qty > 0 ? roundMoney(line.itemTotal / qty) : 0,
5199
+ },
5200
+ breakdown,
5201
+ };
5170
5202
  }),
5171
5203
  feeLineItems: feeLineItemsWithAddOns,
5172
5204
  returnPriceAdjustment: checkoutReturnLineAmount,
@@ -5185,7 +5217,7 @@ export function AdminChangeBookingFlow({
5185
5217
  return;
5186
5218
  }
5187
5219
 
5188
- const ticketLinesForModal: CheckoutModalLineItem[] = ticketLineItemsForChangeFlowDisplay.map((line) => {
5220
+ const ticketLinesForModal: CheckoutModalLineItem[] = ticketRowsForCheckoutBreakdown.map((line) => {
5189
5221
  const rate = pricing.find((r) => r.category === line.category);
5190
5222
  const breakdown = getPriceBreakdown(
5191
5223
  line.category,
@@ -5193,7 +5225,16 @@ export function AdminChangeBookingFlow({
5193
5225
  rate?.baseInDisplayCurrency,
5194
5226
  rate?.appliedAdjustments ?? []
5195
5227
  );
5196
- return { line, breakdown };
5228
+ const qty = Math.max(0, line.qty);
5229
+ return {
5230
+ line: {
5231
+ category: line.category,
5232
+ qty,
5233
+ itemTotal: line.itemTotal,
5234
+ pricePerUnit: qty > 0 ? roundMoney(line.itemTotal / qty) : 0,
5235
+ },
5236
+ breakdown,
5237
+ };
5197
5238
  });
5198
5239
 
5199
5240
  setCheckoutClientSecret(paymentIntent.clientSecret ?? '');
@@ -5724,7 +5765,7 @@ export function AdminChangeBookingFlow({
5724
5765
  {selectedAvailability && (
5725
5766
  <>
5726
5767
  <CheckoutForm
5727
- priceSummaryLines={editableCheckoutPriceSummaryLines}
5768
+ priceSummaryLines={taxIncludedReconciledCheckoutPriceSummaryLines}
5728
5769
  replacePriceSummary={hasChangeSelection ? selfServeCheckoutPlaceholder : undefined}
5729
5770
  totalPrice={changeFlowAmountDue}
5730
5771
  totalSummaryLabel={
@@ -56,6 +56,7 @@ import {
56
56
  changeFlowTicketLineTotalWithReceiptFloor,
57
57
  changeFlowFeeLineTotalWithReceiptFloor,
58
58
  changeFlowReturnLineTotalWithReceiptFloor,
59
+ reconcileTaxIncludedChangeFlowTicketLines,
59
60
  type ChangeFlowProtectedReceiptPricing,
60
61
  roundMoney,
61
62
  } from '../../lib/booking/change-flow-pricing';
@@ -3535,6 +3536,34 @@ export function ChangeBookingFlow({
3535
3536
  const displayChangeFlowSubtotal = displayedChangeAmounts.subtotal;
3536
3537
  const displayChangeFlowTax = displayedChangeAmounts.tax;
3537
3538
 
3539
+ /** When quote is confirmed but BE sends no `priceSummaryLines`, we still show FE-built lines while total is quote-driven — reconcile ticket amounts for tax-included currencies. */
3540
+ const selfServeSummaryUsesServerLines =
3541
+ suppressSelfServeCurrencyUi &&
3542
+ selfServePricingConfirmed &&
3543
+ (latestChangeQuote?.serverPreview?.priceSummaryLines?.length ?? 0) > 0;
3544
+
3545
+ const taxIncludedSelfServeReconciledPriceSummaryLines = useMemo(() => {
3546
+ if (!isTaxIncludedInPrice || !originalReceipt || !isCustomerSelfServeChange) {
3547
+ return checkoutPriceSummaryLinesForCheckout;
3548
+ }
3549
+ if (selfServeSummaryUsesServerLines) {
3550
+ return checkoutPriceSummaryLinesForCheckout;
3551
+ }
3552
+ return reconcileTaxIncludedChangeFlowTicketLines(
3553
+ checkoutPriceSummaryLinesForCheckout,
3554
+ displayChangeFlowProposedTotal,
3555
+ effectivePromoDiscountAmount,
3556
+ );
3557
+ }, [
3558
+ isTaxIncludedInPrice,
3559
+ originalReceipt,
3560
+ isCustomerSelfServeChange,
3561
+ selfServeSummaryUsesServerLines,
3562
+ checkoutPriceSummaryLinesForCheckout,
3563
+ displayChangeFlowProposedTotal,
3564
+ effectivePromoDiscountAmount,
3565
+ ]);
3566
+
3538
3567
  const changeFlowClientEstimateDue = (() => {
3539
3568
  if (!originalReceipt) return totalPrice;
3540
3569
  // Customer self-serve: amount due comes from POST .../change/quote (`amountDueCents` / priceDiff), not FE delta math.
@@ -4437,8 +4466,11 @@ export function ChangeBookingFlow({
4437
4466
  originalReceiptTotal: originalReceipt?.total ?? 0,
4438
4467
  audience: 'customer',
4439
4468
  });
4469
+ const ticketRowsForCheckoutBreakdown = taxIncludedSelfServeReconciledPriceSummaryLines.filter(
4470
+ (l): l is Extract<PriceSummaryLine, { kind: 'ticket' }> => l.kind === 'ticket',
4471
+ );
4440
4472
  const lines = [
4441
- ...ticketLineItemsForChangeFlowDisplay.map((line) => ({
4473
+ ...ticketRowsForCheckoutBreakdown.map((line) => ({
4442
4474
  label: line.category,
4443
4475
  amount: line.itemTotal,
4444
4476
  type: 'TICKET' as const,
@@ -4523,7 +4555,7 @@ export function ChangeBookingFlow({
4523
4555
  })()
4524
4556
  );
4525
4557
 
4526
- const ticketLinesForModal: CheckoutModalLineItem[] = ticketLineItemsForChangeFlowDisplay.map((line) => {
4558
+ const ticketLinesForModal: CheckoutModalLineItem[] = ticketRowsForCheckoutBreakdown.map((line) => {
4527
4559
  const rate = pricing.find((r) => r.category === line.category);
4528
4560
  const breakdown = getPriceBreakdown(
4529
4561
  line.category,
@@ -4531,7 +4563,16 @@ export function ChangeBookingFlow({
4531
4563
  rate?.baseInDisplayCurrency,
4532
4564
  rate?.appliedAdjustments ?? []
4533
4565
  );
4534
- return { line, breakdown };
4566
+ const qty = Math.max(0, line.qty);
4567
+ return {
4568
+ line: {
4569
+ category: line.category,
4570
+ qty,
4571
+ itemTotal: line.itemTotal,
4572
+ pricePerUnit: qty > 0 ? roundMoney(line.itemTotal / qty) : 0,
4573
+ },
4574
+ breakdown,
4575
+ };
4535
4576
  });
4536
4577
 
4537
4578
  setCheckoutClientSecret(paymentIntent.clientSecret ?? '');
@@ -4883,7 +4924,7 @@ export function ChangeBookingFlow({
4883
4924
  {selectedAvailability && (
4884
4925
  <>
4885
4926
  <CheckoutForm
4886
- priceSummaryLines={checkoutPriceSummaryLinesForCheckout}
4927
+ priceSummaryLines={taxIncludedSelfServeReconciledPriceSummaryLines}
4887
4928
  replacePriceSummary={selfServeCheckoutPlaceholder}
4888
4929
  totalPrice={changeFlowAmountDue}
4889
4930
  totalSummaryLabel={
@@ -46,6 +46,7 @@
46
46
  * **Provider** inline pricing may show a **signed** delta from cart or manual overrides.
47
47
  */
48
48
  import type { ChangeBookingQuoteResponse } from '../booking-api';
49
+ import type { PriceSummaryLine } from '../../components/booking/PriceSummary';
49
50
  import { reconcileChangeBookingProposedTotal } from '../currency';
50
51
 
51
52
  /** Money in major units, rounded to cents (half-up). */
@@ -301,6 +302,77 @@ export function resolveChangeFlowDisplayedAmounts(input: {
301
302
  return { ...input.fromCart };
302
303
  }
303
304
 
305
+ function isPromoSummaryLine(row: PriceSummaryLine): boolean {
306
+ if (row.kind !== 'line') return false;
307
+ const t = String(row.type ?? '').toUpperCase();
308
+ if (t === 'PROMO_CODE' || t === 'GIFT_CARD' || t === 'DISCOUNT' || t === 'VOUCHER') return true;
309
+ return row.amount < -0.005 && /promo|discount|voucher|gift/i.test(row.label);
310
+ }
311
+
312
+ /**
313
+ * Tax-included currencies (e.g. EUR): ticket rows from catalog + receipt floors can disagree with the
314
+ * authoritative **new booking total** (quote / display layer). Scale ticket `itemTotal` values so
315
+ * tickets + non-ticket lines + promo = `targetNewBookingTotal` without changing quantities or non-ticket rows.
316
+ */
317
+ export function reconcileTaxIncludedChangeFlowTicketLines(
318
+ lines: PriceSummaryLine[],
319
+ targetNewBookingTotal: number,
320
+ promoDiscountAmountIfNotInLines: number,
321
+ ): PriceSummaryLine[] {
322
+ if (!Number.isFinite(targetNewBookingTotal) || lines.length === 0) return lines;
323
+
324
+ const ticketIndices: number[] = [];
325
+ let currentTicketTotal = 0;
326
+ for (let i = 0; i < lines.length; i++) {
327
+ const row = lines[i];
328
+ if (row.kind === 'ticket') {
329
+ ticketIndices.push(i);
330
+ currentTicketTotal += row.itemTotal;
331
+ }
332
+ }
333
+ if (ticketIndices.length === 0) return lines;
334
+
335
+ let nonTicketSum = 0;
336
+ let promoLineSum = 0;
337
+ for (const row of lines) {
338
+ if (row.kind === 'line') {
339
+ const t = String(row.type ?? '').toUpperCase();
340
+ if (t === 'TAX') continue;
341
+ if (isPromoSummaryLine(row)) {
342
+ promoLineSum += row.amount;
343
+ continue;
344
+ }
345
+ nonTicketSum += row.amount;
346
+ }
347
+ }
348
+
349
+ const desiredTicketTotal =
350
+ Math.abs(promoLineSum) >= 0.005
351
+ ? roundMoney(targetNewBookingTotal - nonTicketSum - promoLineSum)
352
+ : roundMoney(
353
+ targetNewBookingTotal - nonTicketSum + Math.max(0, promoDiscountAmountIfNotInLines),
354
+ );
355
+
356
+ if (currentTicketTotal <= 0 || Math.abs(desiredTicketTotal - currentTicketTotal) < 0.02) {
357
+ return lines;
358
+ }
359
+
360
+ const scale = desiredTicketTotal / currentTicketTotal;
361
+ const next = lines.slice() as PriceSummaryLine[];
362
+ let running = 0;
363
+ for (let k = 0; k < ticketIndices.length; k++) {
364
+ const i = ticketIndices[k];
365
+ const row = next[i] as Extract<PriceSummaryLine, { kind: 'ticket' }>;
366
+ const isLast = k === ticketIndices.length - 1;
367
+ const newTotal = isLast
368
+ ? roundMoney(desiredTicketTotal - running)
369
+ : roundMoney(row.itemTotal * scale);
370
+ running += newTotal;
371
+ next[i] = { ...row, itemTotal: newTotal };
372
+ }
373
+ return next;
374
+ }
375
+
304
376
  /**
305
377
  * Product: **Main “total owed” number** should not show -0.00 or noise when the balance is effectively zero.
306
378
  */