@ticketboothapp/booking 1.2.180 → 1.2.182
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +12 -7
- package/src/components/booking/BookingDialog.tsx +9 -4
- package/src/components/booking/BookingProductGrid.module.css +22 -10
- package/src/components/booking/BookingProductGrid.tsx +2 -2
- package/src/components/booking/ChangeBookingDialog.tsx +3 -1
- package/src/components/booking/ChangeBookingFlow.tsx +1 -1
- package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +3 -0
- package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +3 -0
- package/src/components/booking/NewBookingFlow.tsx +6 -2
- package/src/components/booking/PrivateShuttleAddOnsSection.tsx +1 -1
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +21 -1
- package/src/components/booking/StandardBookingSelectionControlsPanel.tsx +9 -2
- package/src/components/booking/TicketSelector.module.css +8 -0
- package/src/components/booking/TicketSelector.tsx +8 -0
- package/src/components/booking/admin-change-flow-state-helpers.ts +35 -0
- package/src/components/booking/availability-cache-policy.ts +10 -0
- package/src/components/booking/booking-flow-types.ts +3 -0
- package/src/components/booking/booking-flow-ui.ts +10 -0
- package/src/components/booking/use-private-shuttle-availability.ts +5 -1
- package/src/components/booking/use-standard-booking-availability.ts +36 -10
- package/src/constants/pill-values.ts +0 -8
- package/src/constants/products.ts +2 -2
- package/src/data/product-descriptions/private-tour.en.json +1 -2
- package/src/index.ts +5 -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/partner-pricing-profile.ts +74 -0
- package/src/lib/booking/reservation-attempt.ts +138 -0
- package/src/lib/booking-api.ts +297 -71
- package/src/lib/env.ts +13 -0
- package/src/providers/booking-dialog-provider.tsx +3 -2
- package/src/public-partners.ts +12 -1
- package/src/runtime/types.ts +4 -0
- package/src/strings/en.json +1 -2
- package/src/strings/es.json +1 -2
- package/src/strings/fr.json +1 -2
- package/test/change-booking-helpers.test.ts +181 -1
- package/test/partner-pricing-profile.test.ts +46 -0
package/package.json
CHANGED
|
@@ -18,7 +18,6 @@ import { CURRENCIES, DEFAULT_CURRENCY, type Currency } from './CurrencySwitcher'
|
|
|
18
18
|
import { useCompanyTimezone } from '../../contexts/CompanyContext';
|
|
19
19
|
import { useBookingApp } from '../../contexts/BookingAppContext';
|
|
20
20
|
import { useBookingHost } from '../../runtime';
|
|
21
|
-
import { filterAvailabilitiesAfterCurrentTime } from '../../lib/booking/booking-cutoffs';
|
|
22
21
|
import type { ChangeBookingFlowProps } from './booking-flow-types';
|
|
23
22
|
import {
|
|
24
23
|
normalizeAddOnSelections,
|
|
@@ -26,6 +25,7 @@ import {
|
|
|
26
25
|
} from './change-booking-flow-helpers';
|
|
27
26
|
import {
|
|
28
27
|
buildChangeBookingMinimumQuantities,
|
|
28
|
+
filterAdminChangeCalendarAvailabilities,
|
|
29
29
|
isAdminManualOverrideEligibleLine,
|
|
30
30
|
resolveChangeFlowOriginalDate,
|
|
31
31
|
resolveInitialChangeProductOptionId,
|
|
@@ -156,6 +156,10 @@ export function AdminChangeBookingFlow({
|
|
|
156
156
|
isAdmin,
|
|
157
157
|
companyId: env.COMPANY_ID,
|
|
158
158
|
});
|
|
159
|
+
const changeFlowOriginalDate = useMemo(
|
|
160
|
+
() => resolveChangeFlowOriginalDate(initialValues?.dateTime, companyTimezone),
|
|
161
|
+
[initialValues?.dateTime, companyTimezone],
|
|
162
|
+
);
|
|
159
163
|
const [adminAvailabilityNowMs, setAdminAvailabilityNowMs] = useState(() => Date.now());
|
|
160
164
|
|
|
161
165
|
useEffect(() => {
|
|
@@ -171,8 +175,13 @@ export function AdminChangeBookingFlow({
|
|
|
171
175
|
(rows: Availability[]) =>
|
|
172
176
|
product.productType === 'PRIVATE_SHUTTLE'
|
|
173
177
|
? rows
|
|
174
|
-
:
|
|
175
|
-
|
|
178
|
+
: filterAdminChangeCalendarAvailabilities({
|
|
179
|
+
availabilities: rows,
|
|
180
|
+
originalDate: changeFlowOriginalDate,
|
|
181
|
+
companyTimezone,
|
|
182
|
+
now: adminAvailabilityNow,
|
|
183
|
+
}),
|
|
184
|
+
[adminAvailabilityNow, changeFlowOriginalDate, companyTimezone, product.productType],
|
|
176
185
|
);
|
|
177
186
|
const [selectedAvailability, setSelectedAvailability] = useState<Availability | null>(null);
|
|
178
187
|
const [selectedReturnOption, setSelectedReturnOption] = useState<ReturnOption | null>(null);
|
|
@@ -279,10 +288,6 @@ export function AdminChangeBookingFlow({
|
|
|
279
288
|
const [skipConfirmationCommunications, setSkipConfirmationCommunications] = useState(false);
|
|
280
289
|
/** Admin only: disable all auto communications for this booking (provider dashboard). */
|
|
281
290
|
const [disableAutoCommunications, setDisableAutoCommunications] = useState(false);
|
|
282
|
-
const changeFlowOriginalDate = useMemo(
|
|
283
|
-
() => resolveChangeFlowOriginalDate(initialValues?.dateTime, companyTimezone),
|
|
284
|
-
[initialValues?.dateTime, companyTimezone],
|
|
285
|
-
);
|
|
286
291
|
const isProviderDashboardChange = Boolean(onChangeBooking);
|
|
287
292
|
const useAdminFeAuthoritativeQuote = isAdmin && isProviderDashboardChange;
|
|
288
293
|
/** Any change from an existing booking (public or provider). */
|
|
@@ -8,6 +8,7 @@ import { getProduct, type Product } from '../../lib/booking-api';
|
|
|
8
8
|
import './booking-flow.css';
|
|
9
9
|
import BookingProductGrid from './BookingProductGrid';
|
|
10
10
|
import { BookingFlow } from './BookingFlow';
|
|
11
|
+
import { VIAVIA_TICKET_FINE_PRINT_BY_CATEGORY } from './booking-flow-ui';
|
|
11
12
|
import { PrivateShuttleBookingFlow } from './PrivateShuttleBookingFlow';
|
|
12
13
|
import { BookingFlowPreview } from './BookingFlowPreview';
|
|
13
14
|
import { useIsBookingLaunchLive } from '../../hooks/useIsBookingLaunchLive';
|
|
@@ -162,6 +163,7 @@ function BookFlowScreen({
|
|
|
162
163
|
onComplete();
|
|
163
164
|
}}
|
|
164
165
|
isPartialLaunch={isPartialLaunch}
|
|
166
|
+
flowUi={{ ticketFinePrintByCategory: VIAVIA_TICKET_FINE_PRINT_BY_CATEGORY }}
|
|
165
167
|
/>
|
|
166
168
|
)}
|
|
167
169
|
</div>
|
|
@@ -194,6 +196,9 @@ export default function BookingDialog() {
|
|
|
194
196
|
const isLaunchLive = useIsBookingLaunchLive();
|
|
195
197
|
const isPartialLaunch = !isLaunchLive;
|
|
196
198
|
const currentScreen = stack[stack.length - 1];
|
|
199
|
+
const currentScreenType = currentScreen.type;
|
|
200
|
+
const currentBookFlowProductId =
|
|
201
|
+
currentScreenType === 'book-flow' ? currentScreen.productId : null;
|
|
197
202
|
const [productName, setProductName] = useState<string | null>(null);
|
|
198
203
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
199
204
|
|
|
@@ -209,18 +214,18 @@ export default function BookingDialog() {
|
|
|
209
214
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
210
215
|
|
|
211
216
|
useEffect(() => {
|
|
212
|
-
if (
|
|
217
|
+
if (currentScreenType === 'product-grid') {
|
|
213
218
|
setProductName(null);
|
|
214
219
|
}
|
|
215
|
-
}, [
|
|
220
|
+
}, [currentScreenType]);
|
|
216
221
|
|
|
217
222
|
// Reset scroll when switching to book-flow so we don't appear partially scrolled
|
|
218
223
|
// (product-grid restores its own scroll when returning from a product)
|
|
219
224
|
useEffect(() => {
|
|
220
|
-
if (
|
|
225
|
+
if (currentScreenType === 'book-flow' && contentRef.current) {
|
|
221
226
|
contentRef.current.scrollTop = 0;
|
|
222
227
|
}
|
|
223
|
-
}, [
|
|
228
|
+
}, [currentBookFlowProductId, currentScreenType]);
|
|
224
229
|
|
|
225
230
|
const overlayRef = useRef<HTMLDivElement>(null);
|
|
226
231
|
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
.grid {
|
|
51
51
|
display: grid;
|
|
52
|
-
grid-template-columns: 1fr
|
|
52
|
+
grid-template-columns: 1fr;
|
|
53
53
|
gap: 0.75rem;
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -65,10 +65,11 @@
|
|
|
65
65
|
transform: scale(1.05);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/* Mobile: full-width landscape card (single column) */
|
|
68
69
|
.tileImageContainer {
|
|
69
70
|
position: relative;
|
|
70
71
|
width: 100%;
|
|
71
|
-
height:
|
|
72
|
+
height: 220px;
|
|
72
73
|
overflow: hidden;
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -328,31 +329,42 @@
|
|
|
328
329
|
display: none;
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
.tileImageContainer {
|
|
332
|
-
height: 280px;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
332
|
.tileStartTime {
|
|
336
|
-
font-size:
|
|
333
|
+
font-size: 2rem;
|
|
337
334
|
}
|
|
338
335
|
|
|
339
336
|
.expandedInner {
|
|
340
337
|
flex-direction: column;
|
|
338
|
+
min-height: 0;
|
|
341
339
|
}
|
|
342
340
|
|
|
343
341
|
.expandedImage {
|
|
344
|
-
flex: 0 0
|
|
345
|
-
min-height:
|
|
342
|
+
flex: 0 0 260px;
|
|
343
|
+
min-height: 260px;
|
|
346
344
|
}
|
|
347
345
|
|
|
348
346
|
.expandedVideo {
|
|
349
|
-
min-height:
|
|
347
|
+
min-height: 260px;
|
|
350
348
|
}
|
|
351
349
|
|
|
352
350
|
.expandedContent {
|
|
353
351
|
padding: 1rem;
|
|
354
352
|
}
|
|
355
353
|
|
|
354
|
+
/* Keep the CTA above the fold: title → book button → description → pills */
|
|
355
|
+
.expandedTitle {
|
|
356
|
+
order: -2;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.expandedActions {
|
|
360
|
+
order: -1;
|
|
361
|
+
margin: 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.bookButton {
|
|
365
|
+
width: 100%;
|
|
366
|
+
}
|
|
367
|
+
|
|
356
368
|
.expandedTags {
|
|
357
369
|
left: 0.75rem;
|
|
358
370
|
}
|
|
@@ -375,7 +375,7 @@ export default function BookingProductGrid({
|
|
|
375
375
|
const expandedProduct =
|
|
376
376
|
expandedIndex >= 0 ? products[expandedIndex] : null;
|
|
377
377
|
|
|
378
|
-
// Column count for expand-in-place logic (
|
|
378
|
+
// Column count for expand-in-place logic (1 mobile, 3 desktop; compact always 2)
|
|
379
379
|
const [cols, setCols] = useState(compact ? 2 : 3);
|
|
380
380
|
useEffect(() => {
|
|
381
381
|
if (compact) {
|
|
@@ -383,7 +383,7 @@ export default function BookingProductGrid({
|
|
|
383
383
|
return;
|
|
384
384
|
}
|
|
385
385
|
const mq = window.matchMedia('(min-width: 768px)');
|
|
386
|
-
const update = () => setCols(mq.matches ? 3 :
|
|
386
|
+
const update = () => setCols(mq.matches ? 3 : 1);
|
|
387
387
|
update();
|
|
388
388
|
mq.addEventListener('change', update);
|
|
389
389
|
return () => mq.removeEventListener('change', update);
|
|
@@ -8,6 +8,7 @@ import { useBookingHost } from '../../runtime';
|
|
|
8
8
|
import { formatCurrencyAmount, type Currency } from '../../lib/currency';
|
|
9
9
|
import { CURRENCIES, DEFAULT_CURRENCY } from './CurrencySwitcher';
|
|
10
10
|
import { ChangeBookingFlow } from './ChangeBookingFlow';
|
|
11
|
+
import { VIAVIA_TICKET_FINE_PRINT_BY_CATEGORY } from './booking-flow-ui';
|
|
11
12
|
import type { ChangeFlowSelectionPreview } from './booking-flow-types';
|
|
12
13
|
import { useBookingSourceMetadataFromLocation } from '../../hooks/useBookingSourceMetadataFromLocation';
|
|
13
14
|
import styles from './BookingDialog.module.css';
|
|
@@ -131,7 +132,7 @@ export default function ChangeBookingDialog({
|
|
|
131
132
|
return () => {
|
|
132
133
|
cancelled = true;
|
|
133
134
|
};
|
|
134
|
-
}, [isOpen, apiProductId, minimalProduct, staticProduct]);
|
|
135
|
+
}, [isOpen, apiProductId, env.COMPANY_ID, minimalProduct, staticProduct]);
|
|
135
136
|
|
|
136
137
|
useEffect(() => {
|
|
137
138
|
if (isOpen) {
|
|
@@ -448,6 +449,7 @@ export default function ChangeBookingDialog({
|
|
|
448
449
|
}}
|
|
449
450
|
initialValues={initialValues}
|
|
450
451
|
onChangeFlowSelectionPreview={handleChangeFlowSelectionPreview}
|
|
452
|
+
flowUi={{ ticketFinePrintByCategory: VIAVIA_TICKET_FINE_PRINT_BY_CATEGORY }}
|
|
451
453
|
/>
|
|
452
454
|
</div>
|
|
453
455
|
)}
|
|
@@ -329,7 +329,6 @@ export function ChangeBookingFlow({
|
|
|
329
329
|
|
|
330
330
|
const {
|
|
331
331
|
initialAddOnQtyByKey,
|
|
332
|
-
initialAddOnMinQtyByKey,
|
|
333
332
|
initialAddOnMinTotalByAddOnId,
|
|
334
333
|
updateAddOnSelections,
|
|
335
334
|
} = useChangeBookingAddOnFloorController({
|
|
@@ -953,6 +952,7 @@ export function ChangeBookingFlow({
|
|
|
953
952
|
addOnSelections={addOnSelections}
|
|
954
953
|
onAddOnSelectionsChange={updateAddOnSelections}
|
|
955
954
|
minimumTotalByAddOnId={isCustomerSelfServeChange ? initialAddOnMinTotalByAddOnId : undefined}
|
|
955
|
+
ticketFinePrintByCategory={flowUi?.ticketFinePrintByCategory}
|
|
956
956
|
/>
|
|
957
957
|
|
|
958
958
|
{/* Total and Checkout — shared PriceSummary component */}
|
|
@@ -50,6 +50,7 @@ export interface ChangeBookingSelectionControlsPanelProps {
|
|
|
50
50
|
suppressUnitPrices?: boolean;
|
|
51
51
|
suppressReturnPerPersonPrices?: boolean;
|
|
52
52
|
suppressAddOnPrices?: boolean;
|
|
53
|
+
ticketFinePrintByCategory?: Record<string, string>;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export function ChangeBookingSelectionControlsPanel({
|
|
@@ -90,6 +91,7 @@ export function ChangeBookingSelectionControlsPanel({
|
|
|
90
91
|
suppressUnitPrices = false,
|
|
91
92
|
suppressReturnPerPersonPrices = false,
|
|
92
93
|
suppressAddOnPrices = false,
|
|
94
|
+
ticketFinePrintByCategory,
|
|
93
95
|
}: ChangeBookingSelectionControlsPanelProps) {
|
|
94
96
|
return (
|
|
95
97
|
<>
|
|
@@ -156,6 +158,7 @@ export function ChangeBookingSelectionControlsPanel({
|
|
|
156
158
|
onAddOnSelectionsChange={onAddOnSelectionsChange}
|
|
157
159
|
minimumTotalByAddOnId={minimumTotalByAddOnId}
|
|
158
160
|
suppressAddOnPrices={suppressAddOnPrices}
|
|
161
|
+
ticketFinePrintByCategory={ticketFinePrintByCategory}
|
|
159
162
|
/>
|
|
160
163
|
) : null}
|
|
161
164
|
</>
|
|
@@ -40,6 +40,7 @@ export interface ChangeBookingTicketsAndAddOnsPanelProps {
|
|
|
40
40
|
minimumTotalByAddOnId?: Map<string, number>;
|
|
41
41
|
suppressUnitPrices?: boolean;
|
|
42
42
|
suppressAddOnPrices?: boolean;
|
|
43
|
+
ticketFinePrintByCategory?: Record<string, string>;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export function ChangeBookingTicketsAndAddOnsPanel({
|
|
@@ -69,6 +70,7 @@ export function ChangeBookingTicketsAndAddOnsPanel({
|
|
|
69
70
|
minimumTotalByAddOnId,
|
|
70
71
|
suppressUnitPrices = false,
|
|
71
72
|
suppressAddOnPrices = false,
|
|
73
|
+
ticketFinePrintByCategory,
|
|
72
74
|
}: ChangeBookingTicketsAndAddOnsPanelProps) {
|
|
73
75
|
const incompatibleSelections = incompatibleAddOnSelections(
|
|
74
76
|
addOnSelections,
|
|
@@ -101,6 +103,7 @@ export function ChangeBookingTicketsAndAddOnsPanel({
|
|
|
101
103
|
minimumQuantities={minimumQuantities}
|
|
102
104
|
ticketUnitFloorByCategory={applyReceiptPaidFloors ? ticketUnitFloorByCategory : undefined}
|
|
103
105
|
suppressUnitPrices={suppressUnitPrices}
|
|
106
|
+
ticketFinePrintByCategory={ticketFinePrintByCategory}
|
|
104
107
|
/>
|
|
105
108
|
|
|
106
109
|
{totalQuantity > 0 && addOns.length > 0 ? (
|
|
@@ -69,13 +69,14 @@ export function NewBookingFlow({
|
|
|
69
69
|
bookingSourceAttribution,
|
|
70
70
|
partnerPortalBooking = false,
|
|
71
71
|
availabilityPricingProfileId,
|
|
72
|
+
availabilityPricingProfileOverrides,
|
|
72
73
|
availabilityCancellationPolicyProfileId,
|
|
73
74
|
}: NewBookingFlowProps) {
|
|
74
75
|
const { env, analytics, catalog } = useBookingHost();
|
|
75
76
|
const { t } = useTranslations();
|
|
76
77
|
const { locale } = useLocale();
|
|
77
78
|
const companyTimezone = useCompanyTimezone(); // Get timezone from context
|
|
78
|
-
const
|
|
79
|
+
const fallbackPricingProfileIdForAvailabilities = (availabilityPricingProfileId ?? '').trim() || null;
|
|
79
80
|
const cancellationPolicyProfileIdForAvailabilities =
|
|
80
81
|
(availabilityCancellationPolicyProfileId ?? '').trim() || null;
|
|
81
82
|
const {
|
|
@@ -162,6 +163,7 @@ export function NewBookingFlow({
|
|
|
162
163
|
setSelectedReturnOption,
|
|
163
164
|
selectedDate,
|
|
164
165
|
setSelectedDate,
|
|
166
|
+
pricingProfileIdForAvailabilities,
|
|
165
167
|
loadingAvailabilities,
|
|
166
168
|
isFetchingMoreAvailabilities,
|
|
167
169
|
pricingConfig,
|
|
@@ -182,7 +184,8 @@ export function NewBookingFlow({
|
|
|
182
184
|
isAdmin,
|
|
183
185
|
companyTimezone,
|
|
184
186
|
appliedPromoCode,
|
|
185
|
-
pricingProfileIdForAvailabilities,
|
|
187
|
+
pricingProfileIdForAvailabilities: fallbackPricingProfileIdForAvailabilities,
|
|
188
|
+
pricingProfileOverridesForAvailabilities: availabilityPricingProfileOverrides,
|
|
186
189
|
cancellationPolicyProfileIdForAvailabilities,
|
|
187
190
|
bookingCutoffNow,
|
|
188
191
|
bookingCutoffMinutes,
|
|
@@ -952,6 +955,7 @@ export function NewBookingFlow({
|
|
|
952
955
|
returnVacancies={effectiveSelectedReturnVacancies}
|
|
953
956
|
isSimplifiedPricingView={isSimplifiedPricingView}
|
|
954
957
|
onQuantityChange={handleQuantityChange}
|
|
958
|
+
ticketFinePrintByCategory={flowUi?.ticketFinePrintByCategory}
|
|
955
959
|
addOns={addOns}
|
|
956
960
|
addOnSelections={addOnSelections}
|
|
957
961
|
onAddOnSelectionsChange={updateAddOnSelections}
|
|
@@ -153,7 +153,7 @@ export function PrivateShuttleAddOnsSection({
|
|
|
153
153
|
Food restrictions
|
|
154
154
|
</label>
|
|
155
155
|
<p className="mb-2 text-sm text-stone-500">
|
|
156
|
-
Shuttle includes
|
|
156
|
+
Shuttle includes coffee, tea, hot chocolate, and trail snacks. If you are adding on breakfast or lunch, please let us know if you have any dietary restrictions or allergies.
|
|
157
157
|
</p>
|
|
158
158
|
<textarea
|
|
159
159
|
id="food-restrictions"
|
|
@@ -59,6 +59,10 @@ import {
|
|
|
59
59
|
resolveInitialPrivateShuttlePassengerCount,
|
|
60
60
|
} from './private-shuttle-passenger-count';
|
|
61
61
|
import { privateShuttleHiddenLunchAddOnId } from './private-shuttle-lunch-visibility';
|
|
62
|
+
import {
|
|
63
|
+
resolvePartnerPricingProfileId,
|
|
64
|
+
type PartnerPricingProfileOverride,
|
|
65
|
+
} from '../../lib/booking/partner-pricing-profile';
|
|
62
66
|
|
|
63
67
|
interface PrivateShuttleBookingFlowProps {
|
|
64
68
|
product: Product;
|
|
@@ -85,6 +89,8 @@ interface PrivateShuttleBookingFlowProps {
|
|
|
85
89
|
partnerPortalBooking?: boolean;
|
|
86
90
|
/** When set (e.g. partner portal), get-availabilities requests this pricing profile from the API. */
|
|
87
91
|
availabilityPricingProfileId?: string | null;
|
|
92
|
+
/** Month-specific partner profiles resolved against selectedDate. */
|
|
93
|
+
availabilityPricingProfileOverrides?: readonly PartnerPricingProfileOverride[];
|
|
88
94
|
/** When set (e.g. partner portal), get-availabilities filters cancellation policies by this profile. */
|
|
89
95
|
availabilityCancellationPolicyProfileId?: string | null;
|
|
90
96
|
initialValues?: {
|
|
@@ -117,6 +123,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
117
123
|
bookingSourceAttribution,
|
|
118
124
|
partnerPortalBooking = false,
|
|
119
125
|
availabilityPricingProfileId,
|
|
126
|
+
availabilityPricingProfileOverrides,
|
|
120
127
|
availabilityCancellationPolicyProfileId,
|
|
121
128
|
initialValues,
|
|
122
129
|
initialBooking,
|
|
@@ -126,7 +133,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
126
133
|
const { t } = useTranslations();
|
|
127
134
|
const { locale } = useLocale();
|
|
128
135
|
const companyTimezone = useCompanyTimezone();
|
|
129
|
-
const
|
|
136
|
+
const fallbackPricingProfileIdForAvailabilities = (availabilityPricingProfileId ?? '').trim() || null;
|
|
130
137
|
const cancellationPolicyProfileIdForAvailabilities =
|
|
131
138
|
(availabilityCancellationPolicyProfileId ?? '').trim() || null;
|
|
132
139
|
const {
|
|
@@ -145,6 +152,19 @@ export function PrivateShuttleBookingFlow({
|
|
|
145
152
|
companyTimezone,
|
|
146
153
|
)
|
|
147
154
|
);
|
|
155
|
+
const pricingProfileIdForAvailabilities = useMemo(
|
|
156
|
+
() => resolvePartnerPricingProfileId(
|
|
157
|
+
fallbackPricingProfileIdForAvailabilities,
|
|
158
|
+
availabilityPricingProfileOverrides,
|
|
159
|
+
selectedDate || formatInTimeZone(new Date(), companyTimezone, 'yyyy-MM-dd'),
|
|
160
|
+
),
|
|
161
|
+
[
|
|
162
|
+
availabilityPricingProfileOverrides,
|
|
163
|
+
companyTimezone,
|
|
164
|
+
fallbackPricingProfileIdForAvailabilities,
|
|
165
|
+
selectedDate,
|
|
166
|
+
],
|
|
167
|
+
);
|
|
148
168
|
const [selectedAvailability, setSelectedAvailability] = useState<Availability | null>(null);
|
|
149
169
|
const [selectedOption, setSelectedOption] = useState<string>('');
|
|
150
170
|
const [selectedStartTime, setSelectedStartTime] = useState<string>('');
|
|
@@ -65,6 +65,7 @@ export interface StandardBookingSelectionControlsPanelProps {
|
|
|
65
65
|
returnVacancies: number | null;
|
|
66
66
|
isSimplifiedPricingView: boolean;
|
|
67
67
|
onQuantityChange: (category: string, delta: number) => void;
|
|
68
|
+
ticketFinePrintByCategory?: Record<string, string>;
|
|
68
69
|
addOns: AddOn[];
|
|
69
70
|
addOnSelections: AddOnSelection[];
|
|
70
71
|
onAddOnSelectionsChange: AddOnSelectionsChangeHandler;
|
|
@@ -107,6 +108,7 @@ export function StandardBookingSelectionControlsPanel({
|
|
|
107
108
|
returnVacancies,
|
|
108
109
|
isSimplifiedPricingView,
|
|
109
110
|
onQuantityChange,
|
|
111
|
+
ticketFinePrintByCategory,
|
|
110
112
|
addOns,
|
|
111
113
|
addOnSelections,
|
|
112
114
|
onAddOnSelectionsChange,
|
|
@@ -142,9 +144,13 @@ export function StandardBookingSelectionControlsPanel({
|
|
|
142
144
|
) : null}
|
|
143
145
|
|
|
144
146
|
{selectedBookingOptionsHydrating ? (
|
|
145
|
-
<div
|
|
147
|
+
<div
|
|
148
|
+
className="flex items-center justify-center gap-3 py-2 text-stone-600"
|
|
149
|
+
role="status"
|
|
150
|
+
aria-live="polite"
|
|
151
|
+
>
|
|
146
152
|
<div className="booking-loading-spinner" aria-hidden />
|
|
147
|
-
<div>{t('booking.
|
|
153
|
+
<div>{t('booking.confirmingAvailability')}</div>
|
|
148
154
|
</div>
|
|
149
155
|
) : null}
|
|
150
156
|
|
|
@@ -218,6 +224,7 @@ export function StandardBookingSelectionControlsPanel({
|
|
|
218
224
|
onQuantityChange={onQuantityChange}
|
|
219
225
|
minimumQuantities={undefined}
|
|
220
226
|
ticketUnitFloorByCategory={undefined}
|
|
227
|
+
ticketFinePrintByCategory={ticketFinePrintByCategory}
|
|
221
228
|
/>
|
|
222
229
|
) : null}
|
|
223
230
|
|
|
@@ -49,6 +49,14 @@
|
|
|
49
49
|
color: var(--booking-stone-500, #78716c);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
.finePrint {
|
|
53
|
+
margin-top: 0.25rem;
|
|
54
|
+
font-size: 0.7rem;
|
|
55
|
+
line-height: 1.3;
|
|
56
|
+
color: var(--booking-stone-500, #78716c);
|
|
57
|
+
max-width: 16rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
.priceStrikethrough {
|
|
53
61
|
text-decoration: line-through;
|
|
54
62
|
color: var(--booking-stone-400, #a8a29e);
|
|
@@ -48,6 +48,8 @@ interface TicketSelectorProps {
|
|
|
48
48
|
ticketUnitFloorByCategory?: Map<string, number>;
|
|
49
49
|
/** Hide unit/strike prices (customer change flow until server-backed pricing is shown). */
|
|
50
50
|
suppressUnitPrices?: boolean;
|
|
51
|
+
/** Optional small fine print rendered under a ticket row, keyed by category (e.g. INFANT). */
|
|
52
|
+
ticketFinePrintByCategory?: Record<string, string>;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
export function TicketSelector({
|
|
@@ -70,6 +72,7 @@ export function TicketSelector({
|
|
|
70
72
|
minimumQuantities,
|
|
71
73
|
ticketUnitFloorByCategory,
|
|
72
74
|
suppressUnitPrices = false,
|
|
75
|
+
ticketFinePrintByCategory,
|
|
73
76
|
}: TicketSelectorProps) {
|
|
74
77
|
/** Party size vs tighter of pickup/return vacancies (parent passes min of both legs). */
|
|
75
78
|
const isOverbookedTickets = totalQuantity > selectedVacancies;
|
|
@@ -166,6 +169,11 @@ export function TicketSelector({
|
|
|
166
169
|
formatCurrencyAmount(effectiveUnitPrice, currency, locale as 'en' | 'fr')
|
|
167
170
|
)}
|
|
168
171
|
</p>
|
|
172
|
+
{ticketFinePrintByCategory?.[rate.category] && (
|
|
173
|
+
<p className={styles.finePrint}>
|
|
174
|
+
{ticketFinePrintByCategory[rate.category]}
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
169
177
|
</div>
|
|
170
178
|
<div className={styles.controls}>
|
|
171
179
|
<button
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatInTimeZone } from 'date-fns-tz';
|
|
2
2
|
import type { Availability, Product } from '../../lib/booking-api';
|
|
3
|
+
import { isAvailabilityAfterCurrentTime } from '../../lib/booking/booking-cutoffs';
|
|
3
4
|
import {
|
|
4
5
|
effectiveProductOptionIdForChangeFlow,
|
|
5
6
|
normalizeProductOptionIdForChangeFlow,
|
|
@@ -45,6 +46,40 @@ export function resolveChangeFlowOriginalDate(
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Admin changes may need to edit the return for a tour that is already in progress.
|
|
51
|
+
* Keep every availability on that booking's service date visible even after its
|
|
52
|
+
* start time passes, while retaining the normal "future starts only" rule for
|
|
53
|
+
* every other date.
|
|
54
|
+
*/
|
|
55
|
+
export function filterAdminChangeCalendarAvailabilities({
|
|
56
|
+
availabilities,
|
|
57
|
+
originalDate,
|
|
58
|
+
companyTimezone,
|
|
59
|
+
now = new Date(),
|
|
60
|
+
}: {
|
|
61
|
+
availabilities: Availability[];
|
|
62
|
+
originalDate: string | null;
|
|
63
|
+
companyTimezone: string;
|
|
64
|
+
now?: Date;
|
|
65
|
+
}): Availability[] {
|
|
66
|
+
return availabilities.filter((availability) => {
|
|
67
|
+
if (originalDate) {
|
|
68
|
+
try {
|
|
69
|
+
const availabilityDate = formatInTimeZone(
|
|
70
|
+
parseAvailabilityDateTime(availability.dateTime),
|
|
71
|
+
companyTimezone,
|
|
72
|
+
'yyyy-MM-dd',
|
|
73
|
+
);
|
|
74
|
+
if (availabilityDate === originalDate) return true;
|
|
75
|
+
} catch {
|
|
76
|
+
/* fall through to the normal time guard */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return isAvailabilityAfterCurrentTime(availability, now);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
48
83
|
export function buildChangeBookingMinimumQuantities({
|
|
49
84
|
isCustomerSelfServeChange,
|
|
50
85
|
isAdmin,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A covered cache range is revalidated only after the cache TTL expires.
|
|
3
|
+
* Reserve/checkout perform their own fresh validation before committing inventory.
|
|
4
|
+
*/
|
|
5
|
+
export function shouldRevalidateAvailabilityCache(
|
|
6
|
+
cacheCoversRange: boolean,
|
|
7
|
+
isStale: boolean,
|
|
8
|
+
): boolean {
|
|
9
|
+
return cacheCoversRange && isStale;
|
|
10
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
Product,
|
|
7
7
|
} from '../../lib/booking-api';
|
|
8
8
|
import type { BookingSourceMetadata } from '../../lib/booking/source-metadata';
|
|
9
|
+
import type { PartnerPricingProfileOverride } from '../../lib/booking/partner-pricing-profile';
|
|
9
10
|
import type { Currency } from './CurrencySwitcher';
|
|
10
11
|
import type { PriceSummaryLine } from './PriceSummary';
|
|
11
12
|
import type { BookingFlowUiOptions } from './booking-flow-ui';
|
|
@@ -129,6 +130,8 @@ export interface BookingFlowBaseProps {
|
|
|
129
130
|
partnerPortalBooking?: boolean;
|
|
130
131
|
/** When set (e.g. partner portal), get-availabilities requests this pricing profile from the API. */
|
|
131
132
|
availabilityPricingProfileId?: string | null;
|
|
133
|
+
/** Month-specific partner pricing profiles resolved against the selected booking date. */
|
|
134
|
+
availabilityPricingProfileOverrides?: readonly PartnerPricingProfileOverride[];
|
|
132
135
|
/** When set (e.g. partner portal), get-availabilities filters cancellation policies by this profile. */
|
|
133
136
|
availabilityCancellationPolicyProfileId?: string | null;
|
|
134
137
|
/** Admin change-booking: available destination products for switching the booking to another product. */
|
|
@@ -69,8 +69,18 @@ export interface BookingFlowUiOptions {
|
|
|
69
69
|
providerDashboardChangePricingUi?: ProviderDashboardChangePricingUi;
|
|
70
70
|
/** Override the orange section heading above cancellation policy cards (e.g. photo-first shuttle flow). */
|
|
71
71
|
cancellationPolicySectionLabel?: string;
|
|
72
|
+
/** Small fine print shown under a ticket category row, keyed by category (e.g. { INFANT: "..." }). */
|
|
73
|
+
ticketFinePrintByCategory?: Record<string, string>;
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Via Via default per-category ticket fine print, shown under the ticket row in the
|
|
78
|
+
* ticket selector. Surfaces that pass their own `flowUi` can override or omit it.
|
|
79
|
+
*/
|
|
80
|
+
export const VIAVIA_TICKET_FINE_PRINT_BY_CATEGORY: Record<string, string> = {
|
|
81
|
+
INFANT: 'You are required to bring your own safety seat for children under 40lbs (20kg).',
|
|
82
|
+
};
|
|
83
|
+
|
|
74
84
|
/**
|
|
75
85
|
* Baseline UX for embedded partner-style surfaces (partner portal Book tab, TicketBooth provider dashboard):
|
|
76
86
|
* skip collage + tour description, auto-pick the earliest bookable calendar day and first time slot,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
privateShuttleAvailabilityDateInZone,
|
|
33
33
|
privateShuttleStartTimeAllowed,
|
|
34
34
|
} from './private-shuttle-availability';
|
|
35
|
+
import { shouldRevalidateAvailabilityCache } from './availability-cache-policy';
|
|
35
36
|
import { getStartOfCurrentDay } from './standard-booking-availability';
|
|
36
37
|
|
|
37
38
|
interface UsePrivateShuttleAvailabilityParams {
|
|
@@ -364,7 +365,10 @@ export function usePrivateShuttleAvailability({
|
|
|
364
365
|
);
|
|
365
366
|
const isStale = availabilitiesCache?.isStale(cached) ?? false;
|
|
366
367
|
if (cacheCoversRange) {
|
|
367
|
-
shouldRevalidateCachedRange =
|
|
368
|
+
shouldRevalidateCachedRange = shouldRevalidateAvailabilityCache(
|
|
369
|
+
cacheCoversRange,
|
|
370
|
+
isStale,
|
|
371
|
+
);
|
|
368
372
|
setAvailabilities(cached.availabilities);
|
|
369
373
|
if (cached.pricingConfig) applyCachedPricingConfig(cached.pricingConfig);
|
|
370
374
|
if (cached.precomputedPrices) setPrecomputedPrices(cached.precomputedPrices);
|