@ticketboothapp/booking 1.2.166 → 1.2.167
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 +2 -0
- package/src/components/booking/AdminChangeBookingFlow.tsx +95 -30
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +6 -0
- package/src/components/booking/AdminChangePromoEditor.tsx +97 -0
- package/src/components/booking/AdminChangeReceiptComparison.tsx +9 -2
- package/src/components/booking/NewBookingFlow.tsx +14 -2
- package/src/components/booking/PriceBreakdown.tsx +66 -15
- package/src/components/booking/PriceSummary.tsx +12 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +25 -1
- package/src/components/booking/admin-change-provider-payload.ts +9 -1
- package/src/components/booking/admin-change-quote-request-key.ts +14 -1
- package/src/components/booking/admin-change-receipt-lines.ts +49 -0
- package/src/components/booking/admin-change-v2-quote-request.ts +101 -0
- package/src/components/booking/admin-refund-decision-context.ts +85 -0
- package/src/components/booking/availability-date-selection.ts +16 -0
- package/src/components/booking/provider-dashboard-change-booking.ts +2 -1
- package/src/components/booking/use-standard-booking-availability.ts +5 -2
- package/src/components/booking/useAdminChangeCheckoutController.ts +40 -1
- package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +2 -2
- package/src/components/booking/useAdminChangeQuotePreview.ts +72 -43
- package/src/components/booking/useBookingAvailabilityAddOns.ts +7 -2
- package/src/components/booking/useBookingPromoAndQuantityController.ts +16 -2
- package/src/components/booking/useChangeBookingAutoSelections.ts +60 -17
- package/src/components/booking/useStandardBookingAutoSelections.ts +55 -7
- package/src/lib/booking/booking-cutoffs.ts +22 -0
- package/src/lib/booking/change-booking-server-preview.ts +75 -5
- package/src/lib/booking-api.ts +12 -1
- package/test/change-booking-helpers.test.ts +466 -4
|
@@ -29,6 +29,7 @@ export function useBookingAvailabilityAddOns({
|
|
|
29
29
|
() => (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) || null : null),
|
|
30
30
|
[selectedAvailability],
|
|
31
31
|
);
|
|
32
|
+
const availabilityDateTime = selectedAvailability?.dateTime?.trim() || null;
|
|
32
33
|
|
|
33
34
|
useEffect(() => {
|
|
34
35
|
if (!availabilityProductOptionId || !companyId) {
|
|
@@ -41,10 +42,14 @@ export function useBookingAvailabilityAddOns({
|
|
|
41
42
|
onProductOptionChange?.();
|
|
42
43
|
previousAvailabilityProductOptionIdRef.current = availabilityProductOptionId;
|
|
43
44
|
}
|
|
44
|
-
getAddOns(companyId, {
|
|
45
|
+
getAddOns(companyId, {
|
|
46
|
+
productOptionId: availabilityProductOptionId,
|
|
47
|
+
preCheckout: true,
|
|
48
|
+
dateTime: availabilityDateTime ?? undefined,
|
|
49
|
+
})
|
|
45
50
|
.then(setAddOns)
|
|
46
51
|
.catch(() => setAddOns([]));
|
|
47
|
-
}, [availabilityProductOptionId, companyId, onProductOptionChange]);
|
|
52
|
+
}, [availabilityProductOptionId, availabilityDateTime, companyId, onProductOptionChange]);
|
|
48
53
|
|
|
49
54
|
return {
|
|
50
55
|
addOns,
|
|
@@ -27,6 +27,9 @@ export interface UseBookingPromoAndQuantityControllerParams {
|
|
|
27
27
|
export interface UseBookingPromoAndQuantityControllerResult {
|
|
28
28
|
handleQuantityChange: (category: string, delta: number) => void;
|
|
29
29
|
handleApplyPromo: () => Promise<void>;
|
|
30
|
+
handleRemovePromo: () => void;
|
|
31
|
+
promoCodeError: string;
|
|
32
|
+
promoCodeValidating: boolean;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function useBookingPromoAndQuantityController({
|
|
@@ -47,8 +50,8 @@ export function useBookingPromoAndQuantityController({
|
|
|
47
50
|
t,
|
|
48
51
|
setError,
|
|
49
52
|
}: UseBookingPromoAndQuantityControllerParams): UseBookingPromoAndQuantityControllerResult {
|
|
50
|
-
const [, setPromoCodeError] = useState('');
|
|
51
|
-
const [, setPromoCodeValidating] = useState(false);
|
|
53
|
+
const [promoCodeError, setPromoCodeError] = useState('');
|
|
54
|
+
const [promoCodeValidating, setPromoCodeValidating] = useState(false);
|
|
52
55
|
const promoAppliedSelectionKeyRef = useRef<string | null>(null);
|
|
53
56
|
const promoValidateInFlightRef = useRef(false);
|
|
54
57
|
|
|
@@ -156,8 +159,19 @@ export function useBookingPromoAndQuantityController({
|
|
|
156
159
|
return () => clearTimeout(timer);
|
|
157
160
|
}, [promoCodeInput, appliedPromoCode, selectedAvailability, totalQuantity]);
|
|
158
161
|
|
|
162
|
+
const handleRemovePromo = useCallback(() => {
|
|
163
|
+
promoAppliedSelectionKeyRef.current = null;
|
|
164
|
+
setAppliedPromoCode(null);
|
|
165
|
+
setPromoCodeError('');
|
|
166
|
+
setPromoCodeValidating(false);
|
|
167
|
+
clearFetchedAvailabilityRanges();
|
|
168
|
+
}, [clearFetchedAvailabilityRanges, setAppliedPromoCode]);
|
|
169
|
+
|
|
159
170
|
return {
|
|
160
171
|
handleQuantityChange,
|
|
161
172
|
handleApplyPromo,
|
|
173
|
+
handleRemovePromo,
|
|
174
|
+
promoCodeError,
|
|
175
|
+
promoCodeValidating,
|
|
162
176
|
};
|
|
163
177
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, type RefObject } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, type RefObject } from 'react';
|
|
4
4
|
import type { Availability, PickupLocation } from '../../lib/booking-api';
|
|
5
|
+
import {
|
|
6
|
+
findFirstDateWithBookableAvailability,
|
|
7
|
+
selectedDateHasVisibleAvailability,
|
|
8
|
+
} from './availability-date-selection';
|
|
5
9
|
|
|
6
10
|
export interface UseChangeBookingAutoSelectionsOptions {
|
|
7
11
|
autoSelectFirstAvailableDate?: boolean;
|
|
@@ -71,6 +75,55 @@ export function useChangeBookingAutoSelections({
|
|
|
71
75
|
const hasAutoSelectedPartnerDateRef = useRef(false);
|
|
72
76
|
const hasAutoSelectedPartnerPickupRef = useRef(false);
|
|
73
77
|
|
|
78
|
+
const isBookableForAutoSelection = useCallback(
|
|
79
|
+
(availability: Availability): boolean => {
|
|
80
|
+
if (isAdmin) return (availability.vacancies ?? 0) > 0;
|
|
81
|
+
if (isCustomerSelfServeChange && changeFlowInitialTicketCount > 0) {
|
|
82
|
+
return getCalendarEffectiveOutboundVacancies(availability) >= changeFlowInitialTicketCount;
|
|
83
|
+
}
|
|
84
|
+
return (availability.vacancies ?? 0) > 0;
|
|
85
|
+
},
|
|
86
|
+
[
|
|
87
|
+
changeFlowInitialTicketCount,
|
|
88
|
+
getCalendarEffectiveOutboundVacancies,
|
|
89
|
+
isAdmin,
|
|
90
|
+
isCustomerSelfServeChange,
|
|
91
|
+
],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!autoSelectFirstAvailableDate) return;
|
|
96
|
+
if (isPartialLaunch) return;
|
|
97
|
+
if (!selectedDate || dates.length === 0) return;
|
|
98
|
+
if (selectedDateHasVisibleAvailability(selectedDate, getRowsForDate)) return;
|
|
99
|
+
|
|
100
|
+
const first = findFirstDateWithBookableAvailability(
|
|
101
|
+
dates,
|
|
102
|
+
getRowsForDate,
|
|
103
|
+
isBookableForAutoSelection,
|
|
104
|
+
);
|
|
105
|
+
if (!first || first === selectedDate) return;
|
|
106
|
+
|
|
107
|
+
hasAutoSelectedPartnerDateRef.current = true;
|
|
108
|
+
setSelectedDate(first);
|
|
109
|
+
onDateSelect(first);
|
|
110
|
+
if (!suppressCalendarDateScroll) {
|
|
111
|
+
scrollAfterCalendarSelection(contentRef, useWindowScroll);
|
|
112
|
+
}
|
|
113
|
+
}, [
|
|
114
|
+
autoSelectFirstAvailableDate,
|
|
115
|
+
isPartialLaunch,
|
|
116
|
+
selectedDate,
|
|
117
|
+
dates,
|
|
118
|
+
getRowsForDate,
|
|
119
|
+
isBookableForAutoSelection,
|
|
120
|
+
setSelectedDate,
|
|
121
|
+
onDateSelect,
|
|
122
|
+
suppressCalendarDateScroll,
|
|
123
|
+
contentRef,
|
|
124
|
+
useWindowScroll,
|
|
125
|
+
]);
|
|
126
|
+
|
|
74
127
|
useEffect(() => {
|
|
75
128
|
if (!autoSelectFirstAvailableDate) return;
|
|
76
129
|
if (isPartialLaunch) return;
|
|
@@ -79,18 +132,11 @@ export function useChangeBookingAutoSelections({
|
|
|
79
132
|
if (dates.length === 0) return;
|
|
80
133
|
if (hasAutoSelectedPartnerDateRef.current) return;
|
|
81
134
|
|
|
82
|
-
const firstWithInventory =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return rows.some(
|
|
88
|
-
(availability) =>
|
|
89
|
-
getCalendarEffectiveOutboundVacancies(availability) >= changeFlowInitialTicketCount,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
return rows.some((availability) => (availability.vacancies ?? 0) > 0);
|
|
93
|
-
});
|
|
135
|
+
const firstWithInventory = findFirstDateWithBookableAvailability(
|
|
136
|
+
dates,
|
|
137
|
+
getRowsForDate,
|
|
138
|
+
isBookableForAutoSelection,
|
|
139
|
+
);
|
|
94
140
|
const first = firstWithInventory ?? (isAdmin && dates[0] ? dates[0] : undefined);
|
|
95
141
|
if (!first) return;
|
|
96
142
|
|
|
@@ -107,10 +153,7 @@ export function useChangeBookingAutoSelections({
|
|
|
107
153
|
selectedDate,
|
|
108
154
|
dates,
|
|
109
155
|
getRowsForDate,
|
|
110
|
-
|
|
111
|
-
isCustomerSelfServeChange,
|
|
112
|
-
changeFlowInitialTicketCount,
|
|
113
|
-
getCalendarEffectiveOutboundVacancies,
|
|
156
|
+
isBookableForAutoSelection,
|
|
114
157
|
setSelectedDate,
|
|
115
158
|
onDateSelect,
|
|
116
159
|
suppressCalendarDateScroll,
|
|
@@ -3,9 +3,14 @@ import { parseISO } from 'date-fns';
|
|
|
3
3
|
import type { Availability, Product, ReturnOption } from '../../lib/booking-api';
|
|
4
4
|
import {
|
|
5
5
|
availabilityMatchesSelectedDate,
|
|
6
|
+
findMergedAvailabilityForSelection,
|
|
6
7
|
parseAvailabilityDateTime,
|
|
7
8
|
pickDefaultAvailabilityForTimes,
|
|
8
9
|
} from './standard-booking-availability';
|
|
10
|
+
import {
|
|
11
|
+
findFirstDateWithBookableAvailability,
|
|
12
|
+
selectedDateHasVisibleAvailability,
|
|
13
|
+
} from './availability-date-selection';
|
|
9
14
|
|
|
10
15
|
interface UseStandardBookingAutoSelectionsParams {
|
|
11
16
|
selectedDate: string;
|
|
@@ -91,19 +96,22 @@ export function useStandardBookingAutoSelections({
|
|
|
91
96
|
|
|
92
97
|
useEffect(() => {
|
|
93
98
|
if (!selectedDate || timesForSelectedDateLength === 0) return;
|
|
99
|
+
const eligibleTimes = getPickupCutoffEligibleTimesForDate(selectedDate);
|
|
94
100
|
if (
|
|
95
101
|
selectedAvailability &&
|
|
96
|
-
availabilityMatchesSelectedDate(selectedAvailability, selectedDate, companyTimezone)
|
|
102
|
+
availabilityMatchesSelectedDate(selectedAvailability, selectedDate, companyTimezone) &&
|
|
103
|
+
findMergedAvailabilityForSelection(eligibleTimes, selectedAvailability)
|
|
97
104
|
) {
|
|
98
105
|
return;
|
|
99
106
|
}
|
|
100
|
-
const toSelect = pickDefaultAvailabilityForTimes(
|
|
101
|
-
getPickupCutoffEligibleTimesForDate(selectedDate),
|
|
102
|
-
activeOptions,
|
|
103
|
-
);
|
|
107
|
+
const toSelect = pickDefaultAvailabilityForTimes(eligibleTimes, activeOptions);
|
|
104
108
|
if (toSelect) {
|
|
105
109
|
setSelectedAvailability(toSelect);
|
|
110
|
+
setSelectedReturnOption(null);
|
|
106
111
|
setError('');
|
|
112
|
+
} else if (selectedAvailability) {
|
|
113
|
+
setSelectedAvailability(null);
|
|
114
|
+
setSelectedReturnOption(null);
|
|
107
115
|
}
|
|
108
116
|
}, [
|
|
109
117
|
selectedDate,
|
|
@@ -114,9 +122,47 @@ export function useStandardBookingAutoSelections({
|
|
|
114
122
|
companyTimezone,
|
|
115
123
|
getPickupCutoffEligibleTimesForDate,
|
|
116
124
|
setSelectedAvailability,
|
|
125
|
+
setSelectedReturnOption,
|
|
117
126
|
setError,
|
|
118
127
|
]);
|
|
119
128
|
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!autoSelectFirstAvailableDate) return;
|
|
131
|
+
if (isPartialLaunch) return;
|
|
132
|
+
if (!selectedDate || dates.length === 0) return;
|
|
133
|
+
if (selectedDateHasVisibleAvailability(selectedDate, getPickupCutoffEligibleTimesForDate)) return;
|
|
134
|
+
|
|
135
|
+
const first = findFirstDateWithBookableAvailability(
|
|
136
|
+
dates,
|
|
137
|
+
getPickupCutoffEligibleTimesForDate,
|
|
138
|
+
(availability) => (availability.vacancies ?? 0) > 0,
|
|
139
|
+
);
|
|
140
|
+
if (!first || first === selectedDate) return;
|
|
141
|
+
|
|
142
|
+
hasAutoSelectedPartnerDateRef.current = true;
|
|
143
|
+
setSelectedDate(first);
|
|
144
|
+
setSelectedReturnOption(null);
|
|
145
|
+
setSelectedAvailability(
|
|
146
|
+
pickDefaultAvailabilityForTimes(getPickupCutoffEligibleTimesForDate(first), activeOptions),
|
|
147
|
+
);
|
|
148
|
+
if (!suppressCalendarDateScroll) {
|
|
149
|
+
scrollAfterCalendarSelection(contentRef, useWindowScroll);
|
|
150
|
+
}
|
|
151
|
+
}, [
|
|
152
|
+
autoSelectFirstAvailableDate,
|
|
153
|
+
isPartialLaunch,
|
|
154
|
+
selectedDate,
|
|
155
|
+
dates,
|
|
156
|
+
getPickupCutoffEligibleTimesForDate,
|
|
157
|
+
activeOptions,
|
|
158
|
+
setSelectedAvailability,
|
|
159
|
+
setSelectedDate,
|
|
160
|
+
setSelectedReturnOption,
|
|
161
|
+
suppressCalendarDateScroll,
|
|
162
|
+
contentRef,
|
|
163
|
+
useWindowScroll,
|
|
164
|
+
]);
|
|
165
|
+
|
|
120
166
|
useEffect(() => {
|
|
121
167
|
if (!selectedAvailability?.returnOptions?.length || selectedReturnOption) return;
|
|
122
168
|
|
|
@@ -162,8 +208,10 @@ export function useStandardBookingAutoSelections({
|
|
|
162
208
|
if (dates.length === 0) return;
|
|
163
209
|
if (hasAutoSelectedPartnerDateRef.current) return;
|
|
164
210
|
|
|
165
|
-
const firstWithInventory =
|
|
166
|
-
|
|
211
|
+
const firstWithInventory = findFirstDateWithBookableAvailability(
|
|
212
|
+
dates,
|
|
213
|
+
getPickupCutoffEligibleTimesForDate,
|
|
214
|
+
(availability) => (availability.vacancies ?? 0) > 0,
|
|
167
215
|
);
|
|
168
216
|
const first = firstWithInventory ?? (isAdmin && dates[0] ? dates[0] : undefined);
|
|
169
217
|
if (!first) return;
|
|
@@ -55,6 +55,28 @@ export function filterAvailabilitiesAfterPublicBookingCutoff<T extends { dateTim
|
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Admin surfaces intentionally ignore the public advance-booking cutoff, but an
|
|
60
|
+
* availability whose actual start time has passed is no longer bookable by the
|
|
61
|
+
* API. Keep that narrower rule separate so admin and public behavior cannot
|
|
62
|
+
* accidentally inherit one another's cutoff policy.
|
|
63
|
+
*/
|
|
64
|
+
export function isAvailabilityAfterCurrentTime(
|
|
65
|
+
availability: { dateTime: string },
|
|
66
|
+
now: Date = new Date(),
|
|
67
|
+
): boolean {
|
|
68
|
+
return isAvailabilityAfterPublicBookingCutoff(availability, now, 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function filterAvailabilitiesAfterCurrentTime<T extends { dateTime: string }>(
|
|
72
|
+
availabilities: T[],
|
|
73
|
+
now: Date = new Date(),
|
|
74
|
+
): T[] {
|
|
75
|
+
return availabilities.filter((availability) =>
|
|
76
|
+
isAvailabilityAfterCurrentTime(availability, now),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
58
80
|
export function isDateWithinPublicPickupUnknownCutoff(date: Date, now: Date = new Date()): boolean {
|
|
59
81
|
return date.getTime() <= now.getTime() + PUBLIC_PICKUP_UNKNOWN_CUTOFF_MS;
|
|
60
82
|
}
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* See `CHANGE_BOOKING_BE_HANDOFF.md` in this package for fields the API should populate.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
AdminAmendmentOperationSnapshot,
|
|
9
|
+
ChangeBookingQuoteResponse,
|
|
10
|
+
} from '../booking-api';
|
|
8
11
|
import type { PricingV2QuoteLineSnapshot } from '../booking-api';
|
|
9
12
|
import type { PriceBasisSnapshot } from '../booking-api';
|
|
10
13
|
import type { PriceSummaryLine } from '../../components/booking/PriceSummary';
|
|
@@ -207,6 +210,7 @@ export function mapQuoteLineItemsToPriceSummaryLines(
|
|
|
207
210
|
: type;
|
|
208
211
|
out.push({
|
|
209
212
|
kind: 'ticket',
|
|
213
|
+
operationId: isPricingV2Line(item) ? item.metadata?.operationId : undefined,
|
|
210
214
|
category,
|
|
211
215
|
qty: qty > 0 ? qty : 1,
|
|
212
216
|
itemTotal: amount,
|
|
@@ -228,6 +232,7 @@ export function mapQuoteLineItemsToPriceSummaryLines(
|
|
|
228
232
|
) {
|
|
229
233
|
out.push({
|
|
230
234
|
kind: 'ticket',
|
|
235
|
+
operationId: isPricingV2Line(item) ? item.metadata?.operationId : undefined,
|
|
231
236
|
category: lettersOnly,
|
|
232
237
|
qty,
|
|
233
238
|
itemTotal: amount,
|
|
@@ -249,6 +254,64 @@ export function mapQuoteLineItemsToPriceSummaryLines(
|
|
|
249
254
|
return out;
|
|
250
255
|
}
|
|
251
256
|
|
|
257
|
+
/** Clarify ticket deltas caused by moving a booking without changing receipt semantics. */
|
|
258
|
+
export function labelAdminAmendmentPriceSummaryLines(
|
|
259
|
+
lines: PriceSummaryLine[],
|
|
260
|
+
operations: readonly AdminAmendmentOperationSnapshot[],
|
|
261
|
+
originalLines: readonly PriceSummaryLine[] = [],
|
|
262
|
+
): PriceSummaryLine[] {
|
|
263
|
+
const movedServiceOperationIds = new Set(
|
|
264
|
+
operations
|
|
265
|
+
.filter((operation) => operation.type.trim().toUpperCase() === 'MOVE_SERVICE')
|
|
266
|
+
.map((operation) => operation.operationId),
|
|
267
|
+
);
|
|
268
|
+
if (movedServiceOperationIds.size === 0) return lines;
|
|
269
|
+
|
|
270
|
+
const normalizedTicketCategory = (category: string) => category
|
|
271
|
+
.replace(/^refund\s*[·:-]\s*/i, '')
|
|
272
|
+
.replace(/\s*[—-]\s*new date price difference\s*$/i, '')
|
|
273
|
+
.trim()
|
|
274
|
+
.toUpperCase();
|
|
275
|
+
|
|
276
|
+
return lines.map((line) => {
|
|
277
|
+
if (
|
|
278
|
+
line.kind !== 'ticket' ||
|
|
279
|
+
!line.operationId ||
|
|
280
|
+
!movedServiceOperationIds.has(line.operationId) ||
|
|
281
|
+
line.category.toLowerCase().includes('new date price difference')
|
|
282
|
+
) {
|
|
283
|
+
return line;
|
|
284
|
+
}
|
|
285
|
+
const originalLine = originalLines.find(
|
|
286
|
+
(candidate) => candidate.kind === 'ticket' &&
|
|
287
|
+
normalizedTicketCategory(candidate.category) === normalizedTicketCategory(line.category),
|
|
288
|
+
);
|
|
289
|
+
const previousUnitAmount = originalLine?.kind === 'ticket' && originalLine.qty > 0
|
|
290
|
+
? roundMoney(originalLine.itemTotal / originalLine.qty)
|
|
291
|
+
: null;
|
|
292
|
+
const differenceUnitAmount = line.qty > 0
|
|
293
|
+
? roundMoney(line.itemTotal / line.qty)
|
|
294
|
+
: null;
|
|
295
|
+
const ticketLabel = normalizedTicketCategory(line.category).toLowerCase();
|
|
296
|
+
return {
|
|
297
|
+
...line,
|
|
298
|
+
category: `${line.category} — new date price difference`,
|
|
299
|
+
...(previousUnitAmount != null && differenceUnitAmount != null
|
|
300
|
+
? {
|
|
301
|
+
unitPriceComparison: {
|
|
302
|
+
previousLabel: `Existing ${ticketLabel} price`,
|
|
303
|
+
updatedLabel: `New-date ${ticketLabel} price`,
|
|
304
|
+
differenceLabel: `Difference per ${ticketLabel}`,
|
|
305
|
+
previousUnitAmount,
|
|
306
|
+
updatedUnitAmount: roundMoney(previousUnitAmount + differenceUnitAmount),
|
|
307
|
+
differenceUnitAmount,
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
: {}),
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
252
315
|
function signedAmountDueFromQuote(quote: ChangeBookingQuoteResponse): number {
|
|
253
316
|
const pricingQuote = quote.pricingQuote ?? quote.quote;
|
|
254
317
|
// Prefer explicit settlement fields over balanceDelta. Receipt-total math and balanceDelta
|
|
@@ -280,10 +343,17 @@ export function buildChangeBookingServerPreview(
|
|
|
280
343
|
const paymentCreditTotal = pricingQuote?.paymentCreditTotal ?? quote.paymentCreditTotal;
|
|
281
344
|
|
|
282
345
|
const rawLines = lineItemsForSummary(quote);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
346
|
+
const mappedLines = rawLines && rawLines.length > 0
|
|
347
|
+
? mapQuoteLineItemsToPriceSummaryLines(rawLines)
|
|
348
|
+
: [];
|
|
349
|
+
const priceSummaryLines = withPaymentCreditSummaryLine(
|
|
350
|
+
labelAdminAmendmentPriceSummaryLines(
|
|
351
|
+
mappedLines,
|
|
352
|
+
pricingQuote?.amendment?.operations ?? [],
|
|
353
|
+
mapQuoteLineItemsToPriceSummaryLines(quote.originalReceipt?.lineItems),
|
|
354
|
+
),
|
|
355
|
+
paymentCreditTotal,
|
|
356
|
+
);
|
|
287
357
|
|
|
288
358
|
const completeness: ChangeBookingPreviewCompleteness = priceSummaryLines.length > 0 ? 'full' : 'totals_only';
|
|
289
359
|
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -1102,7 +1102,7 @@ export async function getPromoDiscount(
|
|
|
1102
1102
|
|
|
1103
1103
|
export async function getAddOns(
|
|
1104
1104
|
companyId: string,
|
|
1105
|
-
options?: { productOptionId?: string; preCheckout?: boolean }
|
|
1105
|
+
options?: { productOptionId?: string; preCheckout?: boolean; dateTime?: string }
|
|
1106
1106
|
): Promise<AddOn[]> {
|
|
1107
1107
|
const params = new URLSearchParams({ companyId });
|
|
1108
1108
|
if (options?.productOptionId) {
|
|
@@ -1110,6 +1110,7 @@ export async function getAddOns(
|
|
|
1110
1110
|
if (po) params.set('productOptionId', po);
|
|
1111
1111
|
}
|
|
1112
1112
|
if (options?.preCheckout !== undefined) params.set('preCheckout', String(options.preCheckout));
|
|
1113
|
+
if (options?.dateTime?.trim()) params.set('dateTime', options.dateTime.trim());
|
|
1113
1114
|
const res = await fetchBookingGetWithRetry(`${API_BASE}/1/add-ons?${params}`);
|
|
1114
1115
|
if (!res.ok) {
|
|
1115
1116
|
const err = await res.json();
|
|
@@ -1251,6 +1252,8 @@ export type AdminAmendmentRefundDisposition =
|
|
|
1251
1252
|
| 'PENDING_REFUND'
|
|
1252
1253
|
| 'NO_REFUND';
|
|
1253
1254
|
|
|
1255
|
+
export type AdminPromoApplicationScope = 'CHANGE_ONLY' | 'ENTIRE_BOOKING';
|
|
1256
|
+
|
|
1254
1257
|
export interface AdminAmendmentOperationSnapshot {
|
|
1255
1258
|
operationId: string;
|
|
1256
1259
|
type: string;
|
|
@@ -1276,6 +1279,9 @@ export interface ChangeBookingQuoteRequest {
|
|
|
1276
1279
|
newReturnAvailabilityId?: string | null;
|
|
1277
1280
|
newPassengerCounts?: Array<{ category: string; count: number }>;
|
|
1278
1281
|
newAddOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }>;
|
|
1282
|
+
/** Explicit promo intent for this amendment. Historical booking promos are never implied. */
|
|
1283
|
+
promoCode?: string | null;
|
|
1284
|
+
promoApplicationScope?: AdminPromoApplicationScope | null;
|
|
1279
1285
|
/** Full new-booking total shown in the UI; server verifies within tolerance then uses this for the session so charge matches screen. */
|
|
1280
1286
|
clientProposedTotal?: number;
|
|
1281
1287
|
/**
|
|
@@ -1354,6 +1360,7 @@ export interface ChangeBookingQuoteReceipt {
|
|
|
1354
1360
|
amount?: number;
|
|
1355
1361
|
type?: string;
|
|
1356
1362
|
quantity?: number;
|
|
1363
|
+
priceBasis?: PriceBasisSnapshot | null;
|
|
1357
1364
|
}>;
|
|
1358
1365
|
}
|
|
1359
1366
|
|
|
@@ -1704,6 +1711,7 @@ export function mapAdminChangeBookingQuoteV2Data(
|
|
|
1704
1711
|
amount: line.amount ?? undefined,
|
|
1705
1712
|
type: line.type ?? undefined,
|
|
1706
1713
|
quantity: line.quantity ?? undefined,
|
|
1714
|
+
priceBasis: line.priceBasis ?? undefined,
|
|
1707
1715
|
})),
|
|
1708
1716
|
},
|
|
1709
1717
|
};
|
|
@@ -2239,6 +2247,9 @@ export interface PricingV2QuoteSnapshot {
|
|
|
2239
2247
|
bookingReference?: string | null;
|
|
2240
2248
|
reservationReference?: string | null;
|
|
2241
2249
|
relatedChangeIntentId?: string | null;
|
|
2250
|
+
amendment?: {
|
|
2251
|
+
operations?: AdminAmendmentOperationSnapshot[] | null;
|
|
2252
|
+
} | null;
|
|
2242
2253
|
metadata?: Record<string, string> | null;
|
|
2243
2254
|
[key: string]: unknown;
|
|
2244
2255
|
}
|