@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 +1 -1
- package/src/components/booking/admin-change-payment-choice-runner.ts +5 -0
- package/src/components/booking/admin-change-quote-request-key.ts +13 -0
- package/src/components/booking/useAdminChangeCheckoutController.ts +10 -8
- package/src/components/booking/useAdminChangeQuotePreview.ts +34 -10
- package/test/change-booking-helpers.test.ts +13 -1
package/package.json
CHANGED
|
@@ -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?.
|
|
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 =
|
|
863
|
-
const quoteId =
|
|
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 =
|
|
897
|
-
const quoteId =
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
288
|
-
? latestChangeQuote.
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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 {
|
|
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']
|