@ticketboothapp/booking 1.2.187 → 1.2.189
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 +7 -1
- package/src/components/booking/AdminChangeReceiptComparison.tsx +15 -24
- package/src/components/booking/admin-change-quote-request-key.ts +2 -0
- package/src/components/booking/change-booking-error-message.ts +5 -1
- package/src/components/booking/useAdminChangeQuotePreview.ts +9 -37
- package/src/components/booking/useChangeBookingInitialHydration.ts +17 -4
- package/test/change-booking-helpers.test.ts +20 -0
package/package.json
CHANGED
|
@@ -897,6 +897,12 @@ export function AdminChangeBookingFlow({
|
|
|
897
897
|
activeReturnPriceTreatmentOperations,
|
|
898
898
|
refundDispositionsByOperationId,
|
|
899
899
|
]);
|
|
900
|
+
const initialSelectionHydratedForQuote = Boolean(
|
|
901
|
+
selectedAvailability &&
|
|
902
|
+
totalQuantity > 0 &&
|
|
903
|
+
addOnCatalogLoaded &&
|
|
904
|
+
((selectedAvailability.returnOptions?.length ?? 0) === 0 || selectedReturnOption),
|
|
905
|
+
);
|
|
900
906
|
const handleRefundDispositionChange = useCallback((
|
|
901
907
|
operationId: string,
|
|
902
908
|
disposition: AdminReleaseRefundDisposition | null,
|
|
@@ -918,8 +924,8 @@ export function AdminChangeBookingFlow({
|
|
|
918
924
|
} = useAdminChangeQuotePreview({
|
|
919
925
|
isCustomerSelfServeChange,
|
|
920
926
|
isProviderDashboardChange,
|
|
921
|
-
hasChangeSelection,
|
|
922
927
|
hasEffectiveChangeSelection,
|
|
928
|
+
initialSelectionHydrated: initialSelectionHydratedForQuote,
|
|
923
929
|
selectedAvailability,
|
|
924
930
|
selectedReturnOption,
|
|
925
931
|
initialValues,
|
|
@@ -363,7 +363,7 @@ export function AdminChangeReceiptComparison({
|
|
|
363
363
|
</section>
|
|
364
364
|
|
|
365
365
|
<section className="min-w-0 overflow-visible rounded-lg border border-sky-200 bg-sky-50/50 p-3">
|
|
366
|
-
<div className="mb-3
|
|
366
|
+
<div className="mb-3">
|
|
367
367
|
<div>
|
|
368
368
|
<h3 className="text-sm font-semibold text-stone-900">
|
|
369
369
|
{isProductFamilyConversion ? 'New purchase' : 'Current change'}
|
|
@@ -374,29 +374,20 @@ export function AdminChangeReceiptComparison({
|
|
|
374
374
|
</p>
|
|
375
375
|
) : null}
|
|
376
376
|
</div>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
) : null}
|
|
392
|
-
<span className="text-xs font-medium text-stone-500">
|
|
393
|
-
{isProductFamilyConversion
|
|
394
|
-
? formatCurrencyAmount(updatedTotal ?? 0, currency, displayLocale)
|
|
395
|
-
: bookingTotalChange == null
|
|
396
|
-
? '—'
|
|
397
|
-
: formatCurrencyAmount(bookingTotalChange, currency, displayLocale)}
|
|
398
|
-
</span>
|
|
399
|
-
</div>
|
|
377
|
+
{onPreserveExistingReceiptChange ? (
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
aria-pressed={preserveExistingReceipt}
|
|
381
|
+
className={`mt-1.5 inline-flex text-sm font-semibold underline decoration-1 underline-offset-4 transition ${
|
|
382
|
+
preserveExistingReceipt
|
|
383
|
+
? 'text-emerald-800 decoration-emerald-700 hover:text-emerald-950'
|
|
384
|
+
: 'text-emerald-700 decoration-emerald-600 hover:text-emerald-900'
|
|
385
|
+
}`}
|
|
386
|
+
onClick={() => onPreserveExistingReceiptChange(!preserveExistingReceipt)}
|
|
387
|
+
>
|
|
388
|
+
Keep receipt unchanged
|
|
389
|
+
</button>
|
|
390
|
+
) : null}
|
|
400
391
|
</div>
|
|
401
392
|
{preserveExistingReceipt ? (
|
|
402
393
|
<div className="mb-3 rounded-md border border-sky-200 bg-white px-3 py-2">
|
|
@@ -24,6 +24,7 @@ interface AdminChangeQuoteRequestKeyInput {
|
|
|
24
24
|
interface AdminChangeQuoteRequestGateInput {
|
|
25
25
|
isCustomerSelfServeChange: boolean;
|
|
26
26
|
hasEffectiveChangeSelection: boolean;
|
|
27
|
+
initialSelectionHydrated?: boolean;
|
|
27
28
|
hasSelectedAvailability: boolean;
|
|
28
29
|
bookingReference?: string | null;
|
|
29
30
|
lastName: string;
|
|
@@ -45,6 +46,7 @@ export function canRequestAdminChangeQuote(input: AdminChangeQuoteRequestGateInp
|
|
|
45
46
|
return Boolean(
|
|
46
47
|
input.isCustomerSelfServeChange &&
|
|
47
48
|
input.hasEffectiveChangeSelection &&
|
|
49
|
+
input.initialSelectionHydrated !== false &&
|
|
48
50
|
input.hasSelectedAvailability &&
|
|
49
51
|
input.bookingReference?.trim() &&
|
|
50
52
|
input.lastName.trim() &&
|
|
@@ -93,7 +93,7 @@ export function incompatibleAddOnChangeError(
|
|
|
93
93
|
|
|
94
94
|
function errorCodeFromMessage(message: string): string | null {
|
|
95
95
|
const match = message.match(
|
|
96
|
-
/\b(?:pricing_v2|apply_v2|promo|admin|amendment|booking_amendment|private_shuttle|quote|structured_adjustment)_[a-z0-9_]
|
|
96
|
+
/\b(?:(?:missing_(?:add|remove)_operation):[a-z0-9_]+|(?:pricing_v2|apply_v2|promo|admin|amendment|booking_amendment|private_shuttle|quote|structured_adjustment)_[a-z0-9_]+)/i,
|
|
97
97
|
);
|
|
98
98
|
return match?.[0]?.toLowerCase() ?? null;
|
|
99
99
|
}
|
|
@@ -112,6 +112,10 @@ export function friendlyChangeBookingError(message: string | null | undefined):
|
|
|
112
112
|
const exact = CHANGE_BOOKING_ERROR_MESSAGES[code];
|
|
113
113
|
if (exact) return exact;
|
|
114
114
|
|
|
115
|
+
if (code.startsWith('missing_add_operation:return') || code.startsWith('missing_remove_operation:return')) {
|
|
116
|
+
return 'The return portion of this booking could not be priced. Re-select the return time and try again.';
|
|
117
|
+
}
|
|
118
|
+
|
|
115
119
|
if (code.startsWith('admin_adjustment_refund_disposition_required')) {
|
|
116
120
|
return 'Choose how to handle the removed adjustment value before continuing.';
|
|
117
121
|
}
|
|
@@ -64,8 +64,8 @@ interface ChangeSelectionDetailsForPreview {
|
|
|
64
64
|
export interface UseAdminChangeQuotePreviewParams {
|
|
65
65
|
isCustomerSelfServeChange: boolean;
|
|
66
66
|
isProviderDashboardChange: boolean;
|
|
67
|
-
hasChangeSelection: boolean;
|
|
68
67
|
hasEffectiveChangeSelection: boolean;
|
|
68
|
+
initialSelectionHydrated: boolean;
|
|
69
69
|
selectedAvailability: Availability | null;
|
|
70
70
|
selectedReturnOption: ReturnOption | null;
|
|
71
71
|
initialValues: ChangeBookingFlowProps['initialValues'];
|
|
@@ -129,8 +129,8 @@ export interface UseAdminChangeQuotePreviewResult {
|
|
|
129
129
|
export function useAdminChangeQuotePreview({
|
|
130
130
|
isCustomerSelfServeChange,
|
|
131
131
|
isProviderDashboardChange,
|
|
132
|
-
hasChangeSelection,
|
|
133
132
|
hasEffectiveChangeSelection,
|
|
133
|
+
initialSelectionHydrated,
|
|
134
134
|
selectedAvailability,
|
|
135
135
|
selectedReturnOption,
|
|
136
136
|
initialValues,
|
|
@@ -194,37 +194,10 @@ export function useAdminChangeQuotePreview({
|
|
|
194
194
|
(latestChangeQuote.refundDecisionOperations?.length ?? 0) === 0;
|
|
195
195
|
const refundDecisionRequired = (latestChangeQuote?.refundDecisionOperations?.length ?? 0) > 0;
|
|
196
196
|
const hasPublishedChangeQuote = latestChangeQuote !== null;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
* the editable target controls to become complete.
|
|
202
|
-
*/
|
|
203
|
-
const quoteAvailability = useMemo<Availability | null>(() => {
|
|
204
|
-
if (selectedAvailability) return selectedAvailability;
|
|
205
|
-
if (!needsAdjustmentDiscovery || !initialValues?.dateTime?.trim()) return null;
|
|
206
|
-
const optionId = initialValues.productOptionId?.trim() || initialValues.productId?.trim();
|
|
207
|
-
if (!optionId) return null;
|
|
208
|
-
return {
|
|
209
|
-
dateTime: initialValues.dateTime.trim(),
|
|
210
|
-
availabilityId: initialValues.availabilityId?.trim() || undefined,
|
|
211
|
-
productOptionId: optionId,
|
|
212
|
-
vacancies: Math.max(totalQuantity, 1),
|
|
213
|
-
currency,
|
|
214
|
-
};
|
|
215
|
-
}, [
|
|
216
|
-
selectedAvailability,
|
|
217
|
-
needsAdjustmentDiscovery,
|
|
218
|
-
initialValues?.dateTime,
|
|
219
|
-
initialValues?.availabilityId,
|
|
220
|
-
initialValues?.productOptionId,
|
|
221
|
-
initialValues?.productId,
|
|
222
|
-
totalQuantity,
|
|
223
|
-
currency,
|
|
224
|
-
]);
|
|
225
|
-
const quoteReturnAvailabilityId =
|
|
226
|
-
selectedReturnOption?.returnAvailabilityId ??
|
|
227
|
-
(needsAdjustmentDiscovery ? initialValues?.returnAvailabilityId?.trim() || null : null);
|
|
197
|
+
// Existing-adjustment discovery is still a real quote. Never send it with the temporary
|
|
198
|
+
// empty ticket/return state produced while the saved booking is being rehydrated.
|
|
199
|
+
const quoteAvailability = selectedAvailability;
|
|
200
|
+
const quoteReturnAvailabilityId = selectedReturnOption?.returnAvailabilityId ?? null;
|
|
228
201
|
const changeQuoteInputsKey = useMemo(() => buildAdminChangeQuoteRequestKey({
|
|
229
202
|
bookingReference: initialValues?.bookingReference,
|
|
230
203
|
lastName,
|
|
@@ -264,15 +237,14 @@ export function useAdminChangeQuotePreview({
|
|
|
264
237
|
]);
|
|
265
238
|
const destinationRequiresReturnSelection = Boolean(selectedAvailability?.returnOptions?.length);
|
|
266
239
|
const missingRequiredReturnSelection = destinationRequiresReturnSelection && !selectedReturnOption;
|
|
267
|
-
const quoteMissingRequiredReturnSelection =
|
|
268
|
-
missingRequiredReturnSelection && !needsAdjustmentDiscovery;
|
|
269
240
|
const canRequestChangeQuote = canRequestAdminChangeQuote({
|
|
270
241
|
isCustomerSelfServeChange,
|
|
271
|
-
hasEffectiveChangeSelection,
|
|
242
|
+
hasEffectiveChangeSelection: hasEffectiveChangeSelection || needsAdjustmentDiscovery,
|
|
243
|
+
initialSelectionHydrated,
|
|
272
244
|
hasSelectedAvailability: quoteAvailability != null,
|
|
273
245
|
bookingReference: initialValues?.bookingReference,
|
|
274
246
|
lastName,
|
|
275
|
-
missingRequiredReturnSelection
|
|
247
|
+
missingRequiredReturnSelection,
|
|
276
248
|
});
|
|
277
249
|
const changeFlowSubmitDisabled =
|
|
278
250
|
missingRequiredReturnSelection ||
|
|
@@ -45,6 +45,16 @@ export interface UseChangeBookingInitialHydrationOptions {
|
|
|
45
45
|
setError: Dispatch<SetStateAction<string>>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function initialPromoCodeToApply(
|
|
49
|
+
initialPromoCode: string | null | undefined,
|
|
50
|
+
lockedPromoCode: string | null,
|
|
51
|
+
): string | null {
|
|
52
|
+
if (!lockedPromoCode) return null;
|
|
53
|
+
const normalizedInitial = initialPromoCode?.trim().toUpperCase() ?? '';
|
|
54
|
+
const normalizedLocked = lockedPromoCode.trim().toUpperCase();
|
|
55
|
+
return normalizedInitial && normalizedInitial === normalizedLocked ? normalizedInitial : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
export function useChangeBookingInitialHydration({
|
|
49
59
|
initialValues,
|
|
50
60
|
lockedPromoCode,
|
|
@@ -84,7 +94,7 @@ export function useChangeBookingInitialHydration({
|
|
|
84
94
|
const trimmedEmail = initialValues.customer?.email?.trim();
|
|
85
95
|
const trimmedFirstName = initialValues.customer?.firstName?.trim();
|
|
86
96
|
const trimmedLastName = initialValues.customer?.lastName?.trim();
|
|
87
|
-
const
|
|
97
|
+
const initialPromoToApply = initialPromoCodeToApply(initialValues.promoCode, lockedPromoCode);
|
|
88
98
|
if (trimmedEmail) setEmail(trimmedEmail);
|
|
89
99
|
if (trimmedFirstName) setFirstName(trimmedFirstName);
|
|
90
100
|
if (trimmedLastName) setLastName(trimmedLastName);
|
|
@@ -92,13 +102,16 @@ export function useChangeBookingInitialHydration({
|
|
|
92
102
|
setPickupLocationId(initialValues.pickupLocationId.trim());
|
|
93
103
|
setPickupLocationSkipped(false);
|
|
94
104
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
// Provider amendments show the historical code separately and require the operator to
|
|
106
|
+
// explicitly apply it again. Only public/self-serve flows pass a locked promo here.
|
|
107
|
+
if (initialPromoToApply) {
|
|
108
|
+
setPromoCodeInput(initialPromoToApply);
|
|
109
|
+
setAppliedPromoCode(initialPromoToApply);
|
|
98
110
|
}
|
|
99
111
|
hasAppliedInitialValuesRef.current = true;
|
|
100
112
|
}, [
|
|
101
113
|
initialValues,
|
|
114
|
+
lockedPromoCode,
|
|
102
115
|
setAppliedPromoCode,
|
|
103
116
|
setEmail,
|
|
104
117
|
setFirstName,
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
canReuseCompletedAdminChangeQuote,
|
|
36
36
|
isAdminRefundDecisionDiscoveryQuote,
|
|
37
37
|
} from '../src/components/booking/admin-change-quote-request-key';
|
|
38
|
+
import { initialPromoCodeToApply } from '../src/components/booking/useChangeBookingInitialHydration';
|
|
38
39
|
import {
|
|
39
40
|
authoritativeItineraryDisplayFromChangeQuote,
|
|
40
41
|
sliceAndMergeChangeQuoteForUi,
|
|
@@ -762,6 +763,21 @@ test('admin quote gate allows adjustment-only changes', () => {
|
|
|
762
763
|
lastName: 'Tauro',
|
|
763
764
|
missingRequiredReturnSelection: false,
|
|
764
765
|
}), false);
|
|
766
|
+
|
|
767
|
+
assert.equal(canRequestAdminChangeQuote({
|
|
768
|
+
isCustomerSelfServeChange: true,
|
|
769
|
+
hasEffectiveChangeSelection: true,
|
|
770
|
+
initialSelectionHydrated: false,
|
|
771
|
+
hasSelectedAvailability: true,
|
|
772
|
+
bookingReference: 'DIFG66OF',
|
|
773
|
+
lastName: 'Tauro',
|
|
774
|
+
missingRequiredReturnSelection: false,
|
|
775
|
+
}), false);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test('provider reopen does not automatically reapply the historical promo', () => {
|
|
779
|
+
assert.equal(initialPromoCodeToApply(' LOCKEDIN2026 ', null), null);
|
|
780
|
+
assert.equal(initialPromoCodeToApply(' LOCKEDIN2026 ', 'LOCKEDIN2026'), 'LOCKEDIN2026');
|
|
765
781
|
});
|
|
766
782
|
|
|
767
783
|
test('admin preview and checkout share the Pricing V2 request shape', () => {
|
|
@@ -2272,6 +2288,10 @@ test('translates booking-change reason codes into operator-facing messages', ()
|
|
|
2272
2288
|
friendlyChangeBookingError('private_shuttle_itinerary_endpoints_required'),
|
|
2273
2289
|
'The private-tour itinerary is incomplete. Reopen the change and select the pickup time and location again.',
|
|
2274
2290
|
);
|
|
2291
|
+
assert.equal(
|
|
2292
|
+
friendlyChangeBookingError('missing_add_operation:return'),
|
|
2293
|
+
'The return portion of this booking could not be priced. Re-select the return time and try again.',
|
|
2294
|
+
);
|
|
2275
2295
|
assert.equal(
|
|
2276
2296
|
friendlyChangeBookingError('Remove the unavailable add-on before recalculating this amendment.'),
|
|
2277
2297
|
'Remove the unavailable add-on before recalculating this amendment.',
|