@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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/components/booking/AdminChangeBookingContent.tsx +13 -1
  3. package/src/components/booking/AdminChangeBookingFlow.tsx +180 -65
  4. package/src/components/booking/AdminChangeCheckoutPanel.tsx +43 -13
  5. package/src/components/booking/AdminChangePricingPanel.tsx +400 -134
  6. package/src/components/booking/AdminChangeReceiptComparison.tsx +163 -145
  7. package/src/components/booking/ChangeBookingFlow.tsx +6 -3
  8. package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +7 -2
  9. package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +6 -0
  10. package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +80 -0
  11. package/src/components/booking/PickupLocationSelector.module.css +22 -0
  12. package/src/components/booking/PickupLocationSelector.tsx +3 -3
  13. package/src/components/booking/PriceSummary.tsx +4 -0
  14. package/src/components/booking/PrivateShuttleBookingFlow.tsx +27 -13
  15. package/src/components/booking/PrivateShuttleCheckoutSection.tsx +95 -54
  16. package/src/components/booking/admin-adjustment-components.ts +44 -0
  17. package/src/components/booking/admin-adjustment-tax-behavior.ts +50 -0
  18. package/src/components/booking/admin-change-flow-state-helpers.ts +10 -0
  19. package/src/components/booking/admin-change-provider-payload.ts +3 -0
  20. package/src/components/booking/admin-change-quote-request-key.ts +27 -0
  21. package/src/components/booking/admin-change-v2-quote-request.ts +10 -2
  22. package/src/components/booking/admin-refund-decision-context.ts +37 -0
  23. package/src/components/booking/admin-refund-disposition.ts +71 -2
  24. package/src/components/booking/booking-flow-types.ts +4 -0
  25. package/src/components/booking/booking-flow.css +5 -0
  26. package/src/components/booking/change-booking-error-message.ts +137 -0
  27. package/src/components/booking/change-booking-flow-helpers.ts +77 -2
  28. package/src/components/booking/change-booking-quote-guards.ts +4 -1
  29. package/src/components/booking/change-booking-quote-state.ts +44 -1
  30. package/src/components/booking/incompatible-add-on-selections.ts +19 -0
  31. package/src/components/booking/private-shuttle-availability.ts +14 -0
  32. package/src/components/booking/private-shuttle-cancellation-policy.ts +9 -0
  33. package/src/components/booking/private-shuttle-checkout-controller.ts +5 -0
  34. package/src/components/booking/private-shuttle-provider-change-runner.ts +21 -0
  35. package/src/components/booking/private-shuttle-reservation-runner.ts +5 -3
  36. package/src/components/booking/provider-dashboard-change-booking.ts +16 -1
  37. package/src/components/booking/useAdminChangeCheckoutController.ts +16 -0
  38. package/src/components/booking/useAdminChangeProductReset.ts +6 -0
  39. package/src/components/booking/useAdminChangeProtectedPricing.ts +4 -0
  40. package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +6 -4
  41. package/src/components/booking/useAdminChangeQuotePreview.ts +76 -20
  42. package/src/components/booking/useAdminCustomReceiptLines.ts +171 -10
  43. package/src/components/booking/useAdminProviderPricingAdjustments.ts +50 -19
  44. package/src/components/booking/useBookingAvailabilityAddOns.ts +19 -2
  45. package/src/components/booking/useChangeBookingAddOnFloorController.ts +15 -9
  46. package/src/components/booking/useChangeBookingInitialHydration.ts +10 -7
  47. package/src/components/booking/useChangeBookingQuotePreview.ts +3 -1
  48. package/src/components/booking/useChangeBookingSelectionDetails.ts +23 -17
  49. package/src/lib/booking/change-booking-server-preview.ts +26 -9
  50. package/src/lib/booking/change-flow-pricing.ts +12 -0
  51. package/src/lib/booking/i18n/messages/en.json +1 -0
  52. package/src/lib/booking/i18n/messages/fr.json +1 -0
  53. package/src/lib/booking-api.ts +64 -0
  54. package/test/change-booking-helpers.test.ts +853 -3
  55. 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 > 0 ||
215
- providerHasAdditionalAdjustments ||
216
- (isProviderDashboardChange && Math.abs(editableSummaryPreSubtotalDelta) >= 0.005) ||
217
- Math.abs(adminCustomAdjustmentTotal) >= 0.005;
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(setAddOns)
51
- .catch(() => setAddOns([]));
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
- isCustomerSelfServeChange: boolean;
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
- isCustomerSelfServeChange,
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 initialAddOnMinQtyByKey = useMemo(() => {
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
- }, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
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 (!isCustomerSelfServeChange) return map;
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
- }, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
65
+ }, [lockExistingAddOnQuantities, initialAddOnBaselineSelections]);
61
66
 
62
67
  const applyChangeFlowAddOnFloor = useCallback(
63
68
  (nextSelections: AddOnSelection[]) => {
64
- if (!isCustomerSelfServeChange || initialAddOnMinQtyByKey.size === 0) return nextSelections;
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
- [isCustomerSelfServeChange, initialAddOnMinQtyByKey, initialAddOnMinTotalByAddOnId]
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: Record<string, number> = {};
167
- for (const item of initialValues.bookingItems) {
168
- const key = item.category?.trim();
169
- if (!key) continue;
170
- next[key] = Math.max(0, Number(item.count) || 0);
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 checkoutFormError =
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
- initialAddOnMinQtyByKey: Map<string, number>;
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
- initialAddOnMinQtyByKey,
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 currentAddOnQtyByKey = new Map<string, number>();
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
- initialAddOnMinQtyByKey,
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 movedServiceOperationIds = new Set(
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 (movedServiceOperationIds.size === 0) return lines;
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
- !line.operationId ||
280
- !movedServiceOperationIds.has(line.operationId) ||
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} — new date price difference`,
308
+ category: `${line.category} — ${priceDifferenceLabel}`,
299
309
  ...(previousUnitAmount != null && differenceUnitAmount != null
300
310
  ? {
301
311
  unitPriceComparison: {
302
312
  previousLabel: `Existing ${ticketLabel} price`,
303
- updatedLabel: `New-date ${ticketLabel} price`,
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",