@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 +1 -1
- package/src/components/booking/ChangeBookingFlow.tsx +24 -8
- package/src/components/booking/change-booking-flow-helpers.ts +35 -0
- package/src/components/booking/change-booking-quote-guards.ts +4 -3
- package/src/components/booking/useChangeBookingReceiptDerivation.ts +12 -1
- package/test/change-booking-helpers.test.ts +76 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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={
|
|
930
|
-
replacePriceSummary={selfServeCheckoutPlaceholder}
|
|
945
|
+
priceSummaryLines={publicChangeAmountDueSummary.lines}
|
|
946
|
+
replacePriceSummary={selfServeCheckoutPlaceholder ?? publicReceiptComparison}
|
|
931
947
|
totalPrice={changeFlowAmountDue}
|
|
932
|
-
subtotal={
|
|
933
|
-
taxAmount={
|
|
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={
|
|
987
|
+
priceSummaryLinesIncludeTaxRow={false}
|
|
972
988
|
isTaxIncludedInPrice={isTaxIncludedInPrice}
|
|
973
|
-
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
|
-
|
|
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
|
|
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
|
() =>
|