@ticketboothapp/booking 1.2.167 → 1.2.169
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/AdminChangeBookingContent.tsx +13 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +180 -65
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +43 -13
- package/src/components/booking/AdminChangePricingPanel.tsx +400 -134
- package/src/components/booking/AdminChangeReceiptComparison.tsx +166 -145
- package/src/components/booking/ChangeBookingFlow.tsx +7 -3
- package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +7 -2
- package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +6 -0
- package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +80 -0
- package/src/components/booking/PickupLocationSelector.module.css +22 -0
- package/src/components/booking/PickupLocationSelector.tsx +3 -3
- package/src/components/booking/PriceSummary.tsx +4 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +27 -13
- package/src/components/booking/PrivateShuttleCheckoutSection.tsx +95 -54
- package/src/components/booking/admin-adjustment-components.ts +44 -0
- package/src/components/booking/admin-adjustment-tax-behavior.ts +50 -0
- package/src/components/booking/admin-change-flow-state-helpers.ts +10 -0
- package/src/components/booking/admin-change-provider-payload.ts +3 -0
- package/src/components/booking/admin-change-quote-request-key.ts +27 -0
- package/src/components/booking/admin-change-v2-quote-request.ts +10 -2
- package/src/components/booking/admin-refund-decision-context.ts +37 -0
- package/src/components/booking/admin-refund-disposition.ts +71 -2
- package/src/components/booking/booking-flow-types.ts +4 -0
- package/src/components/booking/booking-flow.css +5 -0
- package/src/components/booking/change-booking-error-message.ts +137 -0
- package/src/components/booking/change-booking-flow-helpers.ts +77 -2
- package/src/components/booking/change-booking-quote-guards.ts +4 -1
- package/src/components/booking/change-booking-quote-state.ts +44 -1
- package/src/components/booking/incompatible-add-on-selections.ts +19 -0
- package/src/components/booking/private-shuttle-availability.ts +14 -0
- package/src/components/booking/private-shuttle-cancellation-policy.ts +9 -0
- package/src/components/booking/private-shuttle-checkout-controller.ts +5 -0
- package/src/components/booking/private-shuttle-provider-change-runner.ts +21 -0
- package/src/components/booking/private-shuttle-reservation-runner.ts +5 -3
- package/src/components/booking/provider-dashboard-change-booking.ts +16 -1
- package/src/components/booking/useAdminChangeCheckoutController.ts +16 -0
- package/src/components/booking/useAdminChangeProductReset.ts +6 -0
- package/src/components/booking/useAdminChangeProtectedPricing.ts +4 -0
- package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +6 -4
- package/src/components/booking/useAdminChangeQuotePreview.ts +76 -20
- package/src/components/booking/useAdminCustomReceiptLines.ts +171 -10
- package/src/components/booking/useAdminProviderPricingAdjustments.ts +50 -19
- package/src/components/booking/useBookingAvailabilityAddOns.ts +19 -2
- package/src/components/booking/useChangeBookingAddOnFloorController.ts +15 -9
- package/src/components/booking/useChangeBookingInitialHydration.ts +10 -7
- package/src/components/booking/useChangeBookingQuotePreview.ts +3 -1
- package/src/components/booking/useChangeBookingSelectionDetails.ts +23 -17
- package/src/lib/booking/change-booking-server-preview.ts +26 -9
- package/src/lib/booking/change-flow-pricing.ts +12 -0
- package/src/lib/booking/i18n/messages/en.json +1 -0
- package/src/lib/booking/i18n/messages/fr.json +1 -0
- package/src/lib/booking-api.ts +64 -0
- package/test/change-booking-helpers.test.ts +853 -3
- package/src/components/booking/useAdminChangePricingDebugPanel.tsx +0 -178
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type AddOn,
|
|
13
13
|
type AdminAmendmentOperationSnapshot,
|
|
14
14
|
type AdminAmendmentRefundDisposition,
|
|
15
|
+
type AdminAmendmentActiveAdjustmentSnapshot,
|
|
15
16
|
type AdminFeAuthoritativeReceipt,
|
|
16
17
|
type AdminPromoApplicationScope,
|
|
17
18
|
type Availability,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
import type { ChangeBookingFlowProps } from './booking-flow-types';
|
|
43
44
|
import type { CheckoutModalLineItem } from './CheckoutModal';
|
|
44
45
|
import type { Currency } from './CurrencySwitcher';
|
|
46
|
+
import type { AdminAdjustmentComponentOption } from './admin-adjustment-components';
|
|
45
47
|
import {
|
|
46
48
|
findMergedAvailabilityForSelection,
|
|
47
49
|
findMergedReturnVacancies,
|
|
@@ -91,6 +93,10 @@ export interface AdminChangeLatestQuote {
|
|
|
91
93
|
quoteExpiresAt?: string;
|
|
92
94
|
pricingVersion?: string;
|
|
93
95
|
quotedTotal?: number;
|
|
96
|
+
amountPreviouslyPaid?: number;
|
|
97
|
+
currentPaymentCreditTotal?: number;
|
|
98
|
+
projectedPaymentCreditTotal?: number;
|
|
99
|
+
refundCandidate?: number;
|
|
94
100
|
paymentCreditTotal?: number;
|
|
95
101
|
refundDecisionOperations?: AdminAmendmentOperationSnapshot[];
|
|
96
102
|
returnPriceTreatmentOperations?: AdminAmendmentOperationSnapshot[];
|
|
@@ -100,8 +106,11 @@ export interface AdminChangeLatestQuote {
|
|
|
100
106
|
quotePreviousTotalCents?: number;
|
|
101
107
|
quoteNewTotalCents?: number;
|
|
102
108
|
serverPreview: ChangeBookingServerPreview;
|
|
109
|
+
authoritativeItineraryDisplay?: ItineraryDisplayStep[] | null;
|
|
103
110
|
pricingDriftDetail?: ChangeBookingQuotePricingDriftDetail;
|
|
104
111
|
ticketPricingTrace?: ChangeBookingQuoteTicketPricingTrace | null;
|
|
112
|
+
selectableAdjustmentComponents?: AdminAdjustmentComponentOption[];
|
|
113
|
+
reversibleAdjustments?: AdminAmendmentActiveAdjustmentSnapshot[];
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
export interface UseAdminChangeCheckoutControllerParams {
|
|
@@ -146,6 +155,7 @@ export interface UseAdminChangeCheckoutControllerParams {
|
|
|
146
155
|
providerPricingOverrides: AdminChangeProviderLineOverride[];
|
|
147
156
|
mergedProviderAdditionalAdjustments: AdminChangeProviderAdditionalAdjustment[];
|
|
148
157
|
adminStructuredAdjustments: NonNullable<ProviderDashboardChangeBookingPayload['structuredAdjustments']>;
|
|
158
|
+
adminReverseAdjustmentIds: string[];
|
|
149
159
|
refundDispositionsByOperationId: NonNullable<
|
|
150
160
|
ProviderDashboardChangeBookingPayload['refundDispositionsByOperationId']
|
|
151
161
|
>;
|
|
@@ -248,6 +258,7 @@ export function useAdminChangeCheckoutController({
|
|
|
248
258
|
providerPricingOverrides,
|
|
249
259
|
mergedProviderAdditionalAdjustments,
|
|
250
260
|
adminStructuredAdjustments,
|
|
261
|
+
adminReverseAdjustmentIds,
|
|
251
262
|
refundDispositionsByOperationId,
|
|
252
263
|
providerApplyAuthoritativeReceipt,
|
|
253
264
|
onSuccess,
|
|
@@ -370,6 +381,7 @@ export function useAdminChangeCheckoutController({
|
|
|
370
381
|
providerPricingOverrides,
|
|
371
382
|
mergedProviderAdditionalAdjustments,
|
|
372
383
|
adminStructuredAdjustments,
|
|
384
|
+
adminReverseAdjustmentIds,
|
|
373
385
|
refundDispositionsByOperationId,
|
|
374
386
|
pricingV2QuoteId: authoritativeQuote?.quoteId ?? null,
|
|
375
387
|
pricingV2QuotedTotal: authoritativeQuote?.quotedTotal ?? authoritativeQuote?.serverDisplay?.total ?? null,
|
|
@@ -487,6 +499,10 @@ export function useAdminChangeCheckoutController({
|
|
|
487
499
|
promoCode: appliedPromoCode,
|
|
488
500
|
promoApplicationScope,
|
|
489
501
|
structuredAdjustments: adminStructuredAdjustments,
|
|
502
|
+
reverseAdjustmentIds: adminReverseAdjustmentIds,
|
|
503
|
+
includeSelectableAdjustmentComponents: adminStructuredAdjustments.some(
|
|
504
|
+
(adjustment) => adjustment.scope === 'SELECTED_COMPONENTS',
|
|
505
|
+
),
|
|
490
506
|
refundDispositionsByOperationId,
|
|
491
507
|
previousPassengerCount: changeFlowInitialTicketCount,
|
|
492
508
|
previousAvailabilityId: initialValues?.availabilityId ?? null,
|
|
@@ -8,6 +8,7 @@ interface ResetRef {
|
|
|
8
8
|
|
|
9
9
|
export interface UseAdminChangeProductResetParams {
|
|
10
10
|
selectedChangeProductId: string;
|
|
11
|
+
setQuantities: Dispatch<SetStateAction<Record<string, number>>>;
|
|
11
12
|
setSelectedAvailability: Dispatch<SetStateAction<Availability | null>>;
|
|
12
13
|
setSelectedReturnOption: Dispatch<SetStateAction<ReturnOption | null>>;
|
|
13
14
|
setAddOns: Dispatch<SetStateAction<AddOn[]>>;
|
|
@@ -21,6 +22,7 @@ export interface UseAdminChangeProductResetParams {
|
|
|
21
22
|
|
|
22
23
|
export function useAdminChangeProductReset({
|
|
23
24
|
selectedChangeProductId,
|
|
25
|
+
setQuantities,
|
|
24
26
|
setSelectedAvailability,
|
|
25
27
|
setSelectedReturnOption,
|
|
26
28
|
setAddOns,
|
|
@@ -36,6 +38,9 @@ export function useAdminChangeProductReset({
|
|
|
36
38
|
useEffect(() => {
|
|
37
39
|
if (previousSelectedChangeProductIdRef.current === selectedChangeProductId) return;
|
|
38
40
|
previousSelectedChangeProductIdRef.current = selectedChangeProductId;
|
|
41
|
+
// Ticket categories belong to the selected product family. In particular, a private
|
|
42
|
+
// RESOURCE quantity must not survive a switch to a regular ADULT/CHILD/INFANT product.
|
|
43
|
+
setQuantities({});
|
|
39
44
|
setSelectedAvailability(null);
|
|
40
45
|
setSelectedReturnOption(null);
|
|
41
46
|
setAddOns([]);
|
|
@@ -47,6 +52,7 @@ export function useAdminChangeProductReset({
|
|
|
47
52
|
setError('');
|
|
48
53
|
}, [
|
|
49
54
|
selectedChangeProductId,
|
|
55
|
+
setQuantities,
|
|
50
56
|
setSelectedAvailability,
|
|
51
57
|
setSelectedReturnOption,
|
|
52
58
|
setAddOns,
|
|
@@ -20,6 +20,7 @@ type AdminCustomReceiptLine = {
|
|
|
20
20
|
amountInput: string;
|
|
21
21
|
amountSign?: number;
|
|
22
22
|
calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
|
|
23
|
+
sourceAdjustmentId?: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
function addOnSelectionTotal(selections: AddOnSelection[] | null | undefined, addOns: AddOn[]): number {
|
|
@@ -220,6 +221,9 @@ export function useAdminChangeProtectedPricing({
|
|
|
220
221
|
() =>
|
|
221
222
|
roundMoney(
|
|
222
223
|
adminCustomReceiptLines.reduce((sum, line) => {
|
|
224
|
+
// Existing ledger adjustments are already included in the persisted receipt. Their
|
|
225
|
+
// edit delta is server-priced as reverse + replace and must not be added again here.
|
|
226
|
+
if (line.sourceAdjustmentId) return sum;
|
|
223
227
|
if (line.calculation !== 'FIXED_AMOUNT') return sum;
|
|
224
228
|
const raw = line.amountInput.trim();
|
|
225
229
|
if (raw === '' || raw === '+' || raw === '-') return sum;
|
|
@@ -55,7 +55,7 @@ export interface UseAdminChangeQuoteDisplayStateParams {
|
|
|
55
55
|
pickupLocationId: string | null;
|
|
56
56
|
quantities: Record<string, number>;
|
|
57
57
|
addOnSelections: AddOnSelection[];
|
|
58
|
-
|
|
58
|
+
initialAddOnQtyByKey: Map<string, number>;
|
|
59
59
|
changeFlowResolvedInitialProductOptionId: string | null;
|
|
60
60
|
selectedParentProductChanged: boolean;
|
|
61
61
|
isProviderDashboardChange: boolean;
|
|
@@ -65,6 +65,7 @@ export interface UseAdminChangeQuoteDisplayStateParams {
|
|
|
65
65
|
editableSummaryPreSubtotalDelta: number;
|
|
66
66
|
adminCustomAdjustmentTotal: number;
|
|
67
67
|
adminCustomReceiptLines: AdminCustomReceiptLine[];
|
|
68
|
+
adminReverseAdjustmentIds: string[];
|
|
68
69
|
isManualOverrideEligibleLine: (line: ProviderDashboardPricingLine) => boolean;
|
|
69
70
|
isAdmin: boolean;
|
|
70
71
|
useAdminFeAuthoritativeQuote: boolean;
|
|
@@ -99,7 +100,7 @@ export function useAdminChangeQuoteDisplayState({
|
|
|
99
100
|
pickupLocationId,
|
|
100
101
|
quantities,
|
|
101
102
|
addOnSelections,
|
|
102
|
-
|
|
103
|
+
initialAddOnQtyByKey,
|
|
103
104
|
changeFlowResolvedInitialProductOptionId,
|
|
104
105
|
selectedParentProductChanged,
|
|
105
106
|
isProviderDashboardChange,
|
|
@@ -109,6 +110,7 @@ export function useAdminChangeQuoteDisplayState({
|
|
|
109
110
|
editableSummaryPreSubtotalDelta,
|
|
110
111
|
adminCustomAdjustmentTotal,
|
|
111
112
|
adminCustomReceiptLines,
|
|
113
|
+
adminReverseAdjustmentIds,
|
|
112
114
|
isManualOverrideEligibleLine,
|
|
113
115
|
isAdmin,
|
|
114
116
|
useAdminFeAuthoritativeQuote,
|
|
@@ -321,7 +323,7 @@ export function useAdminChangeQuoteDisplayState({
|
|
|
321
323
|
pickupLocationId,
|
|
322
324
|
quantities,
|
|
323
325
|
addOnSelections,
|
|
324
|
-
|
|
326
|
+
initialAddOnQtyByKey,
|
|
325
327
|
changeFlowResolvedInitialProductOptionId,
|
|
326
328
|
productChanged: selectedParentProductChanged,
|
|
327
329
|
});
|
|
@@ -344,8 +346,8 @@ export function useAdminChangeQuoteDisplayState({
|
|
|
344
346
|
providerQuotedLines,
|
|
345
347
|
hasChangeSelection,
|
|
346
348
|
editableSummaryPreSubtotalDelta,
|
|
347
|
-
adminCustomAdjustmentTotal,
|
|
348
349
|
adminCustomReceiptLines,
|
|
350
|
+
adminReverseAdjustmentIds,
|
|
349
351
|
isManualOverrideEligibleLine,
|
|
350
352
|
});
|
|
351
353
|
|
|
@@ -29,12 +29,14 @@ import type { Currency } from './CurrencySwitcher';
|
|
|
29
29
|
import type { AdminChangeLatestQuote } from './useAdminChangeCheckoutController';
|
|
30
30
|
import {
|
|
31
31
|
buildAdminChangeQuoteRequestKey,
|
|
32
|
+
canRequestAdminChangeQuote,
|
|
32
33
|
canReuseCompletedAdminChangeQuote,
|
|
33
34
|
} from './admin-change-quote-request-key';
|
|
34
35
|
import {
|
|
35
36
|
buildAdminChangeV2QuoteRequest,
|
|
36
37
|
quoteAdminChangeV2ForUi,
|
|
37
38
|
} from './admin-change-v2-quote-request';
|
|
39
|
+
import { friendlyChangeBookingError } from './change-booking-error-message';
|
|
38
40
|
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
39
41
|
|
|
40
42
|
type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
|
|
@@ -43,7 +45,12 @@ type AdminCustomReceiptLine = {
|
|
|
43
45
|
amountInput: string;
|
|
44
46
|
amountSign?: number;
|
|
45
47
|
calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
|
|
46
|
-
scope:
|
|
48
|
+
scope:
|
|
49
|
+
| 'POSITIVE_AMENDMENT_VALUE'
|
|
50
|
+
| 'CURRENT_ACTIVE_TICKETS'
|
|
51
|
+
| 'CURRENT_ACTIVE_BOOKING'
|
|
52
|
+
| 'SELECTED_COMPONENTS';
|
|
53
|
+
selectedComponentIds: string[];
|
|
47
54
|
taxBehavior: 'NON_TAXABLE' | 'TAXABLE' | 'PRE_TAX_DISCOUNT' | 'POST_TAX_DISCOUNT';
|
|
48
55
|
reasonCode: string;
|
|
49
56
|
};
|
|
@@ -71,6 +78,8 @@ export interface UseAdminChangeQuotePreviewParams {
|
|
|
71
78
|
promoApplicationScope: AdminPromoApplicationScope;
|
|
72
79
|
adminCustomReceiptLines: AdminCustomReceiptLine[];
|
|
73
80
|
adminStructuredAdjustments: NonNullable<ProviderDashboardChangeBookingPayload['structuredAdjustments']>;
|
|
81
|
+
adminReverseAdjustmentIds: string[];
|
|
82
|
+
needsAdjustmentDiscovery: boolean;
|
|
74
83
|
refundDispositionsByOperationId: Record<string, AdminAmendmentRefundDisposition>;
|
|
75
84
|
useAdminFeAuthoritativeQuote: boolean;
|
|
76
85
|
latestChangeQuote: AdminChangeLatestQuote | null;
|
|
@@ -132,6 +141,8 @@ export function useAdminChangeQuotePreview({
|
|
|
132
141
|
promoApplicationScope,
|
|
133
142
|
adminCustomReceiptLines,
|
|
134
143
|
adminStructuredAdjustments,
|
|
144
|
+
adminReverseAdjustmentIds,
|
|
145
|
+
needsAdjustmentDiscovery,
|
|
135
146
|
refundDispositionsByOperationId,
|
|
136
147
|
useAdminFeAuthoritativeQuote,
|
|
137
148
|
latestChangeQuote,
|
|
@@ -167,10 +178,9 @@ export function useAdminChangeQuotePreview({
|
|
|
167
178
|
onPricePreviewChange,
|
|
168
179
|
}: UseAdminChangeQuotePreviewParams): UseAdminChangeQuotePreviewResult {
|
|
169
180
|
const lastChangeFlowPreviewKeyRef = useRef<string | null>(null);
|
|
170
|
-
|
|
171
181
|
const changeFlowNeedsServerPrice =
|
|
172
182
|
isCustomerSelfServeChange &&
|
|
173
|
-
|
|
183
|
+
hasEffectiveChangeSelection &&
|
|
174
184
|
!!initialValues?.bookingReference?.trim() &&
|
|
175
185
|
!!lastName.trim();
|
|
176
186
|
|
|
@@ -179,13 +189,44 @@ export function useAdminChangeQuotePreview({
|
|
|
179
189
|
(latestChangeQuote.refundDecisionOperations?.length ?? 0) === 0;
|
|
180
190
|
const refundDecisionRequired = (latestChangeQuote?.refundDecisionOperations?.length ?? 0) > 0;
|
|
181
191
|
const hasPublishedChangeQuote = latestChangeQuote !== null;
|
|
192
|
+
/**
|
|
193
|
+
* Existing admin adjustments must be discoverable before the operator makes a change.
|
|
194
|
+
* Initial selection hydration can briefly leave the live availability/return unset, so use
|
|
195
|
+
* the persisted booking identities for this read-only discovery quote instead of waiting for
|
|
196
|
+
* the editable target controls to become complete.
|
|
197
|
+
*/
|
|
198
|
+
const quoteAvailability = useMemo<Availability | null>(() => {
|
|
199
|
+
if (selectedAvailability) return selectedAvailability;
|
|
200
|
+
if (!needsAdjustmentDiscovery || !initialValues?.dateTime?.trim()) return null;
|
|
201
|
+
const optionId = initialValues.productOptionId?.trim() || initialValues.productId?.trim();
|
|
202
|
+
if (!optionId) return null;
|
|
203
|
+
return {
|
|
204
|
+
dateTime: initialValues.dateTime.trim(),
|
|
205
|
+
availabilityId: initialValues.availabilityId?.trim() || undefined,
|
|
206
|
+
productOptionId: optionId,
|
|
207
|
+
vacancies: Math.max(totalQuantity, 1),
|
|
208
|
+
currency,
|
|
209
|
+
};
|
|
210
|
+
}, [
|
|
211
|
+
selectedAvailability,
|
|
212
|
+
needsAdjustmentDiscovery,
|
|
213
|
+
initialValues?.dateTime,
|
|
214
|
+
initialValues?.availabilityId,
|
|
215
|
+
initialValues?.productOptionId,
|
|
216
|
+
initialValues?.productId,
|
|
217
|
+
totalQuantity,
|
|
218
|
+
currency,
|
|
219
|
+
]);
|
|
220
|
+
const quoteReturnAvailabilityId =
|
|
221
|
+
selectedReturnOption?.returnAvailabilityId ??
|
|
222
|
+
(needsAdjustmentDiscovery ? initialValues?.returnAvailabilityId?.trim() || null : null);
|
|
182
223
|
const changeQuoteInputsKey = useMemo(() => buildAdminChangeQuoteRequestKey({
|
|
183
224
|
bookingReference: initialValues?.bookingReference,
|
|
184
225
|
lastName,
|
|
185
226
|
productId: product.productId,
|
|
186
|
-
selectedAvailability,
|
|
227
|
+
selectedAvailability: quoteAvailability,
|
|
187
228
|
pickupLocationId,
|
|
188
|
-
returnAvailabilityId:
|
|
229
|
+
returnAvailabilityId: quoteReturnAvailabilityId,
|
|
189
230
|
quantities,
|
|
190
231
|
addOnSelections,
|
|
191
232
|
adminCustomReceiptLines,
|
|
@@ -193,15 +234,16 @@ export function useAdminChangeQuotePreview({
|
|
|
193
234
|
promoCode: appliedPromoCode,
|
|
194
235
|
promoApplicationScope,
|
|
195
236
|
structuredAdjustments: adminStructuredAdjustments,
|
|
237
|
+
reverseAdjustmentIds: adminReverseAdjustmentIds,
|
|
196
238
|
refundDispositionsByOperationId,
|
|
197
239
|
useAdminFeAuthoritativeQuote,
|
|
198
240
|
}), [
|
|
199
241
|
initialValues?.bookingReference,
|
|
200
242
|
lastName,
|
|
201
243
|
product.productId,
|
|
202
|
-
|
|
244
|
+
quoteAvailability,
|
|
203
245
|
pickupLocationId,
|
|
204
|
-
|
|
246
|
+
quoteReturnAvailabilityId,
|
|
205
247
|
quantities,
|
|
206
248
|
addOnSelections,
|
|
207
249
|
adminCustomReceiptLines,
|
|
@@ -209,11 +251,22 @@ export function useAdminChangeQuotePreview({
|
|
|
209
251
|
appliedPromoCode,
|
|
210
252
|
promoApplicationScope,
|
|
211
253
|
adminStructuredAdjustments,
|
|
254
|
+
adminReverseAdjustmentIds,
|
|
212
255
|
refundDispositionsByOperationId,
|
|
213
256
|
useAdminFeAuthoritativeQuote,
|
|
214
257
|
]);
|
|
215
258
|
const destinationRequiresReturnSelection = Boolean(selectedAvailability?.returnOptions?.length);
|
|
216
259
|
const missingRequiredReturnSelection = destinationRequiresReturnSelection && !selectedReturnOption;
|
|
260
|
+
const quoteMissingRequiredReturnSelection =
|
|
261
|
+
missingRequiredReturnSelection && !needsAdjustmentDiscovery;
|
|
262
|
+
const canRequestChangeQuote = canRequestAdminChangeQuote({
|
|
263
|
+
isCustomerSelfServeChange,
|
|
264
|
+
hasEffectiveChangeSelection: hasEffectiveChangeSelection || needsAdjustmentDiscovery,
|
|
265
|
+
hasSelectedAvailability: quoteAvailability != null,
|
|
266
|
+
bookingReference: initialValues?.bookingReference,
|
|
267
|
+
lastName,
|
|
268
|
+
missingRequiredReturnSelection: quoteMissingRequiredReturnSelection,
|
|
269
|
+
});
|
|
217
270
|
|
|
218
271
|
const changeFlowSubmitDisabled =
|
|
219
272
|
missingRequiredReturnSelection ||
|
|
@@ -279,20 +332,23 @@ export function useAdminChangeQuotePreview({
|
|
|
279
332
|
: 'Change booking (no charge)';
|
|
280
333
|
})();
|
|
281
334
|
|
|
282
|
-
const
|
|
335
|
+
const rawCheckoutFormError =
|
|
283
336
|
(error || '') ||
|
|
284
337
|
(missingRequiredReturnSelection ? 'Please select a return time for this product.' : '') ||
|
|
285
338
|
(isChangeQuoteBlocked && !refundDecisionRequired
|
|
286
339
|
? (latestChangeQuote?.reasonIfBlocked ?? 'This change cannot be priced right now.')
|
|
287
340
|
: '') ||
|
|
288
341
|
(changeQuoteFetchError ?? '');
|
|
342
|
+
const checkoutFormError = friendlyChangeBookingError(rawCheckoutFormError);
|
|
289
343
|
|
|
290
344
|
const changeFlowSelectionPreview = useMemo((): ChangeFlowSelectionPreview | null => {
|
|
291
345
|
if (!selectedAvailability || totalQuantity <= 0) return null;
|
|
292
346
|
const ticketsLine = formatTicketLineItemsForSummary(
|
|
293
347
|
ticketLineItems.map((l) => ({ category: l.category, qty: l.qty }))
|
|
294
348
|
);
|
|
295
|
-
const itinerary =
|
|
349
|
+
const itinerary =
|
|
350
|
+
latestChangeQuote?.authoritativeItineraryDisplay ??
|
|
351
|
+
computeItineraryDisplay();
|
|
296
352
|
const itinerarySteps =
|
|
297
353
|
itinerary?.map((s) => ({
|
|
298
354
|
time: s.time?.trim() || null,
|
|
@@ -336,6 +392,7 @@ export function useAdminChangeQuotePreview({
|
|
|
336
392
|
latestChangeQuote?.quotedTotal,
|
|
337
393
|
latestChangeQuote?.serverDisplay?.total,
|
|
338
394
|
latestChangeQuote?.serverPreview?.totalNewBooking,
|
|
395
|
+
latestChangeQuote?.authoritativeItineraryDisplay,
|
|
339
396
|
]);
|
|
340
397
|
|
|
341
398
|
useEffect(() => {
|
|
@@ -407,13 +464,7 @@ export function useAdminChangeQuotePreview({
|
|
|
407
464
|
return;
|
|
408
465
|
}
|
|
409
466
|
|
|
410
|
-
if (
|
|
411
|
-
!hasChangeSelection ||
|
|
412
|
-
!selectedAvailability ||
|
|
413
|
-
!initialValues?.bookingReference?.trim() ||
|
|
414
|
-
!lastName.trim() ||
|
|
415
|
-
missingRequiredReturnSelection
|
|
416
|
-
) {
|
|
467
|
+
if (!canRequestChangeQuote || !quoteAvailability || !initialValues?.bookingReference?.trim()) {
|
|
417
468
|
setLatestChangeQuote(null);
|
|
418
469
|
setChangeQuoteLoading(false);
|
|
419
470
|
setChangeQuoteFetchError(null);
|
|
@@ -435,7 +486,7 @@ export function useAdminChangeQuotePreview({
|
|
|
435
486
|
return;
|
|
436
487
|
}
|
|
437
488
|
|
|
438
|
-
const optionId = getAvailabilityOptionId(
|
|
489
|
+
const optionId = getAvailabilityOptionId(quoteAvailability);
|
|
439
490
|
if (!optionId) {
|
|
440
491
|
setLatestChangeQuote(null);
|
|
441
492
|
setChangeQuoteLoading(false);
|
|
@@ -462,15 +513,20 @@ export function useAdminChangeQuotePreview({
|
|
|
462
513
|
lastName,
|
|
463
514
|
parentProductId: product.productId,
|
|
464
515
|
optionId,
|
|
465
|
-
selectedAvailability,
|
|
516
|
+
selectedAvailability: quoteAvailability,
|
|
466
517
|
pickupLocationId,
|
|
467
|
-
returnAvailabilityId:
|
|
518
|
+
returnAvailabilityId: quoteReturnAvailabilityId,
|
|
468
519
|
bookingItems,
|
|
469
520
|
addOnSelections,
|
|
470
521
|
cancellationPolicyId: cancellationPolicyId ?? null,
|
|
471
522
|
promoCode: appliedPromoCode,
|
|
472
523
|
promoApplicationScope,
|
|
473
524
|
structuredAdjustments: adminStructuredAdjustments,
|
|
525
|
+
reverseAdjustmentIds: adminReverseAdjustmentIds,
|
|
526
|
+
includeSelectableAdjustmentComponents:
|
|
527
|
+
needsAdjustmentDiscovery || adminCustomReceiptLines.some(
|
|
528
|
+
(line) => line.scope === 'SELECTED_COMPONENTS',
|
|
529
|
+
),
|
|
474
530
|
refundDispositionsByOperationId,
|
|
475
531
|
previousPassengerCount: changeFlowInitialTicketCount,
|
|
476
532
|
previousAvailabilityId: initialValues.availabilityId ?? null,
|
|
@@ -522,7 +578,7 @@ export function useAdminChangeQuotePreview({
|
|
|
522
578
|
// Fetching is controlled by the canonical user-input identity. Do not add quote-derived
|
|
523
579
|
// totals, receipt objects, or display lines here: a response must not trigger another quote.
|
|
524
580
|
isCustomerSelfServeChange,
|
|
525
|
-
|
|
581
|
+
canRequestChangeQuote,
|
|
526
582
|
missingRequiredReturnSelection,
|
|
527
583
|
changeQuoteInputsKey,
|
|
528
584
|
hasPublishedChangeQuote,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { AdminAmendmentActiveAdjustmentSnapshot } from '../../lib/booking-api';
|
|
2
3
|
|
|
3
4
|
export type AdminCustomReceiptLine = {
|
|
4
5
|
id: string;
|
|
@@ -6,63 +7,223 @@ export type AdminCustomReceiptLine = {
|
|
|
6
7
|
amountInput: string;
|
|
7
8
|
amountSign: 1 | -1;
|
|
8
9
|
calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
|
|
9
|
-
scope:
|
|
10
|
+
scope:
|
|
11
|
+
| 'POSITIVE_AMENDMENT_VALUE'
|
|
12
|
+
| 'CURRENT_ACTIVE_TICKETS'
|
|
13
|
+
| 'CURRENT_ACTIVE_BOOKING'
|
|
14
|
+
| 'SELECTED_COMPONENTS';
|
|
15
|
+
selectedComponentIds: string[];
|
|
10
16
|
taxBehavior: 'NON_TAXABLE' | 'TAXABLE' | 'PRE_TAX_DISCOUNT' | 'POST_TAX_DISCOUNT';
|
|
11
17
|
reasonCode: string;
|
|
18
|
+
/** Immutable ledger id. Existing lines are edited by reversing this id and adding a replacement. */
|
|
19
|
+
sourceAdjustmentId?: string;
|
|
20
|
+
replacementAdjustmentId?: string;
|
|
21
|
+
isDirty?: boolean;
|
|
12
22
|
};
|
|
13
23
|
|
|
14
24
|
type AdminCustomReceiptLinePatch = Partial<
|
|
15
25
|
Pick<
|
|
16
26
|
AdminCustomReceiptLine,
|
|
17
|
-
|
|
27
|
+
| 'label'
|
|
28
|
+
| 'amountInput'
|
|
29
|
+
| 'amountSign'
|
|
30
|
+
| 'calculation'
|
|
31
|
+
| 'scope'
|
|
32
|
+
| 'selectedComponentIds'
|
|
33
|
+
| 'taxBehavior'
|
|
34
|
+
| 'reasonCode'
|
|
18
35
|
>
|
|
19
36
|
>;
|
|
20
37
|
|
|
21
|
-
export function
|
|
38
|
+
export function lineFromActiveAdjustment(
|
|
39
|
+
adjustment: AdminAmendmentActiveAdjustmentSnapshot,
|
|
40
|
+
replacementAdjustmentId: string,
|
|
41
|
+
): AdminCustomReceiptLine {
|
|
42
|
+
const percentage = adjustment.mode !== 'FIXED_AMOUNT';
|
|
43
|
+
return {
|
|
44
|
+
id: `existing:${adjustment.adjustmentId}`,
|
|
45
|
+
label:
|
|
46
|
+
adjustment.reason?.trim() ||
|
|
47
|
+
adjustment.referenceCode?.trim() ||
|
|
48
|
+
adjustment.reasonCode.replace(/_/g, ' ').toLowerCase(),
|
|
49
|
+
amountInput: percentage
|
|
50
|
+
? String(Math.abs(adjustment.percentageBasisPoints ?? 0) / 100)
|
|
51
|
+
: (Math.abs(adjustment.amountCents) / 100).toFixed(2),
|
|
52
|
+
amountSign: adjustment.amountCents < 0 ? -1 : 1,
|
|
53
|
+
calculation: percentage ? 'PERCENTAGE' : 'FIXED_AMOUNT',
|
|
54
|
+
scope: adjustment.scope,
|
|
55
|
+
selectedComponentIds:
|
|
56
|
+
adjustment.scope === 'SELECTED_COMPONENTS' ? adjustment.affectedComponentIds : [],
|
|
57
|
+
taxBehavior: adjustment.taxBehavior,
|
|
58
|
+
reasonCode: adjustment.reasonCode,
|
|
59
|
+
sourceAdjustmentId: adjustment.adjustmentId,
|
|
60
|
+
replacementAdjustmentId,
|
|
61
|
+
isDirty: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function reverseAdjustmentIdsForAdminCustomLines(
|
|
66
|
+
lines: AdminCustomReceiptLine[],
|
|
67
|
+
removedExistingAdjustmentIds: string[] = [],
|
|
68
|
+
): string[] {
|
|
69
|
+
return Array.from(new Set([
|
|
70
|
+
...removedExistingAdjustmentIds,
|
|
71
|
+
...lines.flatMap((line) =>
|
|
72
|
+
line.sourceAdjustmentId && line.isDirty ? [line.sourceAdjustmentId] : [],
|
|
73
|
+
),
|
|
74
|
+
])).sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function useAdminCustomReceiptLines(
|
|
78
|
+
resetKey: string | null | undefined,
|
|
79
|
+
reversibleAdjustments: AdminAmendmentActiveAdjustmentSnapshot[] | undefined,
|
|
80
|
+
) {
|
|
22
81
|
const [adminCustomReceiptLines, setAdminCustomReceiptLines] = useState<AdminCustomReceiptLine[]>([]);
|
|
82
|
+
const [appliedAdminCustomReceiptLines, setAppliedAdminCustomReceiptLines] = useState<
|
|
83
|
+
AdminCustomReceiptLine[]
|
|
84
|
+
>([]);
|
|
85
|
+
const [removedExistingAdjustmentIds, setRemovedExistingAdjustmentIds] = useState<string[]>([]);
|
|
86
|
+
const [appliedRemovedExistingAdjustmentIds, setAppliedRemovedExistingAdjustmentIds] = useState<
|
|
87
|
+
string[]
|
|
88
|
+
>([]);
|
|
89
|
+
const [hasHydratedExistingAdjustments, setHasHydratedExistingAdjustments] = useState(false);
|
|
23
90
|
const adminCustomLineIdRef = useRef(0);
|
|
24
91
|
const previousResetKeyRef = useRef(resetKey);
|
|
92
|
+
const hydratedResetKeyRef = useRef<string | null | undefined>(undefined);
|
|
93
|
+
const skipHydrationForResetKeyRef = useRef<string | null | undefined>(undefined);
|
|
25
94
|
|
|
26
|
-
const
|
|
95
|
+
const nextDraftId = useCallback((prefix: 'admin-rcpt' | 'admin-edit') => {
|
|
27
96
|
adminCustomLineIdRef.current += 1;
|
|
97
|
+
return `${prefix}-${Date.now().toString(36)}-${adminCustomLineIdRef.current}`;
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const handleAddAdminCustomReceiptLine = useCallback(() => {
|
|
101
|
+
const id = nextDraftId('admin-rcpt');
|
|
28
102
|
setAdminCustomReceiptLines((prev) => [
|
|
29
103
|
...prev,
|
|
30
104
|
{
|
|
31
|
-
id
|
|
105
|
+
id,
|
|
32
106
|
label: '',
|
|
33
107
|
amountInput: '',
|
|
34
108
|
amountSign: 1,
|
|
35
109
|
calculation: 'FIXED_AMOUNT',
|
|
36
110
|
scope: 'POSITIVE_AMENDMENT_VALUE',
|
|
37
|
-
|
|
111
|
+
selectedComponentIds: [],
|
|
112
|
+
taxBehavior: 'TAXABLE',
|
|
38
113
|
reasonCode: 'STAFF_ADJUSTMENT',
|
|
39
114
|
},
|
|
40
115
|
]);
|
|
41
|
-
|
|
116
|
+
return id;
|
|
117
|
+
}, [nextDraftId]);
|
|
42
118
|
|
|
43
119
|
const handleUpdateAdminCustomReceiptLine = useCallback(
|
|
44
120
|
(id: string, patch: AdminCustomReceiptLinePatch) => {
|
|
45
121
|
setAdminCustomReceiptLines((prev) =>
|
|
46
|
-
prev.map((line) =>
|
|
122
|
+
prev.map((line) =>
|
|
123
|
+
line.id === id
|
|
124
|
+
? { ...line, ...patch, ...(line.sourceAdjustmentId ? { isDirty: true } : {}) }
|
|
125
|
+
: line,
|
|
126
|
+
)
|
|
47
127
|
);
|
|
48
128
|
},
|
|
49
129
|
[],
|
|
50
130
|
);
|
|
51
131
|
|
|
52
132
|
const handleRemoveAdminCustomReceiptLine = useCallback((id: string) => {
|
|
133
|
+
const removed = adminCustomReceiptLines.find((line) => line.id === id);
|
|
134
|
+
if (removed?.sourceAdjustmentId) {
|
|
135
|
+
setRemovedExistingAdjustmentIds((current) =>
|
|
136
|
+
current.includes(removed.sourceAdjustmentId!)
|
|
137
|
+
? current
|
|
138
|
+
: [...current, removed.sourceAdjustmentId!],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
53
141
|
setAdminCustomReceiptLines((prev) => prev.filter((line) => line.id !== id));
|
|
54
|
-
}, []);
|
|
142
|
+
}, [adminCustomReceiptLines]);
|
|
143
|
+
|
|
144
|
+
const handleApplyAdminCustomReceiptLines = useCallback(() => {
|
|
145
|
+
setAppliedAdminCustomReceiptLines(adminCustomReceiptLines);
|
|
146
|
+
setAppliedRemovedExistingAdjustmentIds(removedExistingAdjustmentIds);
|
|
147
|
+
}, [adminCustomReceiptLines, removedExistingAdjustmentIds]);
|
|
55
148
|
|
|
56
149
|
useEffect(() => {
|
|
57
150
|
if (previousResetKeyRef.current === resetKey) return;
|
|
58
151
|
previousResetKeyRef.current = resetKey;
|
|
59
152
|
setAdminCustomReceiptLines([]);
|
|
153
|
+
setAppliedAdminCustomReceiptLines([]);
|
|
154
|
+
setRemovedExistingAdjustmentIds([]);
|
|
155
|
+
setAppliedRemovedExistingAdjustmentIds([]);
|
|
156
|
+
setHasHydratedExistingAdjustments(false);
|
|
157
|
+
hydratedResetKeyRef.current = undefined;
|
|
158
|
+
skipHydrationForResetKeyRef.current =
|
|
159
|
+
reversibleAdjustments === undefined ? undefined : resetKey;
|
|
160
|
+
// `reversibleAdjustments` is intentionally sampled only when the reset key changes.
|
|
161
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
162
|
}, [resetKey]);
|
|
61
163
|
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (reversibleAdjustments === undefined) return;
|
|
166
|
+
// A product-family reset and clearing the previous quote happen in separate effects.
|
|
167
|
+
// Never hydrate the new editor from the previous target's quote during that transition.
|
|
168
|
+
if (skipHydrationForResetKeyRef.current === resetKey) {
|
|
169
|
+
skipHydrationForResetKeyRef.current = undefined;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (hydratedResetKeyRef.current === resetKey) return;
|
|
173
|
+
hydratedResetKeyRef.current = resetKey;
|
|
174
|
+
const hydratedLines = reversibleAdjustments.map((adjustment) =>
|
|
175
|
+
lineFromActiveAdjustment(adjustment, nextDraftId('admin-edit')),
|
|
176
|
+
);
|
|
177
|
+
setAdminCustomReceiptLines(hydratedLines);
|
|
178
|
+
setAppliedAdminCustomReceiptLines(hydratedLines);
|
|
179
|
+
setRemovedExistingAdjustmentIds([]);
|
|
180
|
+
setAppliedRemovedExistingAdjustmentIds([]);
|
|
181
|
+
setHasHydratedExistingAdjustments(true);
|
|
182
|
+
}, [nextDraftId, resetKey, reversibleAdjustments]);
|
|
183
|
+
|
|
184
|
+
const adminReverseAdjustmentIds = reverseAdjustmentIdsForAdminCustomLines(
|
|
185
|
+
appliedAdminCustomReceiptLines,
|
|
186
|
+
appliedRemovedExistingAdjustmentIds,
|
|
187
|
+
);
|
|
188
|
+
const hasPendingAdminCustomReceiptLineChanges = useMemo(
|
|
189
|
+
() =>
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
lines: adminCustomReceiptLines,
|
|
192
|
+
removedIds: [...removedExistingAdjustmentIds].sort(),
|
|
193
|
+
}) !==
|
|
194
|
+
JSON.stringify({
|
|
195
|
+
lines: appliedAdminCustomReceiptLines,
|
|
196
|
+
removedIds: [...appliedRemovedExistingAdjustmentIds].sort(),
|
|
197
|
+
}),
|
|
198
|
+
[
|
|
199
|
+
adminCustomReceiptLines,
|
|
200
|
+
appliedAdminCustomReceiptLines,
|
|
201
|
+
removedExistingAdjustmentIds,
|
|
202
|
+
appliedRemovedExistingAdjustmentIds,
|
|
203
|
+
],
|
|
204
|
+
);
|
|
205
|
+
const pendingAdminCustomReceiptLineIds = useMemo(() => {
|
|
206
|
+
const appliedLinesById = new Map(
|
|
207
|
+
appliedAdminCustomReceiptLines.map((line) => [line.id, line]),
|
|
208
|
+
);
|
|
209
|
+
return adminCustomReceiptLines.flatMap((line) => {
|
|
210
|
+
const appliedLine = appliedLinesById.get(line.id);
|
|
211
|
+
return !appliedLine || JSON.stringify(line) !== JSON.stringify(appliedLine)
|
|
212
|
+
? [line.id]
|
|
213
|
+
: [];
|
|
214
|
+
});
|
|
215
|
+
}, [adminCustomReceiptLines, appliedAdminCustomReceiptLines]);
|
|
216
|
+
|
|
62
217
|
return {
|
|
63
218
|
adminCustomReceiptLines,
|
|
219
|
+
appliedAdminCustomReceiptLines,
|
|
220
|
+
adminReverseAdjustmentIds,
|
|
221
|
+
hasPendingAdminCustomReceiptLineChanges,
|
|
222
|
+
pendingAdminCustomReceiptLineIds,
|
|
223
|
+
hasHydratedExistingAdjustments,
|
|
64
224
|
handleAddAdminCustomReceiptLine,
|
|
65
225
|
handleUpdateAdminCustomReceiptLine,
|
|
66
226
|
handleRemoveAdminCustomReceiptLine,
|
|
227
|
+
handleApplyAdminCustomReceiptLines,
|
|
67
228
|
};
|
|
68
229
|
}
|