@ticketboothapp/booking 1.2.143 → 1.2.145
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/AdminChangeBookingContent.tsx +1 -0
- package/src/components/booking/AdminChangeBookingFlow.tsx +1 -2
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +3 -0
- package/src/components/booking/AdminChangeReceiptComparison.tsx +6 -0
- package/src/components/booking/admin-change-quote-request-key.ts +37 -0
- package/src/components/booking/useAdminChangeQuotePreview.ts +7 -16
- package/src/lib/booking-api.ts +24 -3
- package/test/change-booking-helpers.test.ts +34 -0
package/package.json
CHANGED
|
@@ -687,8 +687,6 @@ export function AdminChangeBookingFlow({
|
|
|
687
687
|
quantities,
|
|
688
688
|
addOnSelections,
|
|
689
689
|
adminCustomReceiptLines,
|
|
690
|
-
editableSummaryLineAmountInputs,
|
|
691
|
-
editableSummaryLineLabelInputs,
|
|
692
690
|
useAdminFeAuthoritativeQuote,
|
|
693
691
|
latestChangeQuote,
|
|
694
692
|
setLatestChangeQuote,
|
|
@@ -983,6 +981,7 @@ export function AdminChangeBookingFlow({
|
|
|
983
981
|
latestChangeQuote?.pricingVersion === 'booking-amendment-v1'
|
|
984
982
|
? latestChangeQuote.serverPreview?.totalNewBooking ?? null
|
|
985
983
|
: null,
|
|
984
|
+
serverQuoteError: changeQuoteFetchError,
|
|
986
985
|
...{ priceSummaryLinesIncludeTaxRow, isTaxIncludedInPrice, showProviderPricingInlineEditor },
|
|
987
986
|
taxRate: pricingConfig?.taxRate,
|
|
988
987
|
...{ providerPricingUi, providerQuotedLines },
|
|
@@ -33,6 +33,7 @@ export interface AdminChangeCheckoutPanelProps {
|
|
|
33
33
|
serverAmendmentLines: PriceSummaryLine[] | null;
|
|
34
34
|
serverAmountDue: number | null;
|
|
35
35
|
serverUpdatedTotal: number | null;
|
|
36
|
+
serverQuoteError?: string | null;
|
|
36
37
|
isTaxIncludedInPrice: boolean;
|
|
37
38
|
taxRate?: number;
|
|
38
39
|
currency: Currency;
|
|
@@ -105,6 +106,7 @@ export function AdminChangeCheckoutPanel({
|
|
|
105
106
|
serverAmendmentLines,
|
|
106
107
|
serverAmountDue,
|
|
107
108
|
serverUpdatedTotal,
|
|
109
|
+
serverQuoteError,
|
|
108
110
|
isTaxIncludedInPrice,
|
|
109
111
|
taxRate,
|
|
110
112
|
currency,
|
|
@@ -191,6 +193,7 @@ export function AdminChangeCheckoutPanel({
|
|
|
191
193
|
amountDue={serverAmountDue}
|
|
192
194
|
updatedTotal={serverUpdatedTotal}
|
|
193
195
|
selectionChanged={hasEffectiveChangeSelection}
|
|
196
|
+
quoteError={serverQuoteError}
|
|
194
197
|
amountDueLabel={totalSummaryLabel}
|
|
195
198
|
currency={currency}
|
|
196
199
|
locale={locale}
|
|
@@ -18,6 +18,7 @@ export interface AdminChangeReceiptComparisonProps {
|
|
|
18
18
|
/** Full resulting booking total returned by the same server quote. */
|
|
19
19
|
updatedTotal: number | null;
|
|
20
20
|
selectionChanged: boolean;
|
|
21
|
+
quoteError?: string | null;
|
|
21
22
|
amountDueLabel: string;
|
|
22
23
|
currency: Currency;
|
|
23
24
|
locale: string;
|
|
@@ -48,6 +49,7 @@ export function AdminChangeReceiptComparison({
|
|
|
48
49
|
amountDue,
|
|
49
50
|
updatedTotal,
|
|
50
51
|
selectionChanged,
|
|
52
|
+
quoteError,
|
|
51
53
|
amountDueLabel,
|
|
52
54
|
currency,
|
|
53
55
|
locale,
|
|
@@ -104,6 +106,10 @@ export function AdminChangeReceiptComparison({
|
|
|
104
106
|
<p className="text-sm font-medium text-stone-700">No changes selected</p>
|
|
105
107
|
<p className="mt-1 text-xs text-stone-500">Change tickets or another booking option to request a price.</p>
|
|
106
108
|
</div>
|
|
109
|
+
) : quoteError ? (
|
|
110
|
+
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-4 text-center" role="alert">
|
|
111
|
+
<p className="text-sm font-medium text-red-800">{quoteError}</p>
|
|
112
|
+
</div>
|
|
107
113
|
) : !quoteReady ? (
|
|
108
114
|
<div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
|
|
109
115
|
<p className="text-sm font-medium text-stone-700">Waiting for server price…</p>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Availability } from '../../lib/booking-api';
|
|
2
|
+
import { getAvailabilityOptionId } from './change-booking-flow-helpers';
|
|
3
|
+
|
|
4
|
+
interface AdminChangeQuoteRequestKeyInput {
|
|
5
|
+
bookingReference?: string | null;
|
|
6
|
+
lastName: string;
|
|
7
|
+
productId: string;
|
|
8
|
+
selectedAvailability: Availability | null;
|
|
9
|
+
pickupLocationId: string | null;
|
|
10
|
+
returnAvailabilityId: string | null;
|
|
11
|
+
quantities: Record<string, number>;
|
|
12
|
+
addOnSelections: Array<{ addOnId: string; variantId?: string; quantity?: number }>;
|
|
13
|
+
adminCustomReceiptLines: Array<{ label: string; amountInput: string; amountSign?: number }>;
|
|
14
|
+
useAdminFeAuthoritativeQuote: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Identity of a price request. This must contain user-controlled quote inputs only.
|
|
19
|
+
* Server/display-derived receipt state must never invalidate the response that produced it.
|
|
20
|
+
*/
|
|
21
|
+
export function buildAdminChangeQuoteRequestKey(input: AdminChangeQuoteRequestKeyInput): string {
|
|
22
|
+
const availability = input.selectedAvailability;
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
bookingReference: input.bookingReference?.trim() ?? '',
|
|
25
|
+
lastName: input.lastName.trim().toLowerCase(),
|
|
26
|
+
parentProductId: input.productId,
|
|
27
|
+
optionId: availability ? getAvailabilityOptionId(availability) : '',
|
|
28
|
+
dateTime: availability?.dateTime ?? '',
|
|
29
|
+
availabilityId: availability?.availabilityId ?? null,
|
|
30
|
+
pickupLocationId: input.pickupLocationId,
|
|
31
|
+
returnAvailabilityId: input.returnAvailabilityId,
|
|
32
|
+
quantities: input.quantities,
|
|
33
|
+
addOnSelections: input.addOnSelections,
|
|
34
|
+
adminCustomReceiptLines: input.adminCustomReceiptLines,
|
|
35
|
+
useAdminFeAuthoritativeQuote: input.useAdminFeAuthoritativeQuote,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './change-booking-flow-helpers';
|
|
27
27
|
import type { Currency } from './CurrencySwitcher';
|
|
28
28
|
import type { AdminChangeLatestQuote } from './useAdminChangeCheckoutController';
|
|
29
|
+
import { buildAdminChangeQuoteRequestKey } from './admin-change-quote-request-key';
|
|
29
30
|
|
|
30
31
|
type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
|
|
31
32
|
type AdminCustomReceiptLine = { label: string; amountInput: string; amountSign?: number };
|
|
@@ -50,8 +51,6 @@ export interface UseAdminChangeQuotePreviewParams {
|
|
|
50
51
|
quantities: Record<string, number>;
|
|
51
52
|
addOnSelections: AddOnSelection[];
|
|
52
53
|
adminCustomReceiptLines: AdminCustomReceiptLine[];
|
|
53
|
-
editableSummaryLineAmountInputs: Record<string, string>;
|
|
54
|
-
editableSummaryLineLabelInputs: Record<string, string>;
|
|
55
54
|
useAdminFeAuthoritativeQuote: boolean;
|
|
56
55
|
latestChangeQuote: AdminChangeLatestQuote | null;
|
|
57
56
|
setLatestChangeQuote: Dispatch<SetStateAction<AdminChangeLatestQuote | null>>;
|
|
@@ -109,8 +108,6 @@ export function useAdminChangeQuotePreview({
|
|
|
109
108
|
quantities,
|
|
110
109
|
addOnSelections,
|
|
111
110
|
adminCustomReceiptLines,
|
|
112
|
-
editableSummaryLineAmountInputs,
|
|
113
|
-
editableSummaryLineLabelInputs,
|
|
114
111
|
useAdminFeAuthoritativeQuote,
|
|
115
112
|
latestChangeQuote,
|
|
116
113
|
setLatestChangeQuote,
|
|
@@ -154,20 +151,16 @@ export function useAdminChangeQuotePreview({
|
|
|
154
151
|
!!lastName.trim();
|
|
155
152
|
|
|
156
153
|
const isChangeQuoteBlocked = isCustomerSelfServeChange && latestChangeQuote?.canProceed === false;
|
|
157
|
-
const changeQuoteInputsKey = useMemo(() =>
|
|
158
|
-
bookingReference: initialValues?.bookingReference
|
|
159
|
-
lastName
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
availabilityId: selectedAvailability?.availabilityId ?? null,
|
|
164
|
-
pickupLocationId: pickupLocationId ?? null,
|
|
154
|
+
const changeQuoteInputsKey = useMemo(() => buildAdminChangeQuoteRequestKey({
|
|
155
|
+
bookingReference: initialValues?.bookingReference,
|
|
156
|
+
lastName,
|
|
157
|
+
productId: product.productId,
|
|
158
|
+
selectedAvailability,
|
|
159
|
+
pickupLocationId,
|
|
165
160
|
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
166
161
|
quantities,
|
|
167
162
|
addOnSelections,
|
|
168
163
|
adminCustomReceiptLines,
|
|
169
|
-
editableSummaryLineAmountInputs,
|
|
170
|
-
editableSummaryLineLabelInputs,
|
|
171
164
|
useAdminFeAuthoritativeQuote,
|
|
172
165
|
}), [
|
|
173
166
|
initialValues?.bookingReference,
|
|
@@ -179,8 +172,6 @@ export function useAdminChangeQuotePreview({
|
|
|
179
172
|
quantities,
|
|
180
173
|
addOnSelections,
|
|
181
174
|
adminCustomReceiptLines,
|
|
182
|
-
editableSummaryLineAmountInputs,
|
|
183
|
-
editableSummaryLineLabelInputs,
|
|
184
175
|
useAdminFeAuthoritativeQuote,
|
|
185
176
|
]);
|
|
186
177
|
const destinationRequiresReturnSelection = Boolean(selectedAvailability?.returnOptions?.length);
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -1631,9 +1631,30 @@ export async function quoteAdminChangeBookingV2(
|
|
|
1631
1631
|
throw new Error(message);
|
|
1632
1632
|
}
|
|
1633
1633
|
const response = await parseJsonSafely(res);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1634
|
+
let candidate: unknown = response;
|
|
1635
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
1636
|
+
if (
|
|
1637
|
+
candidate != null &&
|
|
1638
|
+
typeof candidate === 'object' &&
|
|
1639
|
+
'oldReceipt' in candidate &&
|
|
1640
|
+
'canApply' in candidate
|
|
1641
|
+
) {
|
|
1642
|
+
break;
|
|
1643
|
+
}
|
|
1644
|
+
candidate =
|
|
1645
|
+
candidate != null && typeof candidate === 'object' && 'data' in candidate
|
|
1646
|
+
? (candidate as { data?: unknown }).data
|
|
1647
|
+
: undefined;
|
|
1648
|
+
}
|
|
1649
|
+
if (
|
|
1650
|
+
candidate == null ||
|
|
1651
|
+
typeof candidate !== 'object' ||
|
|
1652
|
+
!('oldReceipt' in candidate) ||
|
|
1653
|
+
!('canApply' in candidate)
|
|
1654
|
+
) {
|
|
1655
|
+
throw new Error('Invalid admin Pricing V2 quote response');
|
|
1656
|
+
}
|
|
1657
|
+
const data = candidate as AdminChangeBookingQuoteV2Data;
|
|
1637
1658
|
const oldReceipt = data.oldReceipt;
|
|
1638
1659
|
const newQuote = data.newQuote ?? null;
|
|
1639
1660
|
const currency = newQuote?.currency ?? oldReceipt.currency ?? undefined;
|
|
@@ -7,6 +7,7 @@ import { evaluateChangeBookingQuoteForCheckout } from '../src/components/booking
|
|
|
7
7
|
import { buildCheckoutModalSummaryFromPricingV2Quote } from '../src/components/booking/pricing-v2-checkout-summary';
|
|
8
8
|
import type { ChangeQuoteUiSlice } from '../src/lib/booking/change-flow-pricing';
|
|
9
9
|
import type { ChangeBookingQuoteResponse } from '../src/lib/booking-api';
|
|
10
|
+
import { buildAdminChangeQuoteRequestKey } from '../src/components/booking/admin-change-quote-request-key';
|
|
10
11
|
|
|
11
12
|
function quote(overrides: Partial<ChangeBookingQuoteResponse>): ChangeBookingQuoteResponse {
|
|
12
13
|
return {
|
|
@@ -35,6 +36,39 @@ function test(name: string, fn: () => void): void {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
test('admin quote response display state cannot invalidate its request identity', () => {
|
|
40
|
+
const selectedAvailability = {
|
|
41
|
+
availabilityId: 'a_1',
|
|
42
|
+
productId: 'po_1',
|
|
43
|
+
dateTime: '2026-07-22T09:35:00-06:00',
|
|
44
|
+
vacancies: 10,
|
|
45
|
+
} as Parameters<typeof buildAdminChangeQuoteRequestKey>[0]['selectedAvailability'];
|
|
46
|
+
const userInputs = {
|
|
47
|
+
bookingReference: 'BIHAYXQS',
|
|
48
|
+
lastName: 'Tauro',
|
|
49
|
+
productId: 'p_1',
|
|
50
|
+
selectedAvailability,
|
|
51
|
+
pickupLocationId: 'pickup_1',
|
|
52
|
+
returnAvailabilityId: 'return_1',
|
|
53
|
+
quantities: { ADULT: 3 },
|
|
54
|
+
addOnSelections: [],
|
|
55
|
+
adminCustomReceiptLines: [],
|
|
56
|
+
useAdminFeAuthoritativeQuote: true,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const keyBeforeResponse = buildAdminChangeQuoteRequestKey(userInputs);
|
|
60
|
+
// These are populated from the successful server quote. They are deliberately
|
|
61
|
+
// not accepted by the request-key builder and therefore cannot trigger a re-quote.
|
|
62
|
+
const serverDisplayState = {
|
|
63
|
+
editableSummaryLineAmountInputs: { adult: '173.24', tax: '16.09' },
|
|
64
|
+
editableSummaryLineLabelInputs: { adult: 'ADULT' },
|
|
65
|
+
};
|
|
66
|
+
assert.ok(serverDisplayState.editableSummaryLineAmountInputs.adult);
|
|
67
|
+
const keyAfterResponse = buildAdminChangeQuoteRequestKey(userInputs);
|
|
68
|
+
|
|
69
|
+
assert.equal(keyAfterResponse, keyBeforeResponse);
|
|
70
|
+
});
|
|
71
|
+
|
|
38
72
|
test('routes customer change quote to paid checkout when amount is due', () => {
|
|
39
73
|
const decision = evaluateChangeBookingQuoteForCheckout({
|
|
40
74
|
quote: quote({
|