@ticketboothapp/booking 1.2.163 → 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.163",
3
+ "version": "1.2.165",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -16,10 +16,13 @@ import type { ChangeBookingPaidCheckoutModalData } from './change-booking-paymen
16
16
 
17
17
  export type AdminChangeCheckoutModalData = ChangeBookingPaidCheckoutModalData & {
18
18
  finalizeExistingChangeInline?: boolean;
19
+ pricingQuoteId?: string;
19
20
  };
20
21
 
21
22
  export interface AdminChangePaymentChoiceData {
22
23
  reservationReference: string;
24
+ /** Immutable quote snapshot used by both payment preparation and final apply. */
25
+ pricingQuoteId?: string;
23
26
  reservationExpiration?: string;
24
27
  checkoutBreakdown: CheckoutBreakdown;
25
28
  totalAmount: number;
@@ -51,6 +54,7 @@ export interface AdminChangePaymentChoiceData {
51
54
 
52
55
  export interface BuildAdminChangePaymentChoiceDataParams {
53
56
  reservationReference: string;
57
+ pricingQuoteId?: string;
54
58
  checkoutBreakdown: CheckoutBreakdown;
55
59
  totalAmount: number;
56
60
  datePart: string;
@@ -94,6 +98,7 @@ export function buildAdminChangePayNowCheckoutModalData(
94
98
  ): AdminChangeCheckoutModalData {
95
99
  return {
96
100
  reservationReference: adminChoiceData.reservationReference,
101
+ pricingQuoteId: adminChoiceData.pricingQuoteId,
97
102
  reservationExpiration: adminChoiceData.reservationExpiration,
98
103
  customerLastName: lastName.trim(),
99
104
  ticketLines: adminChoiceData.ticketLinesForModal,
@@ -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;
@@ -563,6 +564,7 @@ export function useAdminChangeCheckoutController({
563
564
  }
564
565
  setAdminChoiceData(buildAdminChangePaymentChoiceData({
565
566
  reservationReference: bookingReference,
567
+ pricingQuoteId: quoteId,
566
568
  checkoutBreakdown,
567
569
  totalAmount: amountDueForCheckout,
568
570
  datePart,
@@ -589,7 +591,7 @@ export function useAdminChangeCheckoutController({
589
591
  'Pay later to apply the change and add the amount to the booking balance, or collect a new card payment before applying it.',
590
592
  confirmWithoutPaymentLabel: 'Pay later',
591
593
  previousTotal: originalReceipt?.total,
592
- newTotal: latestChangeQuote?.quotedTotal ?? latestChangeQuote?.serverDisplay?.total,
594
+ newTotal: latestChangeQuote?.serverDisplay?.total ?? latestChangeQuote?.quotedTotal,
593
595
  finalizeExistingChangeInline: true,
594
596
  preferConfirmWithoutPayment: true,
595
597
  }));
@@ -859,8 +861,8 @@ export function useAdminChangeCheckoutController({
859
861
  return;
860
862
  }
861
863
  if (isProviderDashboardChange && isChangeBookingContext) {
862
- const bookingReference = initialValues?.bookingReference?.trim();
863
- const quoteId = latestChangeQuote?.quoteId?.trim();
864
+ const bookingReference = adminChoiceData.reservationReference.trim();
865
+ const quoteId = adminChoiceData.pricingQuoteId?.trim();
864
866
  if (!bookingReference || !quoteId) {
865
867
  setError('The authoritative change quote is no longer available.');
866
868
  return;
@@ -893,8 +895,8 @@ export function useAdminChangeCheckoutController({
893
895
 
894
896
  const handlePaymentConfirmed = useCallback(async (paymentIntentId: string) => {
895
897
  if (!isProviderDashboardChange || !isChangeBookingContext) return;
896
- const bookingReference = initialValues?.bookingReference?.trim();
897
- const quoteId = latestChangeQuote?.quoteId?.trim();
898
+ const bookingReference = checkoutModalData?.reservationReference?.trim();
899
+ const quoteId = checkoutModalData?.pricingQuoteId?.trim();
898
900
  if (!bookingReference || !quoteId) {
899
901
  throw new Error('Payment was authorized, but the booking quote is no longer available.');
900
902
  }
@@ -910,10 +912,10 @@ export function useAdminChangeCheckoutController({
910
912
  onSuccess?.({ reservationReference: bookingReference });
911
913
  finishManageFlow(bookingReference);
912
914
  }, [
913
- initialValues?.bookingReference,
915
+ checkoutModalData?.pricingQuoteId,
916
+ checkoutModalData?.reservationReference,
914
917
  isChangeBookingContext,
915
918
  isProviderDashboardChange,
916
- latestChangeQuote?.quoteId,
917
919
  onSuccess,
918
920
  ]);
919
921
 
@@ -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
@@ -284,8 +296,11 @@ export function useAdminChangeQuotePreview({
284
296
  hasChangesFromInitial: hasEffectiveChangeSelection,
285
297
  selectionTotal:
286
298
  originalReceipt
287
- ? selfServePricingConfirmed && latestChangeQuote?.serverPreview?.totalNewBooking != null
288
- ? latestChangeQuote.serverPreview.totalNewBooking
299
+ ? selfServePricingConfirmed && latestChangeQuote != null
300
+ ? latestChangeQuote.quotedTotal ??
301
+ latestChangeQuote.serverDisplay?.total ??
302
+ latestChangeQuote.serverPreview?.totalNewBooking ??
303
+ null
289
304
  : suppressSelfServeCurrencyUi
290
305
  ? null
291
306
  : displayChangeFlowProposedTotalWithEditableLines
@@ -306,6 +321,8 @@ export function useAdminChangeQuotePreview({
306
321
  displayChangeFlowProposedTotalWithEditableLines,
307
322
  suppressSelfServeCurrencyUi,
308
323
  selfServePricingConfirmed,
324
+ latestChangeQuote?.quotedTotal,
325
+ latestChangeQuote?.serverDisplay?.total,
309
326
  latestChangeQuote?.serverPreview?.totalNewBooking,
310
327
  ]);
311
328
 
@@ -393,11 +410,15 @@ export function useAdminChangeQuotePreview({
393
410
  return;
394
411
  }
395
412
 
396
- if (
397
- changeQuoteInputsKey &&
398
- (inFlightQuoteInputsKeyRef.current === changeQuoteInputsKey ||
399
- lastCompletedQuoteInputsKeyRef.current === changeQuoteInputsKey)
400
- ) {
413
+ if (changeQuoteInputsKey && inFlightQuoteInputsKeyRef.current === changeQuoteInputsKey) {
414
+ return;
415
+ }
416
+
417
+ if (canReuseCompletedAdminChangeQuote(
418
+ changeQuoteInputsKey,
419
+ lastCompletedQuoteInputsKeyRef.current,
420
+ hasPublishedChangeQuote,
421
+ )) {
401
422
  setChangeQuoteLoading(false);
402
423
  return;
403
424
  }
@@ -488,10 +509,13 @@ export function useAdminChangeQuotePreview({
488
509
  }, [
489
510
  // Fetching is controlled by the canonical user-input identity. Do not add quote-derived
490
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.
491
514
  isCustomerSelfServeChange,
492
515
  hasChangeSelection,
493
516
  missingRequiredReturnSelection,
494
517
  changeQuoteInputsKey,
518
+ hasPublishedChangeQuote,
495
519
  ]);
496
520
 
497
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,
@@ -80,6 +83,7 @@ test('admin payment choice countdown expires exactly at the reservation deadline
80
83
  test('admin existing-booking pay-now review keeps previous, updated, and exact difference totals', () => {
81
84
  const choice = buildAdminChangePaymentChoiceData({
82
85
  reservationReference: 'KBRCIGCO',
86
+ pricingQuoteId: 'quote_exact_admin_change',
83
87
  checkoutBreakdown: { lineItems: [] },
84
88
  totalAmount: 256.58,
85
89
  datePart: '2026-07-31',
@@ -112,6 +116,7 @@ test('admin existing-booking pay-now review keeps previous, updated, and exact d
112
116
  differenceTotal: 256.58,
113
117
  });
114
118
  assert.equal(checkout.total, 256.58);
119
+ assert.equal(checkout.pricingQuoteId, 'quote_exact_admin_change');
115
120
  assert.equal(checkout.finalizeExistingChangeInline, true);
116
121
  assert.equal(choice.confirmWithoutPaymentLabel, 'Pay later');
117
122
  assert.equal(choice.preferConfirmWithoutPayment, true);
@@ -193,6 +198,13 @@ test('admin quote response display state cannot invalidate its request identity'
193
198
  assert.equal(keyAfterResponse, keyBeforeResponse);
194
199
  });
195
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
+
196
208
  test('admin no-charge provider payload carries the selected pickup itinerary', () => {
197
209
  const itineraryDisplay: NonNullable<
198
210
  Parameters<typeof buildAdminChangeProviderPayload>[0]['itineraryDisplay']