@ticketboothapp/booking 1.2.165 → 1.2.166
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/AdminChangeBookingFlow.tsx +2 -2
- package/src/components/booking/admin-change-payment-choice-runner.ts +12 -0
- package/src/components/booking/admin-change-quote-request-key.ts +0 -13
- package/src/components/booking/useAdminChangeCheckoutController.ts +50 -10
- package/src/components/booking/useAdminChangeQuotePreview.ts +8 -27
- package/test/change-booking-helpers.test.ts +12 -11
package/package.json
CHANGED
|
@@ -924,7 +924,7 @@ export function AdminChangeBookingFlow({
|
|
|
924
924
|
checkoutModalData,
|
|
925
925
|
showAdminPaymentChoice,
|
|
926
926
|
adminChoiceData,
|
|
927
|
-
|
|
927
|
+
handleCheckoutClose,
|
|
928
928
|
handleCheckout,
|
|
929
929
|
handleConfirmWithoutPayment,
|
|
930
930
|
handlePayNow,
|
|
@@ -1022,7 +1022,7 @@ export function AdminChangeBookingFlow({
|
|
|
1022
1022
|
...{ pickupLocationId, pickupLocationSkipped, firstName, lastName, email, getSuccessUrl, t },
|
|
1023
1023
|
onPayNow: handlePayNow,
|
|
1024
1024
|
onConfirmWithoutPayment: handleConfirmWithoutPayment, onAdminPaymentChoiceCancel: handleAdminPaymentChoiceCancel,
|
|
1025
|
-
onCheckoutClose:
|
|
1025
|
+
onCheckoutClose: handleCheckoutClose, onPaymentSubmitStart: handlePaymentSubmitStart,
|
|
1026
1026
|
onPaymentSubmitError: handlePaymentSubmitError, onPaymentConfirmed: handlePaymentConfirmed,
|
|
1027
1027
|
isPartialLaunch,
|
|
1028
1028
|
checkoutVisible: selectedAvailability != null,
|
|
@@ -126,6 +126,18 @@ export function buildAdminChangePayNowCheckoutModalData(
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
export function shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose(
|
|
130
|
+
isProviderDashboardChange: boolean,
|
|
131
|
+
isChangeBookingContext: boolean,
|
|
132
|
+
adminChoiceData: Pick<AdminChangePaymentChoiceData, 'pricingQuoteId'> | null,
|
|
133
|
+
): boolean {
|
|
134
|
+
return Boolean(
|
|
135
|
+
isProviderDashboardChange &&
|
|
136
|
+
isChangeBookingContext &&
|
|
137
|
+
adminChoiceData?.pricingQuoteId?.trim(),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
export interface ConfirmAdminChangeBookingWithoutPaymentParams {
|
|
130
142
|
adminChoiceData: AdminChangePaymentChoiceData;
|
|
131
143
|
bookingSourceAttribution: Partial<BookingSourceMetadata>;
|
|
@@ -43,16 +43,3 @@ 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
|
-
}
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
buildAdminChangePaymentChoiceData,
|
|
60
60
|
buildAdminChangePayNowCheckoutModalData,
|
|
61
61
|
confirmAdminChangeBookingWithoutPayment,
|
|
62
|
+
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose,
|
|
62
63
|
type AdminChangeCheckoutModalData,
|
|
63
64
|
type AdminChangePaymentChoiceData,
|
|
64
65
|
} from './admin-change-payment-choice-runner';
|
|
@@ -290,8 +291,28 @@ export function useAdminChangeCheckoutController({
|
|
|
290
291
|
setShowCheckoutModal(false);
|
|
291
292
|
setCheckoutModalData(null);
|
|
292
293
|
setCheckoutClientSecret('');
|
|
294
|
+
}, []);
|
|
295
|
+
|
|
296
|
+
const handleCheckoutClose = useCallback(() => {
|
|
297
|
+
if (paymentSubmitInFlightRef.current) return;
|
|
298
|
+
cancelPendingReservation();
|
|
293
299
|
setError('');
|
|
294
|
-
|
|
300
|
+
// An existing-booking Pay now attempt owns an immutable quote/payment-choice snapshot.
|
|
301
|
+
// Closing Stripe must return to that choice instead of making the user create a new quote.
|
|
302
|
+
if (shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose(
|
|
303
|
+
isProviderDashboardChange,
|
|
304
|
+
isChangeBookingContext,
|
|
305
|
+
adminChoiceData,
|
|
306
|
+
)) {
|
|
307
|
+
setShowAdminPaymentChoice(true);
|
|
308
|
+
}
|
|
309
|
+
}, [
|
|
310
|
+
adminChoiceData?.pricingQuoteId,
|
|
311
|
+
cancelPendingReservation,
|
|
312
|
+
isChangeBookingContext,
|
|
313
|
+
isProviderDashboardChange,
|
|
314
|
+
setError,
|
|
315
|
+
]);
|
|
295
316
|
|
|
296
317
|
const cancelPendingReservationBestEffort = useCallback(() => {
|
|
297
318
|
if (paymentSubmitInFlightRef.current) return;
|
|
@@ -324,6 +345,7 @@ export function useAdminChangeCheckoutController({
|
|
|
324
345
|
const buildProviderChangePayload = (
|
|
325
346
|
availabilityProductOptionId: string,
|
|
326
347
|
bookingItems: AdminChangeBookingItem[],
|
|
348
|
+
authoritativeQuote: AdminChangeLatestQuote | null = latestChangeQuote,
|
|
327
349
|
): ProviderDashboardChangeBookingPayload | null => {
|
|
328
350
|
return buildAdminChangeProviderPayload({
|
|
329
351
|
selectedAvailability,
|
|
@@ -341,8 +363,8 @@ export function useAdminChangeCheckoutController({
|
|
|
341
363
|
mergedProviderAdditionalAdjustments,
|
|
342
364
|
adminStructuredAdjustments,
|
|
343
365
|
refundDispositionsByOperationId,
|
|
344
|
-
pricingV2QuoteId:
|
|
345
|
-
pricingV2QuotedTotal:
|
|
366
|
+
pricingV2QuoteId: authoritativeQuote?.quoteId ?? null,
|
|
367
|
+
pricingV2QuotedTotal: authoritativeQuote?.quotedTotal ?? authoritativeQuote?.serverDisplay?.total ?? null,
|
|
346
368
|
providerApplyAuthoritativeReceipt,
|
|
347
369
|
previousPassengerCount: changeFlowInitialTicketCount,
|
|
348
370
|
previousAvailabilityId: initialValues?.availabilityId ?? null,
|
|
@@ -437,6 +459,7 @@ export function useAdminChangeCheckoutController({
|
|
|
437
459
|
let changeIntentIdForCheckout: string | undefined;
|
|
438
460
|
let changeBookingReferenceForPaidFlow: string | undefined;
|
|
439
461
|
let confirmedChangeAmountDueForCheckout: number | null = null;
|
|
462
|
+
let authoritativeChangeQuoteForCheckout = latestChangeQuote;
|
|
440
463
|
|
|
441
464
|
if (isCustomerSelfServeChange) {
|
|
442
465
|
const quoteResult = await quoteAdminCustomerChangeForCheckout({
|
|
@@ -465,11 +488,18 @@ export function useAdminChangeCheckoutController({
|
|
|
465
488
|
});
|
|
466
489
|
changeBookingReferenceForPaidFlow = quoteResult.bookingReference;
|
|
467
490
|
setLatestChangeQuote(quoteResult.mergedQuote);
|
|
491
|
+
// React state is not updated synchronously. Everything in this checkout attempt must
|
|
492
|
+
// use the quote returned above, not the stale latestChangeQuote render closure.
|
|
493
|
+
authoritativeChangeQuoteForCheckout = quoteResult.mergedQuote;
|
|
468
494
|
confirmedChangeAmountDueForCheckout = quoteResult.decision.amountDueForCheckout;
|
|
469
495
|
|
|
470
496
|
if (quoteResult.decision.kind === 'free') {
|
|
471
497
|
if (onChangeBooking) {
|
|
472
|
-
const providerPayload = buildProviderChangePayload(
|
|
498
|
+
const providerPayload = buildProviderChangePayload(
|
|
499
|
+
availabilityProductOptionId,
|
|
500
|
+
bookingItems,
|
|
501
|
+
authoritativeChangeQuoteForCheckout,
|
|
502
|
+
);
|
|
473
503
|
if (!providerPayload) {
|
|
474
504
|
setError('No availability selected');
|
|
475
505
|
setLoading(false);
|
|
@@ -502,7 +532,7 @@ export function useAdminChangeCheckoutController({
|
|
|
502
532
|
const itineraryDisplay = computeItineraryDisplayForStorage() ?? computeItineraryDisplay();
|
|
503
533
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
504
534
|
const amountDueForCheckout = isProviderDashboardChange && isChangeBookingContext
|
|
505
|
-
? Math.max(0,
|
|
535
|
+
? Math.max(0, authoritativeChangeQuoteForCheckout?.priceDiff ?? 0)
|
|
506
536
|
: isCustomerSelfServeChange
|
|
507
537
|
? confirmedChangeAmountDueForCheckout ??
|
|
508
538
|
Math.max(
|
|
@@ -547,14 +577,21 @@ export function useAdminChangeCheckoutController({
|
|
|
547
577
|
// quote-bound Stripe authorization and applies only after that authorization succeeds.
|
|
548
578
|
if (isProviderDashboardChange && isChangeBookingContext) {
|
|
549
579
|
const bookingReference = initialValues?.bookingReference?.trim();
|
|
550
|
-
const quoteId =
|
|
580
|
+
const quoteId = authoritativeChangeQuoteForCheckout?.quoteId?.trim();
|
|
551
581
|
if (!bookingReference) throw new Error('Missing booking reference.');
|
|
552
|
-
if (!quoteId ||
|
|
553
|
-
throw new Error(
|
|
582
|
+
if (!quoteId || authoritativeChangeQuoteForCheckout?.canProceed === false) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
authoritativeChangeQuoteForCheckout?.reasonIfBlocked ||
|
|
585
|
+
'The authoritative change quote is not ready.',
|
|
586
|
+
);
|
|
554
587
|
}
|
|
555
588
|
if (amountDueForCheckout <= 0) {
|
|
556
589
|
if (!onChangeBooking) throw new Error('Provider change handler is unavailable.');
|
|
557
|
-
const providerPayload = buildProviderChangePayload(
|
|
590
|
+
const providerPayload = buildProviderChangePayload(
|
|
591
|
+
availabilityProductOptionId,
|
|
592
|
+
bookingItems,
|
|
593
|
+
authoritativeChangeQuoteForCheckout,
|
|
594
|
+
);
|
|
558
595
|
if (!providerPayload) throw new Error('No availability selected');
|
|
559
596
|
await onChangeBooking(providerPayload);
|
|
560
597
|
onSuccess?.({ reservationReference: bookingReference });
|
|
@@ -591,7 +628,9 @@ export function useAdminChangeCheckoutController({
|
|
|
591
628
|
'Pay later to apply the change and add the amount to the booking balance, or collect a new card payment before applying it.',
|
|
592
629
|
confirmWithoutPaymentLabel: 'Pay later',
|
|
593
630
|
previousTotal: originalReceipt?.total,
|
|
594
|
-
newTotal:
|
|
631
|
+
newTotal:
|
|
632
|
+
authoritativeChangeQuoteForCheckout?.serverDisplay?.total ??
|
|
633
|
+
authoritativeChangeQuoteForCheckout?.quotedTotal,
|
|
595
634
|
finalizeExistingChangeInline: true,
|
|
596
635
|
preferConfirmWithoutPayment: true,
|
|
597
636
|
}));
|
|
@@ -941,6 +980,7 @@ export function useAdminChangeCheckoutController({
|
|
|
941
980
|
showAdminPaymentChoice,
|
|
942
981
|
adminChoiceData,
|
|
943
982
|
cancelPendingReservation,
|
|
983
|
+
handleCheckoutClose,
|
|
944
984
|
handleCheckout,
|
|
945
985
|
handleConfirmWithoutPayment,
|
|
946
986
|
handlePayNow,
|
|
@@ -27,10 +27,7 @@ import {
|
|
|
27
27
|
} from './change-booking-flow-helpers';
|
|
28
28
|
import type { Currency } from './CurrencySwitcher';
|
|
29
29
|
import type { AdminChangeLatestQuote } from './useAdminChangeCheckoutController';
|
|
30
|
-
import {
|
|
31
|
-
buildAdminChangeQuoteRequestKey,
|
|
32
|
-
canReuseCompletedAdminChangeQuote,
|
|
33
|
-
} from './admin-change-quote-request-key';
|
|
30
|
+
import { buildAdminChangeQuoteRequestKey } from './admin-change-quote-request-key';
|
|
34
31
|
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
35
32
|
|
|
36
33
|
type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
|
|
@@ -170,7 +167,6 @@ export function useAdminChangeQuotePreview({
|
|
|
170
167
|
latestChangeQuote?.canProceed === false &&
|
|
171
168
|
(latestChangeQuote.refundDecisionOperations?.length ?? 0) === 0;
|
|
172
169
|
const refundDecisionRequired = (latestChangeQuote?.refundDecisionOperations?.length ?? 0) > 0;
|
|
173
|
-
const hasPublishedChangeQuote = latestChangeQuote !== null;
|
|
174
170
|
const changeQuoteInputsKey = useMemo(() => buildAdminChangeQuoteRequestKey({
|
|
175
171
|
bookingReference: initialValues?.bookingReference,
|
|
176
172
|
lastName,
|
|
@@ -212,14 +208,6 @@ export function useAdminChangeQuotePreview({
|
|
|
212
208
|
const changeCheckoutButtonLabel = (() => {
|
|
213
209
|
if (!hasEffectiveChangeSelection) return undefined;
|
|
214
210
|
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
|
-
}
|
|
223
211
|
if (isChangeQuoteBlocked) {
|
|
224
212
|
const tr = t('booking.changeBookingNotAvailable');
|
|
225
213
|
return tr !== 'booking.changeBookingNotAvailable' ? tr : 'Change not available';
|
|
@@ -227,8 +215,8 @@ export function useAdminChangeQuotePreview({
|
|
|
227
215
|
// Prefer authoritative V2 signed due (charge positive / refund negative). Do not use the
|
|
228
216
|
// FE cart estimate once a server quote exists — receipt total deltas can disagree with
|
|
229
217
|
// floored/settlement amounts (DEFECT-PV2-018 follow-up).
|
|
230
|
-
const serverDue = latestChangeQuote?.serverPreview?.amountDue
|
|
231
|
-
const due = Math.round(serverDue * 100) / 100;
|
|
218
|
+
const serverDue = latestChangeQuote?.serverPreview?.amountDue;
|
|
219
|
+
const due = Math.round((serverDue ?? changeFlowClientEstimateDue) * 100) / 100;
|
|
232
220
|
return due > 0
|
|
233
221
|
? `Change booking (${formatCurrencyAmount(due, currency, locale as 'en' | 'fr')})`
|
|
234
222
|
: due < 0
|
|
@@ -410,15 +398,11 @@ export function useAdminChangeQuotePreview({
|
|
|
410
398
|
return;
|
|
411
399
|
}
|
|
412
400
|
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
changeQuoteInputsKey,
|
|
419
|
-
lastCompletedQuoteInputsKeyRef.current,
|
|
420
|
-
hasPublishedChangeQuote,
|
|
421
|
-
)) {
|
|
401
|
+
if (
|
|
402
|
+
changeQuoteInputsKey &&
|
|
403
|
+
(inFlightQuoteInputsKeyRef.current === changeQuoteInputsKey ||
|
|
404
|
+
lastCompletedQuoteInputsKeyRef.current === changeQuoteInputsKey)
|
|
405
|
+
) {
|
|
422
406
|
setChangeQuoteLoading(false);
|
|
423
407
|
return;
|
|
424
408
|
}
|
|
@@ -509,13 +493,10 @@ export function useAdminChangeQuotePreview({
|
|
|
509
493
|
}, [
|
|
510
494
|
// Fetching is controlled by the canonical user-input identity. Do not add quote-derived
|
|
511
495
|
// 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.
|
|
514
496
|
isCustomerSelfServeChange,
|
|
515
497
|
hasChangeSelection,
|
|
516
498
|
missingRequiredReturnSelection,
|
|
517
499
|
changeQuoteInputsKey,
|
|
518
|
-
hasPublishedChangeQuote,
|
|
519
500
|
]);
|
|
520
501
|
|
|
521
502
|
return {
|
|
@@ -13,10 +13,7 @@ import {
|
|
|
13
13
|
mapAdminChangeBookingQuoteV2Data,
|
|
14
14
|
type ChangeBookingQuoteResponse,
|
|
15
15
|
} from '../src/lib/booking-api';
|
|
16
|
-
import {
|
|
17
|
-
buildAdminChangeQuoteRequestKey,
|
|
18
|
-
canReuseCompletedAdminChangeQuote,
|
|
19
|
-
} from '../src/components/booking/admin-change-quote-request-key';
|
|
16
|
+
import { buildAdminChangeQuoteRequestKey } from '../src/components/booking/admin-change-quote-request-key';
|
|
20
17
|
import {
|
|
21
18
|
noRefundRemovedValue,
|
|
22
19
|
normalizeReturnPriceTreatment,
|
|
@@ -40,6 +37,7 @@ import { buildPublicChangeAmountDueSummary } from '../src/components/booking/cha
|
|
|
40
37
|
import {
|
|
41
38
|
buildAdminChangePaymentChoiceData,
|
|
42
39
|
buildAdminChangePayNowCheckoutModalData,
|
|
40
|
+
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose,
|
|
43
41
|
} from '../src/components/booking/admin-change-payment-choice-runner';
|
|
44
42
|
|
|
45
43
|
function quote(overrides: Partial<ChangeBookingQuoteResponse>): ChangeBookingQuoteResponse {
|
|
@@ -120,6 +118,16 @@ test('admin existing-booking pay-now review keeps previous, updated, and exact d
|
|
|
120
118
|
assert.equal(checkout.finalizeExistingChangeInline, true);
|
|
121
119
|
assert.equal(choice.confirmWithoutPaymentLabel, 'Pay later');
|
|
122
120
|
assert.equal(choice.preferConfirmWithoutPayment, true);
|
|
121
|
+
assert.equal(
|
|
122
|
+
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose(true, true, choice),
|
|
123
|
+
true,
|
|
124
|
+
);
|
|
125
|
+
assert.equal(
|
|
126
|
+
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose(true, true, {
|
|
127
|
+
pricingQuoteId: ' ',
|
|
128
|
+
}),
|
|
129
|
+
false,
|
|
130
|
+
);
|
|
123
131
|
assert.equal(
|
|
124
132
|
buildAdminChangePayNowCheckoutModalData(
|
|
125
133
|
{ ...choice, finalizeExistingChangeInline: undefined },
|
|
@@ -198,13 +206,6 @@ test('admin quote response display state cannot invalidate its request identity'
|
|
|
198
206
|
assert.equal(keyAfterResponse, keyBeforeResponse);
|
|
199
207
|
});
|
|
200
208
|
|
|
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
|
-
|
|
208
209
|
test('admin no-charge provider payload carries the selected pickup itinerary', () => {
|
|
209
210
|
const itineraryDisplay: NonNullable<
|
|
210
211
|
Parameters<typeof buildAdminChangeProviderPayload>[0]['itineraryDisplay']
|