@ticketboothapp/booking 1.2.164 → 1.2.165

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.164",
3
+ "version": "1.2.165",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -43,3 +43,16 @@ export function buildAdminChangeQuoteRequestKey(input: AdminChangeQuoteRequestKe
43
43
  useAdminFeAuthoritativeQuote: input.useAdminFeAuthoritativeQuote,
44
44
  });
45
45
  }
46
+
47
+ /**
48
+ * A completed request is reusable only while its authoritative quote is still published.
49
+ * Closing a nested payment dialog can clear transient quote state without changing the
50
+ * selection inputs; in that case the same input key must be eligible for a refetch.
51
+ */
52
+ export function canReuseCompletedAdminChangeQuote(
53
+ inputKey: string,
54
+ lastCompletedInputKey: string | null,
55
+ hasPublishedQuote: boolean,
56
+ ): boolean {
57
+ return Boolean(inputKey) && lastCompletedInputKey === inputKey && hasPublishedQuote;
58
+ }
@@ -290,7 +290,8 @@ export function useAdminChangeCheckoutController({
290
290
  setShowCheckoutModal(false);
291
291
  setCheckoutModalData(null);
292
292
  setCheckoutClientSecret('');
293
- }, []);
293
+ setError('');
294
+ }, [setError]);
294
295
 
295
296
  const cancelPendingReservationBestEffort = useCallback(() => {
296
297
  if (paymentSubmitInFlightRef.current) return;
@@ -27,7 +27,10 @@ import {
27
27
  } from './change-booking-flow-helpers';
28
28
  import type { Currency } from './CurrencySwitcher';
29
29
  import type { AdminChangeLatestQuote } from './useAdminChangeCheckoutController';
30
- import { buildAdminChangeQuoteRequestKey } from './admin-change-quote-request-key';
30
+ import {
31
+ buildAdminChangeQuoteRequestKey,
32
+ canReuseCompletedAdminChangeQuote,
33
+ } from './admin-change-quote-request-key';
31
34
  import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
32
35
 
33
36
  type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
@@ -167,6 +170,7 @@ export function useAdminChangeQuotePreview({
167
170
  latestChangeQuote?.canProceed === false &&
168
171
  (latestChangeQuote.refundDecisionOperations?.length ?? 0) === 0;
169
172
  const refundDecisionRequired = (latestChangeQuote?.refundDecisionOperations?.length ?? 0) > 0;
173
+ const hasPublishedChangeQuote = latestChangeQuote !== null;
170
174
  const changeQuoteInputsKey = useMemo(() => buildAdminChangeQuoteRequestKey({
171
175
  bookingReference: initialValues?.bookingReference,
172
176
  lastName,
@@ -208,6 +212,14 @@ export function useAdminChangeQuotePreview({
208
212
  const changeCheckoutButtonLabel = (() => {
209
213
  if (!hasEffectiveChangeSelection) return undefined;
210
214
  if (isProviderDashboardChange) {
215
+ if (changeQuoteLoading || (!latestChangeQuote && !changeQuoteFetchError)) {
216
+ const tr = t('booking.updatingPrice');
217
+ return tr !== 'booking.updatingPrice' ? tr : 'Updating price\u2026';
218
+ }
219
+ if (changeQuoteFetchError) {
220
+ const tr = t('booking.changeBookingRetry');
221
+ return tr !== 'booking.changeBookingRetry' ? tr : 'Change booking (retry)';
222
+ }
211
223
  if (isChangeQuoteBlocked) {
212
224
  const tr = t('booking.changeBookingNotAvailable');
213
225
  return tr !== 'booking.changeBookingNotAvailable' ? tr : 'Change not available';
@@ -215,8 +227,8 @@ export function useAdminChangeQuotePreview({
215
227
  // Prefer authoritative V2 signed due (charge positive / refund negative). Do not use the
216
228
  // FE cart estimate once a server quote exists — receipt total deltas can disagree with
217
229
  // floored/settlement amounts (DEFECT-PV2-018 follow-up).
218
- const serverDue = latestChangeQuote?.serverPreview?.amountDue;
219
- const due = Math.round((serverDue ?? changeFlowClientEstimateDue) * 100) / 100;
230
+ const serverDue = latestChangeQuote?.serverPreview?.amountDue ?? latestChangeQuote?.priceDiff ?? 0;
231
+ const due = Math.round(serverDue * 100) / 100;
220
232
  return due > 0
221
233
  ? `Change booking (${formatCurrencyAmount(due, currency, locale as 'en' | 'fr')})`
222
234
  : due < 0
@@ -398,11 +410,15 @@ export function useAdminChangeQuotePreview({
398
410
  return;
399
411
  }
400
412
 
401
- if (
402
- changeQuoteInputsKey &&
403
- (inFlightQuoteInputsKeyRef.current === changeQuoteInputsKey ||
404
- lastCompletedQuoteInputsKeyRef.current === changeQuoteInputsKey)
405
- ) {
413
+ if (changeQuoteInputsKey && inFlightQuoteInputsKeyRef.current === changeQuoteInputsKey) {
414
+ return;
415
+ }
416
+
417
+ if (canReuseCompletedAdminChangeQuote(
418
+ changeQuoteInputsKey,
419
+ lastCompletedQuoteInputsKeyRef.current,
420
+ hasPublishedChangeQuote,
421
+ )) {
406
422
  setChangeQuoteLoading(false);
407
423
  return;
408
424
  }
@@ -493,10 +509,13 @@ export function useAdminChangeQuotePreview({
493
509
  }, [
494
510
  // Fetching is controlled by the canonical user-input identity. Do not add quote-derived
495
511
  // totals, receipt objects, or display lines here: a response must not trigger another quote.
512
+ // Quote presence is intentionally included so a cleared transient quote can be restored by
513
+ // refetching the same canonical input identity after a nested checkout is closed.
496
514
  isCustomerSelfServeChange,
497
515
  hasChangeSelection,
498
516
  missingRequiredReturnSelection,
499
517
  changeQuoteInputsKey,
518
+ hasPublishedChangeQuote,
500
519
  ]);
501
520
 
502
521
  return {
@@ -13,7 +13,10 @@ import {
13
13
  mapAdminChangeBookingQuoteV2Data,
14
14
  type ChangeBookingQuoteResponse,
15
15
  } from '../src/lib/booking-api';
16
- import { buildAdminChangeQuoteRequestKey } from '../src/components/booking/admin-change-quote-request-key';
16
+ import {
17
+ buildAdminChangeQuoteRequestKey,
18
+ canReuseCompletedAdminChangeQuote,
19
+ } from '../src/components/booking/admin-change-quote-request-key';
17
20
  import {
18
21
  noRefundRemovedValue,
19
22
  normalizeReturnPriceTreatment,
@@ -195,6 +198,13 @@ test('admin quote response display state cannot invalidate its request identity'
195
198
  assert.equal(keyAfterResponse, keyBeforeResponse);
196
199
  });
197
200
 
201
+ test('admin quote dedupe refetches unchanged inputs after the published quote is cleared', () => {
202
+ const inputKey = 'same-admin-change-inputs';
203
+ assert.equal(canReuseCompletedAdminChangeQuote(inputKey, inputKey, true), true);
204
+ assert.equal(canReuseCompletedAdminChangeQuote(inputKey, inputKey, false), false);
205
+ assert.equal(canReuseCompletedAdminChangeQuote(inputKey, 'different-inputs', true), false);
206
+ });
207
+
198
208
  test('admin no-charge provider payload carries the selected pickup itinerary', () => {
199
209
  const itineraryDisplay: NonNullable<
200
210
  Parameters<typeof buildAdminChangeProviderPayload>[0]['itineraryDisplay']