@ticketboothapp/booking 1.2.159 → 1.2.161

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.159",
3
+ "version": "1.2.161",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -60,6 +60,7 @@ import { useSeedPromoCodeInput } from './useSeedPromoCodeInput';
60
60
  import { useBookingQuantityCapTrim } from './useBookingQuantityCapTrim';
61
61
  import { useBookingViewItemAnalytics } from './useBookingViewItemAnalytics';
62
62
  import type { ChangeBookingMergedQuoteState } from './change-booking-quote-state';
63
+ import { AdminChangeReceiptComparison } from './AdminChangeReceiptComparison';
63
64
 
64
65
  export type { ChangeBookingFlowProps } from './booking-flow-types';
65
66
 
@@ -504,7 +505,6 @@ export function ChangeBookingFlow({
504
505
  effectiveSubtotal,
505
506
  feeLineItemsWithAddOns,
506
507
  checkoutPriceSummaryLinesForCheckout,
507
- priceSummaryLinesIncludeTaxRow,
508
508
  effectivePromoDiscountAmount,
509
509
  effectiveTax,
510
510
  totalPrice,
@@ -581,7 +581,7 @@ export function ChangeBookingFlow({
581
581
  displayChangeFlowProposedTotal,
582
582
  displayChangeFlowSubtotal,
583
583
  displayChangeFlowTax,
584
- taxIncludedSelfServeReconciledPriceSummaryLines,
584
+ publicChangeAmountDueSummary,
585
585
  checkoutModalTicketLinesDisplay,
586
586
  changeFlowClientEstimateDue,
587
587
  changeFlowAmountDue,
@@ -600,6 +600,22 @@ export function ChangeBookingFlow({
600
600
  totalPrice,
601
601
  });
602
602
 
603
+ const publicReceiptComparison = originalReceipt ? (
604
+ <AdminChangeReceiptComparison
605
+ originalReceipt={originalReceipt}
606
+ amendmentLines={latestChangeQuote?.serverPreview?.priceSummaryLines ?? null}
607
+ amountDue={latestChangeQuote?.serverPreview?.amountDue ?? null}
608
+ updatedTotal={latestChangeQuote?.serverPreview?.totalNewBooking ?? null}
609
+ selectionChanged={hasEffectiveChangeSelection}
610
+ quoteError={changeQuoteFetchError}
611
+ amountDueLabel="Amount due now"
612
+ currency={currency}
613
+ locale={locale}
614
+ t={t}
615
+ taxRate={pricingConfig?.taxRate}
616
+ />
617
+ ) : null;
618
+
603
619
  const {
604
620
  missingRequiredReturnSelection,
605
621
  isChangeQuoteBlocked,
@@ -926,11 +942,11 @@ export function ChangeBookingFlow({
926
942
  {/* Total and Checkout — shared PriceSummary component */}
927
943
  {selectedAvailability && (
928
944
  <ChangeBookingCheckoutPanel
929
- priceSummaryLines={taxIncludedSelfServeReconciledPriceSummaryLines}
930
- replacePriceSummary={selfServeCheckoutPlaceholder}
945
+ priceSummaryLines={publicChangeAmountDueSummary.lines}
946
+ replacePriceSummary={selfServeCheckoutPlaceholder ?? publicReceiptComparison}
931
947
  totalPrice={changeFlowAmountDue}
932
- subtotal={displayChangeFlowSubtotal}
933
- taxAmount={displayChangeFlowTax}
948
+ subtotal={publicChangeAmountDueSummary.subtotal}
949
+ taxAmount={publicChangeAmountDueSummary.tax}
934
950
  taxRate={pricingConfig?.taxRate}
935
951
  currency={currency}
936
952
  locale={locale}
@@ -968,9 +984,9 @@ export function ChangeBookingFlow({
968
984
  attributionConfirmLabel={flowUi?.partnerAttributionConfirmLabel}
969
985
  attributionConfirmed={partnerAttributionConfirmed}
970
986
  onAttributionConfirmedChange={setPartnerAttributionConfirmed}
971
- priceSummaryLinesIncludeTaxRow={priceSummaryLinesIncludeTaxRow}
987
+ priceSummaryLinesIncludeTaxRow={false}
972
988
  isTaxIncludedInPrice={isTaxIncludedInPrice}
973
- displayChangeFlowTax={displayChangeFlowTax}
989
+ displayChangeFlowTax={publicChangeAmountDueSummary.tax}
974
990
  />
975
991
  )}
976
992
  </div>
@@ -112,6 +112,41 @@ export function mergePickerTicketQtyIntoPriceSummaryLines(
112
112
  });
113
113
  }
114
114
 
115
+ /**
116
+ * Build the customer-facing amount-due summary from authoritative amendment
117
+ * lines. Change quotes contain only the incremental lines, so they must not be
118
+ * scaled to the booking's final picker quantities or mixed with the full new
119
+ * booking subtotal. Taxes are combined into the shared summary tax row to keep
120
+ * the preview auditable: charges, amendment subtotal, tax, amount due.
121
+ */
122
+ export function buildPublicChangeAmountDueSummary(lines: PriceSummaryLine[]): {
123
+ lines: PriceSummaryLine[];
124
+ subtotal: number;
125
+ tax: number;
126
+ } {
127
+ const isTaxLine = (line: PriceSummaryLine): boolean =>
128
+ line.kind === 'line' && String(line.type ?? '').trim().toUpperCase() === 'TAX';
129
+ const nonTaxLines = lines.filter((line) => !isTaxLine(line));
130
+ const ticketLines = nonTaxLines.filter((line) => line.kind === 'ticket');
131
+ const otherLines = nonTaxLines.filter((line) => line.kind !== 'ticket');
132
+
133
+ return {
134
+ lines: [...ticketLines, ...otherLines],
135
+ subtotal: roundMoney(
136
+ nonTaxLines.reduce(
137
+ (sum, line) => sum + (line.kind === 'ticket' ? line.itemTotal : line.amount),
138
+ 0,
139
+ ),
140
+ ),
141
+ tax: roundMoney(
142
+ lines.reduce(
143
+ (sum, line) => sum + (isTaxLine(line) && line.kind === 'line' ? line.amount : 0),
144
+ 0,
145
+ ),
146
+ ),
147
+ };
148
+ }
149
+
115
150
  export function normalizeFeReceiptLineItemType(type: string | null | undefined): string | undefined {
116
151
  const normalized = String(type ?? '').trim().toUpperCase();
117
152
  if (!normalized) return undefined;
@@ -115,11 +115,13 @@ function resolveFeChangeDue({
115
115
  function assertNoUnconfirmedNoChargeUpgrade({
116
116
  quote,
117
117
  quoteSlice,
118
+ originalReceiptTotal,
118
119
  audience,
119
120
  useAdminFeAuthoritativeQuote,
120
121
  }: {
121
122
  quote: ChangeBookingQuoteResponse;
122
123
  quoteSlice: ChangeQuoteUiSlice;
124
+ originalReceiptTotal: number;
123
125
  audience: ChangeBookingCheckoutQuoteAudience;
124
126
  useAdminFeAuthoritativeQuote?: boolean;
125
127
  }): void {
@@ -141,9 +143,7 @@ function assertNoUnconfirmedNoChargeUpgrade({
141
143
  pricingQuote?.totalAmount ??
142
144
  quote.proposed?.total ??
143
145
  quote.newReceipt?.total;
144
- const originalTotal =
145
- pricingQuote?.amountPreviouslyPaid ?? quote.original?.total ?? quote.originalReceipt?.total;
146
- if (proposedTotal != null && originalTotal != null && proposedTotal - originalTotal > 0.01) {
146
+ if (proposedTotal != null && proposedTotal - originalReceiptTotal > 0.01) {
147
147
  throw new Error(CHANGE_PAYMENT_CONFIRMATION_ERROR);
148
148
  }
149
149
  }
@@ -198,6 +198,7 @@ export function evaluateChangeBookingQuoteForCheckout(
198
198
  assertNoUnconfirmedNoChargeUpgrade({
199
199
  quote,
200
200
  quoteSlice,
201
+ originalReceiptTotal,
201
202
  audience,
202
203
  useAdminFeAuthoritativeQuote,
203
204
  });
@@ -11,7 +11,10 @@ import type { ChangeBookingFlowProps } from './booking-flow-types';
11
11
  import type { ChangeBookingMergedQuoteState } from './change-booking-quote-state';
12
12
  import type { CheckoutModalLineItem } from './CheckoutModal';
13
13
  import type { PriceSummaryLine } from './PriceSummary';
14
- import type { PricingRate } from './change-booking-flow-helpers';
14
+ import {
15
+ buildPublicChangeAmountDueSummary,
16
+ type PricingRate,
17
+ } from './change-booking-flow-helpers';
15
18
 
16
19
  type GetPriceBreakdown = (
17
20
  category: string,
@@ -103,6 +106,13 @@ export function useChangeBookingReceiptDerivation({
103
106
  });
104
107
  }, [taxIncludedSelfServeReconciledPriceSummaryLines, pricing, getPriceBreakdown]);
105
108
 
109
+ const publicChangeAmountDueSummary = useMemo(() => {
110
+ const authoritativeAmendmentLines = latestChangeQuote?.serverPreview?.priceSummaryLines;
111
+ return buildPublicChangeAmountDueSummary(
112
+ authoritativeAmendmentLines ?? taxIncludedSelfServeReconciledPriceSummaryLines,
113
+ );
114
+ }, [latestChangeQuote, taxIncludedSelfServeReconciledPriceSummaryLines]);
115
+
106
116
  const changeFlowClientEstimateDue = (() => {
107
117
  if (!originalReceipt) return totalPrice;
108
118
  if (latestChangeQuote != null && !changeQuoteFetchError) {
@@ -121,6 +131,7 @@ export function useChangeBookingReceiptDerivation({
121
131
  displayChangeFlowSubtotal,
122
132
  displayChangeFlowTax,
123
133
  taxIncludedSelfServeReconciledPriceSummaryLines,
134
+ publicChangeAmountDueSummary,
124
135
  checkoutModalTicketLinesDisplay,
125
136
  changeFlowClientEstimateDue,
126
137
  changeFlowAmountDue,
@@ -33,6 +33,7 @@ import {
33
33
  reservationHoldSecondsRemaining,
34
34
  } from '../src/components/booking/reservation-hold';
35
35
  import { buildAdminChangeProviderPayload } from '../src/components/booking/admin-change-provider-payload';
36
+ import { buildPublicChangeAmountDueSummary } from '../src/components/booking/change-booking-flow-helpers';
36
37
 
37
38
  function quote(overrides: Partial<ChangeBookingQuoteResponse>): ChangeBookingQuoteResponse {
38
39
  return {
@@ -186,6 +187,50 @@ test('admin no-charge provider payload carries the selected pickup itinerary', (
186
187
  assert.equal(payload?.travelerHotel, 'A Bear and Bison Inn');
187
188
  });
188
189
 
190
+ test('public paid change preview shows amendment-only charges before combined tax', () => {
191
+ const summary = buildPublicChangeAmountDueSummary([
192
+ {
193
+ kind: 'line',
194
+ label: 'Moraine Lake Road Access Fee',
195
+ amount: 15.99,
196
+ type: 'FEE',
197
+ },
198
+ {
199
+ kind: 'line',
200
+ label: 'Tax · Moraine Lake Road Access Fee',
201
+ amount: 1.36,
202
+ type: 'TAX',
203
+ },
204
+ {
205
+ kind: 'ticket',
206
+ category: 'ADULT',
207
+ qty: 1,
208
+ itemTotal: 181.49,
209
+ },
210
+ {
211
+ kind: 'line',
212
+ label: 'Tax · ADULT',
213
+ amount: 15.43,
214
+ type: 'TAX',
215
+ },
216
+ ]);
217
+
218
+ assert.deepEqual(
219
+ summary.lines.map((line) =>
220
+ line.kind === 'ticket'
221
+ ? { label: line.category, quantity: line.qty, amount: line.itemTotal }
222
+ : { label: line.label, amount: line.amount },
223
+ ),
224
+ [
225
+ { label: 'ADULT', quantity: 1, amount: 181.49 },
226
+ { label: 'Moraine Lake Road Access Fee', amount: 15.99 },
227
+ ],
228
+ );
229
+ assert.equal(summary.subtotal, 197.48);
230
+ assert.equal(summary.tax, 16.79);
231
+ assert.equal(Number((summary.subtotal + summary.tax).toFixed(2)), 214.27);
232
+ });
233
+
189
234
  test('admin removal quote preserves explicit release refund decisions for the preview', () => {
190
235
  const operation = {
191
236
  operationId: 'op_remove_adult',
@@ -398,6 +443,37 @@ test('routes customer no-pay quote to free confirmation with the quote intent',
398
443
  });
399
444
  });
400
445
 
446
+ test('allows pay-later customer no-charge change when receipt total is unchanged', () => {
447
+ const decision = evaluateChangeBookingQuoteForCheckout({
448
+ quote: quote({
449
+ changeIntentId: 'ci_pay_later_free',
450
+ amountToCharge: 0,
451
+ balanceDelta: 0,
452
+ pricingQuote: {
453
+ payableTotal: 202.87,
454
+ amountPreviouslyPaid: 0,
455
+ balanceDelta: 0,
456
+ amountToCharge: 0,
457
+ },
458
+ proposed: { total: 202.87 },
459
+ original: { total: 202.87 },
460
+ }),
461
+ quoteSlice: quoteSlice({
462
+ changeIntentId: 'ci_pay_later_free',
463
+ quotedTotal: 202.87,
464
+ }),
465
+ fallbackNewBookingTotal: 202.87,
466
+ originalReceiptTotal: 202.87,
467
+ audience: 'customer',
468
+ });
469
+
470
+ assert.deepEqual(decision, {
471
+ kind: 'free',
472
+ changeIntentId: 'ci_pay_later_free',
473
+ amountDueForCheckout: 0,
474
+ });
475
+ });
476
+
401
477
  test('blocks customer upgrade when server confirms no charge but totals increased', () => {
402
478
  assert.throws(
403
479
  () =>