@ticketboothapp/booking 1.2.167 → 1.2.168
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 +163 -145
- package/src/components/booking/ChangeBookingFlow.tsx +6 -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
|
@@ -5,17 +5,8 @@ import type {
|
|
|
5
5
|
ProviderDashboardPricingLine,
|
|
6
6
|
} from './booking-flow-ui';
|
|
7
7
|
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
8
|
+
import type { AdminCustomReceiptLine } from './useAdminCustomReceiptLines';
|
|
8
9
|
|
|
9
|
-
type AdminCustomReceiptLine = {
|
|
10
|
-
id: string;
|
|
11
|
-
label: string;
|
|
12
|
-
amountInput: string;
|
|
13
|
-
amountSign?: number;
|
|
14
|
-
calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
|
|
15
|
-
scope: 'POSITIVE_AMENDMENT_VALUE' | 'CURRENT_ACTIVE_TICKETS' | 'CURRENT_ACTIVE_BOOKING';
|
|
16
|
-
taxBehavior: 'NON_TAXABLE' | 'TAXABLE' | 'PRE_TAX_DISCOUNT' | 'POST_TAX_DISCOUNT';
|
|
17
|
-
reasonCode: string;
|
|
18
|
-
};
|
|
19
10
|
type AdminStructuredAdjustment = NonNullable<
|
|
20
11
|
ProviderDashboardChangeBookingPayload['structuredAdjustments']
|
|
21
12
|
>[number];
|
|
@@ -26,6 +17,35 @@ type ProviderAdditionalAdjustment = NonNullable<
|
|
|
26
17
|
NonNullable<ProviderDashboardChangeBookingPayload['pricingAdjustment']>['additionalAdjustments']
|
|
27
18
|
>[number];
|
|
28
19
|
|
|
20
|
+
export interface EffectiveAdminChangeSelectionInput {
|
|
21
|
+
hasChangeSelection: boolean;
|
|
22
|
+
providerPricingOverrideCount: number;
|
|
23
|
+
providerHasAdditionalAdjustments: boolean;
|
|
24
|
+
isProviderDashboardChange: boolean;
|
|
25
|
+
editableSummaryPreSubtotalDelta: number;
|
|
26
|
+
adminStructuredAdjustmentCount: number;
|
|
27
|
+
adminReverseAdjustmentCount?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hasEffectiveAdminChangeSelection({
|
|
31
|
+
hasChangeSelection,
|
|
32
|
+
providerPricingOverrideCount,
|
|
33
|
+
providerHasAdditionalAdjustments,
|
|
34
|
+
isProviderDashboardChange,
|
|
35
|
+
editableSummaryPreSubtotalDelta,
|
|
36
|
+
adminStructuredAdjustmentCount,
|
|
37
|
+
adminReverseAdjustmentCount = 0,
|
|
38
|
+
}: EffectiveAdminChangeSelectionInput): boolean {
|
|
39
|
+
return Boolean(
|
|
40
|
+
hasChangeSelection ||
|
|
41
|
+
providerPricingOverrideCount > 0 ||
|
|
42
|
+
providerHasAdditionalAdjustments ||
|
|
43
|
+
(isProviderDashboardChange && Math.abs(editableSummaryPreSubtotalDelta) >= 0.005) ||
|
|
44
|
+
adminStructuredAdjustmentCount > 0 ||
|
|
45
|
+
adminReverseAdjustmentCount > 0,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
export interface UseAdminProviderPricingAdjustmentsParams {
|
|
30
50
|
isProviderDashboardChange: boolean;
|
|
31
51
|
showProviderPricingInlineEditor: boolean;
|
|
@@ -33,8 +53,8 @@ export interface UseAdminProviderPricingAdjustmentsParams {
|
|
|
33
53
|
providerQuotedLines: ProviderDashboardPricingLine[];
|
|
34
54
|
hasChangeSelection: boolean;
|
|
35
55
|
editableSummaryPreSubtotalDelta: number;
|
|
36
|
-
adminCustomAdjustmentTotal: number;
|
|
37
56
|
adminCustomReceiptLines: AdminCustomReceiptLine[];
|
|
57
|
+
adminReverseAdjustmentIds: string[];
|
|
38
58
|
isManualOverrideEligibleLine: (line: ProviderDashboardPricingLine) => boolean;
|
|
39
59
|
}
|
|
40
60
|
|
|
@@ -51,8 +71,8 @@ export function useAdminProviderPricingAdjustments({
|
|
|
51
71
|
providerQuotedLines,
|
|
52
72
|
hasChangeSelection,
|
|
53
73
|
editableSummaryPreSubtotalDelta,
|
|
54
|
-
adminCustomAdjustmentTotal,
|
|
55
74
|
adminCustomReceiptLines,
|
|
75
|
+
adminReverseAdjustmentIds,
|
|
56
76
|
isManualOverrideEligibleLine,
|
|
57
77
|
}: UseAdminProviderPricingAdjustmentsParams) {
|
|
58
78
|
const [adjustmentIdNamespace] = useState(() =>
|
|
@@ -127,6 +147,7 @@ export function useAdminProviderPricingAdjustments({
|
|
|
127
147
|
const adminCustomLinesAsAdditionalAdjustments = useMemo(
|
|
128
148
|
(): ProviderAdditionalAdjustment[] =>
|
|
129
149
|
adminCustomReceiptLines.flatMap((line) => {
|
|
150
|
+
if (line.sourceAdjustmentId) return [];
|
|
130
151
|
const raw = line.amountInput.trim();
|
|
131
152
|
if (raw === '' || raw === '+' || raw === '-') return [];
|
|
132
153
|
const n = Number(raw);
|
|
@@ -142,20 +163,26 @@ export function useAdminProviderPricingAdjustments({
|
|
|
142
163
|
const adminStructuredAdjustments = useMemo(
|
|
143
164
|
(): AdminStructuredAdjustment[] => {
|
|
144
165
|
const customAdjustments = adminCustomReceiptLines.flatMap((line) => {
|
|
166
|
+
if (line.sourceAdjustmentId && !line.isDirty) return [];
|
|
145
167
|
const value = Number(line.amountInput.trim());
|
|
146
168
|
if (!Number.isFinite(value) || value <= 0) return [];
|
|
169
|
+
if (line.scope === 'SELECTED_COMPONENTS' && line.selectedComponentIds.length === 0) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
147
172
|
const discount = (line.amountSign ?? 1) < 0;
|
|
148
173
|
const percentage = line.calculation === 'PERCENTAGE';
|
|
149
174
|
const mode: AdminStructuredAdjustment['mode'] = percentage
|
|
150
175
|
? discount ? 'PERCENTAGE_DISCOUNT' : 'PERCENTAGE_SURCHARGE'
|
|
151
176
|
: 'FIXED_AMOUNT';
|
|
152
177
|
return [{
|
|
153
|
-
adjustmentId: line.id,
|
|
178
|
+
adjustmentId: line.replacementAdjustmentId ?? line.id,
|
|
154
179
|
mode,
|
|
155
180
|
...(percentage
|
|
156
181
|
? { percentageBasisPoints: Math.round(value * 100) }
|
|
157
182
|
: { fixedAmount: roundMoney((discount ? -1 : 1) * value) }),
|
|
158
183
|
scope: line.scope,
|
|
184
|
+
selectedComponentIds:
|
|
185
|
+
line.scope === 'SELECTED_COMPONENTS' ? line.selectedComponentIds : [],
|
|
159
186
|
taxBehavior: line.taxBehavior,
|
|
160
187
|
reasonCode: line.reasonCode.trim() || 'STAFF_ADJUSTMENT',
|
|
161
188
|
reason: line.label.trim() || null,
|
|
@@ -209,12 +236,15 @@ export function useAdminProviderPricingAdjustments({
|
|
|
209
236
|
[providerAdditionalAdjustments],
|
|
210
237
|
);
|
|
211
238
|
|
|
212
|
-
const hasEffectiveChangeSelection =
|
|
213
|
-
hasChangeSelection
|
|
214
|
-
providerPricingOverrides.length
|
|
215
|
-
providerHasAdditionalAdjustments
|
|
216
|
-
|
|
217
|
-
|
|
239
|
+
const hasEffectiveChangeSelection = hasEffectiveAdminChangeSelection({
|
|
240
|
+
hasChangeSelection,
|
|
241
|
+
providerPricingOverrideCount: providerPricingOverrides.length,
|
|
242
|
+
providerHasAdditionalAdjustments,
|
|
243
|
+
isProviderDashboardChange,
|
|
244
|
+
editableSummaryPreSubtotalDelta,
|
|
245
|
+
adminStructuredAdjustmentCount: adminStructuredAdjustments.length,
|
|
246
|
+
adminReverseAdjustmentCount: adminReverseAdjustmentIds.length,
|
|
247
|
+
});
|
|
218
248
|
|
|
219
249
|
return {
|
|
220
250
|
providerTotalsPreview,
|
|
@@ -223,5 +253,6 @@ export function useAdminProviderPricingAdjustments({
|
|
|
223
253
|
adminCustomLinesAsAdditionalAdjustments,
|
|
224
254
|
mergedProviderAdditionalAdjustments,
|
|
225
255
|
adminStructuredAdjustments,
|
|
256
|
+
adminReverseAdjustmentIds,
|
|
226
257
|
};
|
|
227
258
|
}
|
|
@@ -16,6 +16,7 @@ export interface UseBookingAvailabilityAddOnsResult {
|
|
|
16
16
|
addOns: AddOn[];
|
|
17
17
|
setAddOns: Dispatch<SetStateAction<AddOn[]>>;
|
|
18
18
|
availabilityProductOptionId: string | null;
|
|
19
|
+
loadedProductOptionId: string | null;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function useBookingAvailabilityAddOns({
|
|
@@ -24,6 +25,7 @@ export function useBookingAvailabilityAddOns({
|
|
|
24
25
|
onProductOptionChange,
|
|
25
26
|
}: UseBookingAvailabilityAddOnsParams): UseBookingAvailabilityAddOnsResult {
|
|
26
27
|
const [addOns, setAddOns] = useState<AddOn[]>([]);
|
|
28
|
+
const [loadedProductOptionId, setLoadedProductOptionId] = useState<string | null>(null);
|
|
27
29
|
const previousAvailabilityProductOptionIdRef = useRef<string | null>(null);
|
|
28
30
|
const availabilityProductOptionId = useMemo(
|
|
29
31
|
() => (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) || null : null),
|
|
@@ -32,8 +34,10 @@ export function useBookingAvailabilityAddOns({
|
|
|
32
34
|
const availabilityDateTime = selectedAvailability?.dateTime?.trim() || null;
|
|
33
35
|
|
|
34
36
|
useEffect(() => {
|
|
37
|
+
let cancelled = false;
|
|
35
38
|
if (!availabilityProductOptionId || !companyId) {
|
|
36
39
|
setAddOns([]);
|
|
40
|
+
setLoadedProductOptionId(null);
|
|
37
41
|
onProductOptionChange?.();
|
|
38
42
|
return;
|
|
39
43
|
}
|
|
@@ -42,18 +46,31 @@ export function useBookingAvailabilityAddOns({
|
|
|
42
46
|
onProductOptionChange?.();
|
|
43
47
|
previousAvailabilityProductOptionIdRef.current = availabilityProductOptionId;
|
|
44
48
|
}
|
|
49
|
+
setLoadedProductOptionId(null);
|
|
45
50
|
getAddOns(companyId, {
|
|
46
51
|
productOptionId: availabilityProductOptionId,
|
|
47
52
|
preCheckout: true,
|
|
48
53
|
dateTime: availabilityDateTime ?? undefined,
|
|
49
54
|
})
|
|
50
|
-
.then(
|
|
51
|
-
|
|
55
|
+
.then((rows) => {
|
|
56
|
+
if (cancelled) return;
|
|
57
|
+
setAddOns(rows);
|
|
58
|
+
setLoadedProductOptionId(availabilityProductOptionId);
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {
|
|
61
|
+
if (cancelled) return;
|
|
62
|
+
setAddOns([]);
|
|
63
|
+
setLoadedProductOptionId(availabilityProductOptionId);
|
|
64
|
+
});
|
|
65
|
+
return () => {
|
|
66
|
+
cancelled = true;
|
|
67
|
+
};
|
|
52
68
|
}, [availabilityProductOptionId, availabilityDateTime, companyId, onProductOptionChange]);
|
|
53
69
|
|
|
54
70
|
return {
|
|
55
71
|
addOns,
|
|
56
72
|
setAddOns,
|
|
57
73
|
availabilityProductOptionId,
|
|
74
|
+
loadedProductOptionId,
|
|
58
75
|
};
|
|
59
76
|
}
|
|
@@ -13,7 +13,7 @@ interface UseChangeBookingAddOnFloorControllerOptions {
|
|
|
13
13
|
initialValues?: ChangeBookingFlowProps['initialValues'];
|
|
14
14
|
originalReceiptLineItems?: NonNullable<ChangeBookingFlowProps['originalReceipt']>['lineItems'];
|
|
15
15
|
addOns: AddOn[];
|
|
16
|
-
|
|
16
|
+
lockExistingAddOnQuantities: boolean;
|
|
17
17
|
setAddOnSelections: Dispatch<SetStateAction<AddOnSelection[]>>;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -21,9 +21,10 @@ export function useChangeBookingAddOnFloorController({
|
|
|
21
21
|
initialValues,
|
|
22
22
|
originalReceiptLineItems,
|
|
23
23
|
addOns,
|
|
24
|
-
|
|
24
|
+
lockExistingAddOnQuantities,
|
|
25
25
|
setAddOnSelections,
|
|
26
26
|
}: UseChangeBookingAddOnFloorControllerOptions): {
|
|
27
|
+
initialAddOnQtyByKey: Map<string, number>;
|
|
27
28
|
initialAddOnMinQtyByKey: Map<string, number>;
|
|
28
29
|
initialAddOnMinTotalByAddOnId: Map<string, number>;
|
|
29
30
|
updateAddOnSelections: (updater: AddOnSelectionsUpdater) => void;
|
|
@@ -39,29 +40,33 @@ export function useChangeBookingAddOnFloorController({
|
|
|
39
40
|
return [] as AddOnSelection[];
|
|
40
41
|
}, [initialValues?.addOnSelections, originalReceiptLineItems, addOns]);
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
+
const initialAddOnQtyByKey = useMemo(() => {
|
|
43
44
|
const map = new Map<string, number>();
|
|
44
|
-
if (!isCustomerSelfServeChange) return map;
|
|
45
45
|
for (const sel of initialAddOnBaselineSelections) {
|
|
46
46
|
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
47
47
|
map.set(key, (map.get(key) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
48
48
|
}
|
|
49
49
|
return map;
|
|
50
|
-
}, [
|
|
50
|
+
}, [initialAddOnBaselineSelections]);
|
|
51
|
+
|
|
52
|
+
const initialAddOnMinQtyByKey = useMemo(
|
|
53
|
+
() => (lockExistingAddOnQuantities ? initialAddOnQtyByKey : new Map<string, number>()),
|
|
54
|
+
[lockExistingAddOnQuantities, initialAddOnQtyByKey]
|
|
55
|
+
);
|
|
51
56
|
|
|
52
57
|
const initialAddOnMinTotalByAddOnId = useMemo(() => {
|
|
53
58
|
const map = new Map<string, number>();
|
|
54
|
-
if (!
|
|
59
|
+
if (!lockExistingAddOnQuantities) return map;
|
|
55
60
|
for (const sel of initialAddOnBaselineSelections) {
|
|
56
61
|
const addOnId = sel.addOnId.trim();
|
|
57
62
|
map.set(addOnId, (map.get(addOnId) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
58
63
|
}
|
|
59
64
|
return map;
|
|
60
|
-
}, [
|
|
65
|
+
}, [lockExistingAddOnQuantities, initialAddOnBaselineSelections]);
|
|
61
66
|
|
|
62
67
|
const applyChangeFlowAddOnFloor = useCallback(
|
|
63
68
|
(nextSelections: AddOnSelection[]) => {
|
|
64
|
-
if (!
|
|
69
|
+
if (!lockExistingAddOnQuantities || initialAddOnMinQtyByKey.size === 0) return nextSelections;
|
|
65
70
|
const qtyByKey = new Map<string, number>();
|
|
66
71
|
for (const sel of nextSelections) {
|
|
67
72
|
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
@@ -88,7 +93,7 @@ export function useChangeBookingAddOnFloorController({
|
|
|
88
93
|
return { addOnId, variantId, quantity: qty };
|
|
89
94
|
});
|
|
90
95
|
},
|
|
91
|
-
[
|
|
96
|
+
[lockExistingAddOnQuantities, initialAddOnMinQtyByKey, initialAddOnMinTotalByAddOnId]
|
|
92
97
|
);
|
|
93
98
|
|
|
94
99
|
const updateAddOnSelections = useCallback(
|
|
@@ -102,6 +107,7 @@ export function useChangeBookingAddOnFloorController({
|
|
|
102
107
|
);
|
|
103
108
|
|
|
104
109
|
return {
|
|
110
|
+
initialAddOnQtyByKey,
|
|
105
111
|
initialAddOnMinQtyByKey,
|
|
106
112
|
initialAddOnMinTotalByAddOnId,
|
|
107
113
|
updateAddOnSelections,
|
|
@@ -6,6 +6,7 @@ import type { AddOn, Availability, PrecomputedPricesByCategory } from '../../lib
|
|
|
6
6
|
import type { Currency } from './CurrencySwitcher';
|
|
7
7
|
import type { ChangeBookingFlowProps } from './booking-flow-types';
|
|
8
8
|
import {
|
|
9
|
+
deriveCompatibleInitialBookingQuantities,
|
|
9
10
|
deriveAddOnSelectionsFromReceiptLines,
|
|
10
11
|
normalizeAddOnSelections,
|
|
11
12
|
parseAvailabilityDateTime,
|
|
@@ -23,6 +24,7 @@ export interface UseChangeBookingInitialHydrationOptions {
|
|
|
23
24
|
timesForSelectedDate: Availability[];
|
|
24
25
|
companyTimezone: string;
|
|
25
26
|
changeFlowResolvedInitialProductOptionId: string | null;
|
|
27
|
+
mostPopularProductOptionId: string | null;
|
|
26
28
|
precomputedPricesByOption: Record<string, PrecomputedPricesByCategory> | null | undefined;
|
|
27
29
|
currency: Currency;
|
|
28
30
|
originalReceiptSubtotal?: number | null;
|
|
@@ -53,6 +55,7 @@ export function useChangeBookingInitialHydration({
|
|
|
53
55
|
timesForSelectedDate,
|
|
54
56
|
companyTimezone,
|
|
55
57
|
changeFlowResolvedInitialProductOptionId,
|
|
58
|
+
mostPopularProductOptionId,
|
|
56
59
|
precomputedPricesByOption,
|
|
57
60
|
currency,
|
|
58
61
|
originalReceiptSubtotal,
|
|
@@ -130,6 +133,7 @@ export function useChangeBookingInitialHydration({
|
|
|
130
133
|
precomputedPricesByOption,
|
|
131
134
|
currency,
|
|
132
135
|
originalReceiptSubtotal ?? undefined,
|
|
136
|
+
mostPopularProductOptionId,
|
|
133
137
|
);
|
|
134
138
|
if (defer) return;
|
|
135
139
|
if (selection) {
|
|
@@ -142,6 +146,7 @@ export function useChangeBookingInitialHydration({
|
|
|
142
146
|
initialValues?.productOptionId,
|
|
143
147
|
initialValues?.bookingItems,
|
|
144
148
|
changeFlowResolvedInitialProductOptionId,
|
|
149
|
+
mostPopularProductOptionId,
|
|
145
150
|
selectedAvailability,
|
|
146
151
|
selectedDate,
|
|
147
152
|
timesForSelectedDate,
|
|
@@ -163,15 +168,13 @@ export function useChangeBookingInitialHydration({
|
|
|
163
168
|
) {
|
|
164
169
|
return;
|
|
165
170
|
}
|
|
166
|
-
const next
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
171
|
+
const next = deriveCompatibleInitialBookingQuantities(
|
|
172
|
+
initialValues.bookingItems,
|
|
173
|
+
selectedAvailability,
|
|
174
|
+
);
|
|
175
|
+
hasAppliedInitialQuantitiesRef.current = true;
|
|
172
176
|
if (Object.keys(next).length > 0) {
|
|
173
177
|
setQuantities((prev) => ({ ...prev, ...next }));
|
|
174
|
-
hasAppliedInitialQuantitiesRef.current = true;
|
|
175
178
|
}
|
|
176
179
|
}, [initialValues, selectedAvailability, setQuantities]);
|
|
177
180
|
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
formatTicketLineItemsForSummary,
|
|
25
25
|
getAvailabilityOptionId,
|
|
26
26
|
} from './change-booking-flow-helpers';
|
|
27
|
+
import { friendlyChangeBookingError } from './change-booking-error-message';
|
|
27
28
|
import type { Currency } from './CurrencySwitcher';
|
|
28
29
|
|
|
29
30
|
type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
|
|
@@ -197,11 +198,12 @@ export function useChangeBookingQuotePreview({
|
|
|
197
198
|
: 'Change booking (no charge)';
|
|
198
199
|
})();
|
|
199
200
|
|
|
200
|
-
const
|
|
201
|
+
const rawCheckoutFormError =
|
|
201
202
|
(error || '') ||
|
|
202
203
|
(missingRequiredReturnSelection ? 'Removing return option in self-serve is not available. Please contact support.' : '') ||
|
|
203
204
|
(isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
|
|
204
205
|
(changeQuoteFetchError ?? '');
|
|
206
|
+
const checkoutFormError = friendlyChangeBookingError(rawCheckoutFormError);
|
|
205
207
|
|
|
206
208
|
const changeFlowSelectionPreview = useMemo((): ChangeFlowSelectionPreview | null => {
|
|
207
209
|
if (!selectedAvailability || totalQuantity <= 0) return null;
|
|
@@ -30,7 +30,7 @@ export interface UseChangeBookingSelectionDetailsParams {
|
|
|
30
30
|
pickupLocationId: string | null;
|
|
31
31
|
quantities: Record<string, number>;
|
|
32
32
|
addOnSelections: AddOnSelection[];
|
|
33
|
-
|
|
33
|
+
initialAddOnQtyByKey: Map<string, number>;
|
|
34
34
|
changeFlowResolvedInitialProductOptionId: string | null;
|
|
35
35
|
productChanged?: boolean;
|
|
36
36
|
}
|
|
@@ -65,6 +65,25 @@ function normalizeCounts(items: Array<{ category: string; count: number }> | nul
|
|
|
65
65
|
return map;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export function haveAddOnSelectionsChanged(
|
|
69
|
+
initialAddOnQtyByKey: Map<string, number>,
|
|
70
|
+
addOnSelections: AddOnSelection[],
|
|
71
|
+
): boolean {
|
|
72
|
+
const currentAddOnQtyByKey = new Map<string, number>();
|
|
73
|
+
for (const sel of addOnSelections) {
|
|
74
|
+
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
75
|
+
currentAddOnQtyByKey.set(key, (currentAddOnQtyByKey.get(key) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const allAddOnKeys = new Set([...initialAddOnQtyByKey.keys(), ...currentAddOnQtyByKey.keys()]);
|
|
79
|
+
for (const key of allAddOnKeys) {
|
|
80
|
+
if ((initialAddOnQtyByKey.get(key) ?? 0) !== (currentAddOnQtyByKey.get(key) ?? 0)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
export function useChangeBookingSelectionDetails({
|
|
69
88
|
initialValues,
|
|
70
89
|
selectedAvailability,
|
|
@@ -73,7 +92,7 @@ export function useChangeBookingSelectionDetails({
|
|
|
73
92
|
pickupLocationId,
|
|
74
93
|
quantities,
|
|
75
94
|
addOnSelections,
|
|
76
|
-
|
|
95
|
+
initialAddOnQtyByKey,
|
|
77
96
|
changeFlowResolvedInitialProductOptionId,
|
|
78
97
|
productChanged = false,
|
|
79
98
|
}: UseChangeBookingSelectionDetailsParams): ChangeBookingSelectionDetails {
|
|
@@ -115,20 +134,7 @@ export function useChangeBookingSelectionDetails({
|
|
|
115
134
|
}
|
|
116
135
|
}
|
|
117
136
|
|
|
118
|
-
const
|
|
119
|
-
for (const sel of addOnSelections) {
|
|
120
|
-
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
121
|
-
currentAddOnQtyByKey.set(key, (currentAddOnQtyByKey.get(key) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const allAddOnKeys = new Set([...initialAddOnMinQtyByKey.keys(), ...currentAddOnQtyByKey.keys()]);
|
|
125
|
-
let addOnsChanged = false;
|
|
126
|
-
for (const key of allAddOnKeys) {
|
|
127
|
-
if ((initialAddOnMinQtyByKey.get(key) ?? 0) !== (currentAddOnQtyByKey.get(key) ?? 0)) {
|
|
128
|
-
addOnsChanged = true;
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
137
|
+
const addOnsChanged = haveAddOnSelectionsChanged(initialAddOnQtyByKey, addOnSelections);
|
|
132
138
|
|
|
133
139
|
const hasReturnOptions = (selectedAvailability?.returnOptions?.length ?? 0) > 0;
|
|
134
140
|
const initialReturnId = initialValues.returnAvailabilityId?.trim() || null;
|
|
@@ -193,7 +199,7 @@ export function useChangeBookingSelectionDetails({
|
|
|
193
199
|
pickupLocationId,
|
|
194
200
|
quantities,
|
|
195
201
|
addOnSelections,
|
|
196
|
-
|
|
202
|
+
initialAddOnQtyByKey,
|
|
197
203
|
changeFlowResolvedInitialProductOptionId,
|
|
198
204
|
productChanged,
|
|
199
205
|
]);
|
|
@@ -27,6 +27,8 @@ export interface ChangeBookingServerPreview {
|
|
|
27
27
|
completeness: ChangeBookingPreviewCompleteness;
|
|
28
28
|
/** Rows for {@link PriceSummary} / checkout — from quote receipt line items + BE extensions. */
|
|
29
29
|
priceSummaryLines: PriceSummaryLine[];
|
|
30
|
+
/** Full resulting receipt rows, when the amendment quote supplies them. */
|
|
31
|
+
resultingReceiptLines?: PriceSummaryLine[];
|
|
30
32
|
/** From quote — per-category unit prices (major units) for ticket picker when BE sends them. */
|
|
31
33
|
ticketUnitPriceByCategory?: Record<string, number>;
|
|
32
34
|
cancellationPolicyFeeByPolicyId?: Record<string, number>;
|
|
@@ -260,28 +262,36 @@ export function labelAdminAmendmentPriceSummaryLines(
|
|
|
260
262
|
operations: readonly AdminAmendmentOperationSnapshot[],
|
|
261
263
|
originalLines: readonly PriceSummaryLine[] = [],
|
|
262
264
|
): PriceSummaryLine[] {
|
|
263
|
-
const
|
|
265
|
+
const movedServiceOperations = new Map(
|
|
264
266
|
operations
|
|
265
267
|
.filter((operation) => operation.type.trim().toUpperCase() === 'MOVE_SERVICE')
|
|
266
|
-
.map((operation) => operation.operationId),
|
|
268
|
+
.map((operation) => [operation.operationId, operation] as const),
|
|
267
269
|
);
|
|
268
|
-
if (
|
|
270
|
+
if (movedServiceOperations.size === 0) return lines;
|
|
269
271
|
|
|
270
272
|
const normalizedTicketCategory = (category: string) => category
|
|
271
273
|
.replace(/^refund\s*[·:-]\s*/i, '')
|
|
272
|
-
.replace(/\s*[—-]\s*new date price difference\s*$/i, '')
|
|
274
|
+
.replace(/\s*[—-]\s*(?:new date|tour) price difference\s*$/i, '')
|
|
273
275
|
.trim()
|
|
274
276
|
.toUpperCase();
|
|
275
277
|
|
|
276
278
|
return lines.map((line) => {
|
|
279
|
+
const moveOperation = line.kind === 'ticket' && line.operationId
|
|
280
|
+
? movedServiceOperations.get(line.operationId)
|
|
281
|
+
: undefined;
|
|
277
282
|
if (
|
|
278
283
|
line.kind !== 'ticket' ||
|
|
279
|
-
!
|
|
280
|
-
|
|
281
|
-
line.category.toLowerCase().includes('new date price difference')
|
|
284
|
+
!moveOperation ||
|
|
285
|
+
/(?:new date|tour) price difference/i.test(line.category)
|
|
282
286
|
) {
|
|
283
287
|
return line;
|
|
284
288
|
}
|
|
289
|
+
const changesTour = moveOperation.changedFields?.some(
|
|
290
|
+
(field) => field.trim().toLowerCase() === 'productid',
|
|
291
|
+
) ?? false;
|
|
292
|
+
const priceDifferenceLabel = changesTour
|
|
293
|
+
? 'tour price difference'
|
|
294
|
+
: 'new date price difference';
|
|
285
295
|
const originalLine = originalLines.find(
|
|
286
296
|
(candidate) => candidate.kind === 'ticket' &&
|
|
287
297
|
normalizedTicketCategory(candidate.category) === normalizedTicketCategory(line.category),
|
|
@@ -295,12 +305,14 @@ export function labelAdminAmendmentPriceSummaryLines(
|
|
|
295
305
|
const ticketLabel = normalizedTicketCategory(line.category).toLowerCase();
|
|
296
306
|
return {
|
|
297
307
|
...line,
|
|
298
|
-
category: `${line.category} —
|
|
308
|
+
category: `${line.category} — ${priceDifferenceLabel}`,
|
|
299
309
|
...(previousUnitAmount != null && differenceUnitAmount != null
|
|
300
310
|
? {
|
|
301
311
|
unitPriceComparison: {
|
|
302
312
|
previousLabel: `Existing ${ticketLabel} price`,
|
|
303
|
-
updatedLabel:
|
|
313
|
+
updatedLabel: changesTour
|
|
314
|
+
? `New-tour ${ticketLabel} price`
|
|
315
|
+
: `New-date ${ticketLabel} price`,
|
|
304
316
|
differenceLabel: `Difference per ${ticketLabel}`,
|
|
305
317
|
previousUnitAmount,
|
|
306
318
|
updatedUnitAmount: roundMoney(previousUnitAmount + differenceUnitAmount),
|
|
@@ -354,6 +366,10 @@ export function buildChangeBookingServerPreview(
|
|
|
354
366
|
),
|
|
355
367
|
paymentCreditTotal,
|
|
356
368
|
);
|
|
369
|
+
const resultingReceiptLines = pricingQuote?.amendment?.updatedReceipt?.lineItems;
|
|
370
|
+
const resultingReceiptPriceSummaryLines = resultingReceiptLines?.length
|
|
371
|
+
? mapQuoteLineItemsToPriceSummaryLines(resultingReceiptLines)
|
|
372
|
+
: undefined;
|
|
357
373
|
|
|
358
374
|
const completeness: ChangeBookingPreviewCompleteness = priceSummaryLines.length > 0 ? 'full' : 'totals_only';
|
|
359
375
|
|
|
@@ -365,6 +381,7 @@ export function buildChangeBookingServerPreview(
|
|
|
365
381
|
totalNewBooking: totals.total,
|
|
366
382
|
completeness,
|
|
367
383
|
priceSummaryLines,
|
|
384
|
+
resultingReceiptLines: resultingReceiptPriceSummaryLines,
|
|
368
385
|
ticketUnitPriceByCategory: quote.ticketUnitPriceByCategory,
|
|
369
386
|
cancellationPolicyFeeByPolicyId: quote.cancellationPolicyFeeByPolicyId,
|
|
370
387
|
returnOptionPriceByReturnAvailabilityId: quote.returnOptionPriceByReturnAvailabilityId,
|
|
@@ -255,6 +255,12 @@ export interface ChangeQuoteUiSlice {
|
|
|
255
255
|
quoteExpiresAt?: string;
|
|
256
256
|
pricingVersion?: string;
|
|
257
257
|
quotedTotal?: number;
|
|
258
|
+
/** Settled cash after completed refunds, calculated by the backend from payment records. */
|
|
259
|
+
amountPreviouslyPaid?: number;
|
|
260
|
+
/** Non-cash value allocated before and after this amendment. */
|
|
261
|
+
currentPaymentCreditTotal?: number;
|
|
262
|
+
projectedPaymentCreditTotal?: number;
|
|
263
|
+
refundCandidate?: number;
|
|
258
264
|
paymentCreditTotal?: number;
|
|
259
265
|
refundDecisionOperations?: ChangeBookingQuoteResponse['refundDecisionOperations'];
|
|
260
266
|
returnPriceTreatmentOperations?: ChangeBookingQuoteResponse['returnPriceTreatmentOperations'];
|
|
@@ -296,6 +302,12 @@ export function sliceChangeQuoteForUi(
|
|
|
296
302
|
quoteExpiresAt: pricingQuote?.expiresAt ?? quote.expiresAt,
|
|
297
303
|
pricingVersion: pricingQuote?.pricingVersion ?? undefined,
|
|
298
304
|
quotedTotal: pricingQuote?.payableTotal ?? pricingQuote?.totalAmount ?? quote.proposed?.total ?? quote.newReceipt?.total,
|
|
305
|
+
amountPreviouslyPaid:
|
|
306
|
+
pricingQuote?.amountPreviouslyPaid ?? quote.amountPreviouslyPaid ?? undefined,
|
|
307
|
+
currentPaymentCreditTotal: quote.originalReceipt?.paymentCreditTotal ?? 0,
|
|
308
|
+
projectedPaymentCreditTotal:
|
|
309
|
+
pricingQuote?.paymentCreditTotal ?? quote.paymentCreditTotal ?? 0,
|
|
310
|
+
refundCandidate: quote.refundCandidate ?? pricingQuote?.refundCandidate ?? 0,
|
|
299
311
|
paymentCreditTotal: pricingQuote?.paymentCreditTotal ?? quote.paymentCreditTotal,
|
|
300
312
|
refundDecisionOperations: quote.refundDecisionOperations,
|
|
301
313
|
returnPriceTreatmentOperations: quote.returnPriceTreatmentOperations,
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"rounding": "Rounding",
|
|
120
120
|
"deposit": "Deposit",
|
|
121
121
|
"totalOwedForBookingChange": "Total owed for booking difference",
|
|
122
|
+
"changeToBookingTotal": "Change to booking total",
|
|
122
123
|
"subtotal": "Subtotal",
|
|
123
124
|
"tax": "Taxes and fees",
|
|
124
125
|
"returnOption": "Return Option",
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"rounding": "Arrondi",
|
|
120
120
|
"deposit": "Acompte",
|
|
121
121
|
"totalOwedForBookingChange": "Montant dû pour le changement de réservation",
|
|
122
|
+
"changeToBookingTotal": "Modification du total de la réservation",
|
|
122
123
|
"subtotal": "Sous-total",
|
|
123
124
|
"tax": "Taxes et frais",
|
|
124
125
|
"returnOption": "Option de retour",
|