@ticketboothapp/booking 1.2.144 → 1.2.146

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.144",
3
+ "version": "1.2.146",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -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,
@@ -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);
@@ -432,14 +423,15 @@ export function useAdminChangeQuotePreview({
432
423
  })
433
424
  : await quoteChangeBooking(quoteRequestBase);
434
425
  if (seq !== changeQuoteRequestSeq.current) return;
435
- setLatestChangeQuote(
436
- sliceAndMergeChangeQuoteForUi(quote, {
426
+ const mergedQuote = sliceAndMergeChangeQuoteForUi(quote, {
437
427
  total: changeFlowNewBookingTotal,
438
428
  subtotal: effectiveSubtotalForCheckout,
439
429
  tax: effectiveTax,
440
- }, currency).mergedQuote,
441
- );
430
+ }, currency).mergedQuote;
431
+ // Commit the completed identity before publishing its result. This keeps a
432
+ // render caused by setLatestChangeQuote from being interpreted as a new request.
442
433
  lastCompletedQuoteInputsKeyRef.current = changeQuoteInputsKey;
434
+ setLatestChangeQuote(mergedQuote);
443
435
  } catch (e) {
444
436
  if (seq !== changeQuoteRequestSeq.current) return;
445
437
  setLatestChangeQuote(null);
@@ -460,34 +452,12 @@ export function useAdminChangeQuotePreview({
460
452
  window.clearTimeout(timer);
461
453
  };
462
454
  }, [
455
+ // Fetching is controlled by the canonical user-input identity. Do not add quote-derived
456
+ // totals, receipt objects, or display lines here: a response must not trigger another quote.
463
457
  isCustomerSelfServeChange,
464
458
  hasChangeSelection,
465
- selectedAvailability,
466
- initialValues,
467
- lastName,
468
459
  missingRequiredReturnSelection,
469
- pickupLocationId,
470
- selectedReturnOption?.returnAvailabilityId,
471
- quantities,
472
- addOnSelections,
473
- changeFlowInitialTicketCount,
474
- changeFlowNewBookingTotal,
475
- changeFlowClientEstimateDue,
476
- effectiveSubtotalForCheckout,
477
- effectiveTax,
478
- currency,
479
- adminCustomLinesAsAdditionalAdjustments,
480
- adminFeAuthoritativeReceipt,
481
- useAdminFeAuthoritativeQuote,
482
460
  changeQuoteInputsKey,
483
- originalReceipt,
484
- product.productId,
485
- setChangeQuoteLoading,
486
- setChangeQuoteFetchError,
487
- setLatestChangeQuote,
488
- lastCompletedQuoteInputsKeyRef,
489
- inFlightQuoteInputsKeyRef,
490
- changeQuoteRequestSeq,
491
461
  ]);
492
462
 
493
463
  return {
@@ -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({