@ticketboothapp/booking 1.2.159 → 1.2.160

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.160",
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;
@@ -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',