@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.143",
3
+ "version": "1.2.145",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -158,6 +158,7 @@ const checkoutPanelPropKeys = [
158
158
  'serverAmendmentLines',
159
159
  'serverAmountDue',
160
160
  'serverUpdatedTotal',
161
+ 'serverQuoteError',
161
162
  'isTaxIncludedInPrice',
162
163
  'taxRate',
163
164
  'currency',
@@ -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(() => JSON.stringify({
158
- bookingReference: initialValues?.bookingReference?.trim() ?? '',
159
- lastName: lastName.trim().toLowerCase(),
160
- parentProductId: product.productId,
161
- optionId: selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
162
- dateTime: selectedAvailability?.dateTime ?? '',
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);
@@ -1631,9 +1631,30 @@ export async function quoteAdminChangeBookingV2(
1631
1631
  throw new Error(message);
1632
1632
  }
1633
1633
  const response = await parseJsonSafely(res);
1634
- const outer = (response as { data?: unknown } | null)?.data;
1635
- const data = ((outer as { data?: AdminChangeBookingQuoteV2Data } | null)?.data ??
1636
- outer) as AdminChangeBookingQuoteV2Data;
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({