@ticketboothapp/booking 1.2.98 → 1.2.100
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 +3 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +5 -8
- package/src/components/booking/BookingDialog.tsx +29 -16
- package/src/components/booking/ChangeBookingDialog.tsx +10 -2
- package/src/components/booking/CheckoutModal.tsx +4 -1
- package/src/components/booking/NewBookingFlow.tsx +494 -134
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +208 -6
- package/src/components/booking/booking-flow-types.ts +50 -2
- package/src/components/booking/booking-flow-ui.ts +2 -0
- package/src/contexts/AvailabilitiesCacheContext.tsx +2 -2
- package/src/lib/booking/pricing.ts +1 -0
- package/src/lib/booking-api.ts +24 -1
- package/src/runtime/types.ts +2 -0
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
EARLIEST_AVAILABILITY_DATE,
|
|
29
29
|
LATEST_AVAILABILITY_DATE,
|
|
30
30
|
INITIAL_FETCH_WEEKS,
|
|
31
|
+
VISIBLE_RANGE_BUFFER_WEEKS,
|
|
31
32
|
} from '../../lib/booking-constants';
|
|
32
33
|
import { formatCurrencyAmount } from '../../lib/currency';
|
|
33
34
|
import { formatBookingRefForDisplay } from '../../lib/booking-ref';
|
|
@@ -447,11 +448,86 @@ export function PrivateShuttleBookingFlow({
|
|
|
447
448
|
availabilitiesCache,
|
|
448
449
|
]);
|
|
449
450
|
|
|
451
|
+
/** Fresh selected-day check before reserve so stale private shuttle vacancies are corrected before payment. */
|
|
452
|
+
const refreshSelectedDateDetailsForCheckout = useCallback(async (): Promise<Availability[]> => {
|
|
453
|
+
if (!selectedDate || !product.productId) return [];
|
|
454
|
+
|
|
455
|
+
const result = await getAvailabilities(product.productId, selectedDate, selectedDate, {
|
|
456
|
+
allOptions: true,
|
|
457
|
+
promoCode: activePromoCode || undefined,
|
|
458
|
+
...(pricingProfileIdForAvailabilities
|
|
459
|
+
? { pricingProfileId: pricingProfileIdForAvailabilities }
|
|
460
|
+
: {}),
|
|
461
|
+
...(cancellationPolicyProfileIdForAvailabilities
|
|
462
|
+
? { cancellationPolicyProfileId: cancellationPolicyProfileIdForAvailabilities }
|
|
463
|
+
: {}),
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (result.pricingConfig && !pricingConfigSetRef.current) {
|
|
467
|
+
setPricingConfig(result.pricingConfig);
|
|
468
|
+
pricingConfigSetRef.current = true;
|
|
469
|
+
}
|
|
470
|
+
if (result.precomputedPrices) setPrecomputedPrices(result.precomputedPrices);
|
|
471
|
+
if (result.resourcePriceByCurrency) setResourcePriceByCurrency(result.resourcePriceByCurrency);
|
|
472
|
+
if (result.resourcePriceByOption) setResourcePriceByOption(result.resourcePriceByOption);
|
|
473
|
+
|
|
474
|
+
let mergedAvailabilities: Availability[] = [];
|
|
475
|
+
setAvailabilities((prev) => {
|
|
476
|
+
const existingMap = new Map(
|
|
477
|
+
prev.map((avail) => [`${avail.dateTime}-${avail.productId || avail.productOptionId}`, avail])
|
|
478
|
+
);
|
|
479
|
+
result.availabilities.forEach((avail) => {
|
|
480
|
+
existingMap.set(`${avail.dateTime}-${avail.productId || avail.productOptionId}`, avail);
|
|
481
|
+
});
|
|
482
|
+
mergedAvailabilities = Array.from(existingMap.values());
|
|
483
|
+
return mergedAvailabilities;
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const cacheKey = availabilitiesCache
|
|
487
|
+
? buildAvailabilitiesCacheKey(
|
|
488
|
+
product.productId,
|
|
489
|
+
activeOptionIdsKey,
|
|
490
|
+
activePromoCode,
|
|
491
|
+
pricingProfileIdForAvailabilities,
|
|
492
|
+
)
|
|
493
|
+
: null;
|
|
494
|
+
if (cacheKey && availabilitiesCache) {
|
|
495
|
+
const existingCache = availabilitiesCache.get(cacheKey);
|
|
496
|
+
const mergedAvailabilitiesMap = new Map(
|
|
497
|
+
(existingCache?.availabilities ?? []).map((availability) => [
|
|
498
|
+
`${availability.dateTime}-${availability.productId || availability.productOptionId}`,
|
|
499
|
+
availability,
|
|
500
|
+
])
|
|
501
|
+
);
|
|
502
|
+
result.availabilities.forEach((availability) => {
|
|
503
|
+
mergedAvailabilitiesMap.set(`${availability.dateTime}-${availability.productId || availability.productOptionId}`, availability);
|
|
504
|
+
});
|
|
505
|
+
availabilitiesCache.merge(cacheKey, {
|
|
506
|
+
fetchedRanges: existingCache?.fetchedRanges ?? fetchedRangesRef.current,
|
|
507
|
+
availabilities: Array.from(mergedAvailabilitiesMap.values()),
|
|
508
|
+
pricingConfig: result.pricingConfig ?? existingCache?.pricingConfig ?? null,
|
|
509
|
+
precomputedPrices: result.precomputedPrices ?? existingCache?.precomputedPrices ?? null,
|
|
510
|
+
resourcePriceByCurrency: result.resourcePriceByCurrency ?? existingCache?.resourcePriceByCurrency ?? null,
|
|
511
|
+
resourcePriceByOption: result.resourcePriceByOption ?? existingCache?.resourcePriceByOption ?? null,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return result.availabilities;
|
|
516
|
+
}, [
|
|
517
|
+
selectedDate,
|
|
518
|
+
product.productId,
|
|
519
|
+
activePromoCode,
|
|
520
|
+
pricingProfileIdForAvailabilities,
|
|
521
|
+
cancellationPolicyProfileIdForAvailabilities,
|
|
522
|
+
availabilitiesCache,
|
|
523
|
+
activeOptionIdsKey,
|
|
524
|
+
]);
|
|
525
|
+
|
|
450
526
|
useEffect(() => {
|
|
451
527
|
if (!visibleRange) {
|
|
452
528
|
setVisibleRange({
|
|
453
529
|
start: EARLIEST_AVAILABILITY_DATE,
|
|
454
|
-
end: addWeeks(EARLIEST_AVAILABILITY_DATE, INITIAL_FETCH_WEEKS),
|
|
530
|
+
end: addWeeks(EARLIEST_AVAILABILITY_DATE, INITIAL_FETCH_WEEKS + VISIBLE_RANGE_BUFFER_WEEKS),
|
|
455
531
|
});
|
|
456
532
|
}
|
|
457
533
|
}, [visibleRange]);
|
|
@@ -534,6 +610,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
534
610
|
)
|
|
535
611
|
: null;
|
|
536
612
|
const cached = cacheKey ? availabilitiesCache!.get(cacheKey) : undefined;
|
|
613
|
+
let shouldRevalidateCachedRange = false;
|
|
537
614
|
if (cached && cached.availabilities.length > 0) {
|
|
538
615
|
const cacheCoversRange = cached.fetchedRanges.some(
|
|
539
616
|
(r) =>
|
|
@@ -541,6 +618,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
541
618
|
);
|
|
542
619
|
const isStale = availabilitiesCache?.isStale(cached) ?? false;
|
|
543
620
|
if (cacheCoversRange) {
|
|
621
|
+
shouldRevalidateCachedRange = true;
|
|
544
622
|
setAvailabilities(cached.availabilities);
|
|
545
623
|
if (cached.pricingConfig) {
|
|
546
624
|
setPricingConfig(cached.pricingConfig);
|
|
@@ -553,8 +631,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
553
631
|
setIsFetchingMoreAvailabilities(false);
|
|
554
632
|
if (!isStale) {
|
|
555
633
|
fetchedRangesRef.current = [...cached.fetchedRanges];
|
|
556
|
-
|
|
557
|
-
return;
|
|
634
|
+
// Show cached availability instantly, then quietly revalidate the visible range.
|
|
558
635
|
}
|
|
559
636
|
}
|
|
560
637
|
// Partial cache: show cached data immediately, then fetch missing range below
|
|
@@ -570,7 +647,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
570
647
|
setLoadingAvailabilities(false);
|
|
571
648
|
}
|
|
572
649
|
|
|
573
|
-
if (!needsFetch(clampedStart, clampedEnd)) {
|
|
650
|
+
if (!shouldRevalidateCachedRange && !needsFetch(clampedStart, clampedEnd)) {
|
|
574
651
|
setLoadingAvailabilities(false);
|
|
575
652
|
setIsFetchingMoreAvailabilities(false);
|
|
576
653
|
fetchingRef.current = false;
|
|
@@ -579,12 +656,13 @@ export function PrivateShuttleBookingFlow({
|
|
|
579
656
|
const hasPartialCache = cached && cached.availabilities.length > 0;
|
|
580
657
|
fetchingRef.current = true;
|
|
581
658
|
if (!hasPartialCache) setLoadingAvailabilities(true);
|
|
582
|
-
else setIsFetchingMoreAvailabilities(true);
|
|
659
|
+
else if (!shouldRevalidateCachedRange) setIsFetchingMoreAvailabilities(true);
|
|
583
660
|
try {
|
|
584
661
|
const startDate = format(startOfDay(clampedStart), 'yyyy-MM-dd');
|
|
585
662
|
const endDate = format(endOfDay(clampedEnd), 'yyyy-MM-dd');
|
|
586
663
|
const result = await getAvailabilities(product.productId, startDate, endDate, {
|
|
587
664
|
allOptions: true,
|
|
665
|
+
summary: true,
|
|
588
666
|
promoCode: activePromoCode || undefined,
|
|
589
667
|
...(pricingProfileIdForAvailabilities
|
|
590
668
|
? { pricingProfileId: pricingProfileIdForAvailabilities }
|
|
@@ -643,7 +721,11 @@ export function PrivateShuttleBookingFlow({
|
|
|
643
721
|
});
|
|
644
722
|
}
|
|
645
723
|
} catch (err) {
|
|
646
|
-
|
|
724
|
+
if (shouldRevalidateCachedRange && cached?.availabilities.length) {
|
|
725
|
+
console.warn('Background private shuttle availability refresh failed; keeping cached availability visible.', err);
|
|
726
|
+
} else {
|
|
727
|
+
setError(err instanceof Error ? err.message : 'Failed to load availabilities');
|
|
728
|
+
}
|
|
647
729
|
} finally {
|
|
648
730
|
setLoadingAvailabilities(false);
|
|
649
731
|
setIsFetchingMoreAvailabilities(false);
|
|
@@ -702,6 +784,100 @@ export function PrivateShuttleBookingFlow({
|
|
|
702
784
|
|
|
703
785
|
const dates = useMemo(() => Object.keys(availabilitiesByDate).sort(), [availabilitiesByDate]);
|
|
704
786
|
|
|
787
|
+
useEffect(() => {
|
|
788
|
+
if (!selectedDate || !product.productId) return;
|
|
789
|
+
const selectedDateAvailabilities = availabilitiesByDate[selectedDate] ?? [];
|
|
790
|
+
if (!selectedDateAvailabilities.some((availability) => availability.isSummary)) return;
|
|
791
|
+
|
|
792
|
+
let cancelled = false;
|
|
793
|
+
async function hydrateSelectedDateDetails() {
|
|
794
|
+
setIsFetchingMoreAvailabilities(true);
|
|
795
|
+
try {
|
|
796
|
+
const result = await getAvailabilities(product.productId, selectedDate, selectedDate, {
|
|
797
|
+
allOptions: true,
|
|
798
|
+
promoCode: activePromoCode || undefined,
|
|
799
|
+
...(pricingProfileIdForAvailabilities
|
|
800
|
+
? { pricingProfileId: pricingProfileIdForAvailabilities }
|
|
801
|
+
: {}),
|
|
802
|
+
...(cancellationPolicyProfileIdForAvailabilities
|
|
803
|
+
? { cancellationPolicyProfileId: cancellationPolicyProfileIdForAvailabilities }
|
|
804
|
+
: {}),
|
|
805
|
+
});
|
|
806
|
+
if (cancelled) return;
|
|
807
|
+
|
|
808
|
+
if (result.pricingConfig && !pricingConfigSetRef.current) {
|
|
809
|
+
setPricingConfig(result.pricingConfig);
|
|
810
|
+
pricingConfigSetRef.current = true;
|
|
811
|
+
}
|
|
812
|
+
if (result.precomputedPrices) setPrecomputedPrices(result.precomputedPrices);
|
|
813
|
+
if (result.resourcePriceByCurrency) setResourcePriceByCurrency(result.resourcePriceByCurrency);
|
|
814
|
+
if (result.resourcePriceByOption) setResourcePriceByOption(result.resourcePriceByOption);
|
|
815
|
+
|
|
816
|
+
setAvailabilities((prev) => {
|
|
817
|
+
const merged = new Map(
|
|
818
|
+
prev.map((availability) => [
|
|
819
|
+
`${availability.dateTime}-${availability.productId || availability.productOptionId}`,
|
|
820
|
+
availability,
|
|
821
|
+
])
|
|
822
|
+
);
|
|
823
|
+
result.availabilities.forEach((availability) => {
|
|
824
|
+
merged.set(`${availability.dateTime}-${availability.productId || availability.productOptionId}`, availability);
|
|
825
|
+
});
|
|
826
|
+
return Array.from(merged.values());
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const cacheKey = availabilitiesCache
|
|
830
|
+
? buildAvailabilitiesCacheKey(
|
|
831
|
+
product.productId,
|
|
832
|
+
activeOptionIdsKey,
|
|
833
|
+
activePromoCode,
|
|
834
|
+
pricingProfileIdForAvailabilities,
|
|
835
|
+
)
|
|
836
|
+
: null;
|
|
837
|
+
if (cacheKey && availabilitiesCache) {
|
|
838
|
+
const existingCache = availabilitiesCache.get(cacheKey);
|
|
839
|
+
const mergedAvailabilities = new Map(
|
|
840
|
+
(existingCache?.availabilities ?? []).map((availability) => [
|
|
841
|
+
`${availability.dateTime}-${availability.productId || availability.productOptionId}`,
|
|
842
|
+
availability,
|
|
843
|
+
])
|
|
844
|
+
);
|
|
845
|
+
result.availabilities.forEach((availability) => {
|
|
846
|
+
mergedAvailabilities.set(`${availability.dateTime}-${availability.productId || availability.productOptionId}`, availability);
|
|
847
|
+
});
|
|
848
|
+
availabilitiesCache.merge(cacheKey, {
|
|
849
|
+
fetchedRanges: existingCache?.fetchedRanges ?? fetchedRangesRef.current,
|
|
850
|
+
availabilities: Array.from(mergedAvailabilities.values()),
|
|
851
|
+
pricingConfig: result.pricingConfig ?? existingCache?.pricingConfig ?? null,
|
|
852
|
+
precomputedPrices: result.precomputedPrices ?? existingCache?.precomputedPrices ?? null,
|
|
853
|
+
resourcePriceByCurrency: result.resourcePriceByCurrency ?? existingCache?.resourcePriceByCurrency ?? null,
|
|
854
|
+
resourcePriceByOption: result.resourcePriceByOption ?? existingCache?.resourcePriceByOption ?? null,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
} catch (err) {
|
|
858
|
+
if (!cancelled) {
|
|
859
|
+
console.error('Error hydrating private shuttle availability details:', err);
|
|
860
|
+
}
|
|
861
|
+
} finally {
|
|
862
|
+
if (!cancelled) setIsFetchingMoreAvailabilities(false);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
hydrateSelectedDateDetails();
|
|
867
|
+
return () => {
|
|
868
|
+
cancelled = true;
|
|
869
|
+
};
|
|
870
|
+
}, [
|
|
871
|
+
selectedDate,
|
|
872
|
+
availabilitiesByDate,
|
|
873
|
+
product.productId,
|
|
874
|
+
activeOptionIdsKey,
|
|
875
|
+
activePromoCode,
|
|
876
|
+
pricingProfileIdForAvailabilities,
|
|
877
|
+
cancellationPolicyProfileIdForAvailabilities,
|
|
878
|
+
availabilitiesCache,
|
|
879
|
+
]);
|
|
880
|
+
|
|
705
881
|
const earliestAvailabilityDate = useMemo(() => {
|
|
706
882
|
if (dates.length === 0) return EARLIEST_AVAILABILITY_DATE;
|
|
707
883
|
// Build date in company timezone at noon to avoid cross-timezone day shifts.
|
|
@@ -1319,6 +1495,32 @@ export function PrivateShuttleBookingFlow({
|
|
|
1319
1495
|
const [hours, minutes] = selectedStartTime.split(':').map(Number);
|
|
1320
1496
|
const startTimeISO = `${selectedDate}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00-06:00`;
|
|
1321
1497
|
|
|
1498
|
+
try {
|
|
1499
|
+
const refreshed = await refreshSelectedDateDetailsForCheckout();
|
|
1500
|
+
const slot = findMergedPrivateShuttleAvailability(
|
|
1501
|
+
refreshed,
|
|
1502
|
+
selectedAvailability,
|
|
1503
|
+
selectedOption || null,
|
|
1504
|
+
selectedDate || null,
|
|
1505
|
+
companyTimezone
|
|
1506
|
+
);
|
|
1507
|
+
const passengersToReserve = isSpecialRequest
|
|
1508
|
+
? Math.min(passengerCount, maxVacancies)
|
|
1509
|
+
: passengerCount;
|
|
1510
|
+
if (slot?.vacancies != null && (slot.vacancies === 0 || slot.vacancies < passengersToReserve)) {
|
|
1511
|
+
setError(
|
|
1512
|
+
describePrivateShuttleCapacityConflictMessage({
|
|
1513
|
+
passengersRequested: passengerCount,
|
|
1514
|
+
vacancies: slot.vacancies,
|
|
1515
|
+
})
|
|
1516
|
+
);
|
|
1517
|
+
setLoading(false);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
} catch (refreshErr) {
|
|
1521
|
+
console.warn('Fresh private shuttle availability check failed before reserve; falling back to reserve validation.', refreshErr);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1322
1524
|
const specialRequestNote = isSpecialRequest
|
|
1323
1525
|
? `Special request: Customer requested ${passengerCount} passengers (${resourceCount} shuttles needed). We reserved ${billableResourceCount} at standard capacity. Team to add ${resourceCount - billableResourceCount} more shuttle(s) and verify fleet availability.`
|
|
1324
1526
|
: '';
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import type { RefObject } from 'react';
|
|
2
|
-
import type {
|
|
1
|
+
import type { ReactNode, RefObject } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
Availability,
|
|
4
|
+
CheckoutDependentAddOnSelection,
|
|
5
|
+
ItineraryDisplayStep,
|
|
6
|
+
Product,
|
|
7
|
+
} from '../../lib/booking-api';
|
|
3
8
|
import type { BookingSourceMetadata } from '../../lib/booking/source-metadata';
|
|
4
9
|
import type { Currency } from './CurrencySwitcher';
|
|
10
|
+
import type { PriceSummaryLine } from './PriceSummary';
|
|
5
11
|
import type { BookingFlowUiOptions } from './booking-flow-ui';
|
|
6
12
|
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
7
13
|
|
|
@@ -96,6 +102,25 @@ export interface BookingFlowBaseProps {
|
|
|
96
102
|
hideItineraryBox?: boolean;
|
|
97
103
|
/** Partner / embed-only tweaks; omit for default website behavior. */
|
|
98
104
|
flowUi?: BookingFlowUiOptions;
|
|
105
|
+
/**
|
|
106
|
+
* Optional inline extension rendered immediately below the Build Itinerary box once
|
|
107
|
+
* a date and shuttle time are selected. Used by photo-first dependent add-on flows.
|
|
108
|
+
*/
|
|
109
|
+
afterItinerary?: ReactNode | ((context: BookingFlowAfterItineraryContext) => ReactNode);
|
|
110
|
+
/**
|
|
111
|
+
* Optional hook to insert add-on rows into the Build Itinerary box. Receives the
|
|
112
|
+
* base itinerary from the selected shuttle and returns the display list to render.
|
|
113
|
+
*/
|
|
114
|
+
augmentItineraryItems?: (context: BookingFlowAfterItineraryContext) => ItineraryDisplayStep[] | null;
|
|
115
|
+
/** Optional hook for embed flows to show extra rows in the checkout receipt summary. */
|
|
116
|
+
augmentPriceSummary?: (context: BookingFlowPriceSummaryContext) => BookingFlowPriceSummaryAugmentation | null;
|
|
117
|
+
/**
|
|
118
|
+
* Optional dependent add-on to create with the primary booking after checkout.
|
|
119
|
+
* Used by photo-first flows where the add-on is selected before the shuttle booking exists.
|
|
120
|
+
*/
|
|
121
|
+
dependentAddOnSelection?: CheckoutDependentAddOnSelection | null;
|
|
122
|
+
/** Optional external validation before checkout submit (e.g. required add-on questionnaire answers). */
|
|
123
|
+
validateBeforeCheckout?: (context: BookingFlowAfterItineraryContext) => string | null;
|
|
99
124
|
/** Explicit reserve/checkout source metadata (browser URL layer + optional portal merge at call site). */
|
|
100
125
|
bookingSourceAttribution: Partial<BookingSourceMetadata>;
|
|
101
126
|
/** Dedicated partner portal app (`booking.*`): persist reserve `source` as PARTNER_PORTAL. */
|
|
@@ -108,6 +133,29 @@ export interface BookingFlowBaseProps {
|
|
|
108
133
|
changeProductOptions?: Product[];
|
|
109
134
|
}
|
|
110
135
|
|
|
136
|
+
export interface BookingFlowAfterItineraryContext {
|
|
137
|
+
selectedDate: string | null;
|
|
138
|
+
selectedAvailability: Availability | null;
|
|
139
|
+
itineraryItems: ItineraryDisplayStep[] | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface BookingFlowPriceSummaryContext {
|
|
143
|
+
selectedDate: string | null;
|
|
144
|
+
selectedAvailability: Availability | null;
|
|
145
|
+
itineraryItems?: ItineraryDisplayStep[] | null;
|
|
146
|
+
priceSummaryLines: PriceSummaryLine[];
|
|
147
|
+
subtotal: number;
|
|
148
|
+
tax: number;
|
|
149
|
+
total: number;
|
|
150
|
+
currency: Currency;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface BookingFlowPriceSummaryAugmentation {
|
|
154
|
+
lines?: PriceSummaryLine[];
|
|
155
|
+
subtotalAdjustment?: number;
|
|
156
|
+
totalAdjustment?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
111
159
|
/** Standard (new) reservation flow — no change-booking receipt or callbacks. */
|
|
112
160
|
export interface NewBookingFlowProps extends BookingFlowBaseProps {}
|
|
113
161
|
|
|
@@ -45,6 +45,8 @@ export type ProviderDashboardChangePricingUi = {
|
|
|
45
45
|
export interface BookingFlowUiOptions {
|
|
46
46
|
showCollage?: boolean;
|
|
47
47
|
showTourDescription?: boolean;
|
|
48
|
+
/** When false, the flow still uses the selected/initial date but does not render the calendar picker. */
|
|
49
|
+
showCalendar?: boolean;
|
|
48
50
|
autoSelectFirstAvailableDate?: boolean;
|
|
49
51
|
autoSelectFirstHighlightedPickup?: boolean;
|
|
50
52
|
/**
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
} from 'react';
|
|
10
10
|
import type { Availability, PricingConfig, PrecomputedPricesByCategory } from '../lib/booking-api';
|
|
11
11
|
|
|
12
|
-
/** Cache TTL in milliseconds (
|
|
13
|
-
export const AVAILABILITIES_CACHE_TTL_MS =
|
|
12
|
+
/** Cache TTL in milliseconds (1 minute). Entries older than this are considered stale. */
|
|
13
|
+
export const AVAILABILITIES_CACHE_TTL_MS = 60 * 1000;
|
|
14
14
|
|
|
15
15
|
export interface CachedAvailabilitiesData {
|
|
16
16
|
fetchedRanges: Array<{ start: Date; end: Date }>;
|
|
@@ -355,6 +355,7 @@ export interface OrderSummaryFeeLine {
|
|
|
355
355
|
name: string;
|
|
356
356
|
totalAmount: number;
|
|
357
357
|
description?: string;
|
|
358
|
+
showQuantityLabel?: boolean;
|
|
358
359
|
}
|
|
359
360
|
|
|
360
361
|
/** One ticket line for order summary (category, qty, price per unit, item total in display currency). */
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -1298,6 +1298,7 @@ export interface Availability {
|
|
|
1298
1298
|
pricesByCategory?: {
|
|
1299
1299
|
retailPrices: Array<{ category: string; price: number }>;
|
|
1300
1300
|
};
|
|
1301
|
+
isSummary?: boolean;
|
|
1301
1302
|
}
|
|
1302
1303
|
|
|
1303
1304
|
export type PrecomputedPricesByCategory = Record<string, Record<string, number>>;
|
|
@@ -1306,13 +1307,16 @@ export interface GetAvailabilitiesResponse {
|
|
|
1306
1307
|
availabilities: Availability[];
|
|
1307
1308
|
pricingConfig?: PricingConfig;
|
|
1308
1309
|
precomputedPrices?: PrecomputedPricesByCategory;
|
|
1310
|
+
precomputedPricesByOption?: Record<string, PrecomputedPricesByCategory>;
|
|
1309
1311
|
resourcePriceByCurrency?: Record<string, number>;
|
|
1310
1312
|
resourcePriceByOption?: Record<string, Record<string, number>>;
|
|
1313
|
+
responseMode?: 'summary' | string;
|
|
1311
1314
|
}
|
|
1312
1315
|
|
|
1313
1316
|
export interface GetAvailabilitiesOptions {
|
|
1314
1317
|
promoCode?: string | null;
|
|
1315
1318
|
allOptions?: boolean;
|
|
1319
|
+
summary?: boolean;
|
|
1316
1320
|
/**
|
|
1317
1321
|
* When set, TicketBooth should return rates/precomputed prices for this partner pricing profile.
|
|
1318
1322
|
* Must match the profile linked on the partner (e.g. capabilities.pricingProfileId).
|
|
@@ -1359,6 +1363,7 @@ export async function getAvailabilities(
|
|
|
1359
1363
|
});
|
|
1360
1364
|
if (options?.promoCode?.trim()) params.set('promoCode', options.promoCode.trim());
|
|
1361
1365
|
if (options?.allOptions === true) params.set('allOptions', 'true');
|
|
1366
|
+
if (options?.summary === true) params.set('summary', 'true');
|
|
1362
1367
|
const pricingProfileId = options?.pricingProfileId?.trim();
|
|
1363
1368
|
if (pricingProfileId) params.set('pricingProfileId', pricingProfileId);
|
|
1364
1369
|
const cancellationPolicyProfileId = options?.cancellationPolicyProfileId?.trim();
|
|
@@ -1407,12 +1412,17 @@ export async function getAvailabilities(
|
|
|
1407
1412
|
}
|
|
1408
1413
|
|
|
1409
1414
|
const bookingData = (data as { data?: GetAvailabilitiesResponse } | null)?.data;
|
|
1415
|
+
const isSummary = options?.summary === true || bookingData?.responseMode === 'summary';
|
|
1410
1416
|
return {
|
|
1411
|
-
availabilities: bookingData?.availabilities ?? []
|
|
1417
|
+
availabilities: (bookingData?.availabilities ?? []).map((availability) =>
|
|
1418
|
+
isSummary ? { ...availability, isSummary: true } : availability
|
|
1419
|
+
),
|
|
1412
1420
|
pricingConfig: bookingData?.pricingConfig,
|
|
1413
1421
|
precomputedPrices: bookingData?.precomputedPrices,
|
|
1422
|
+
precomputedPricesByOption: bookingData?.precomputedPricesByOption,
|
|
1414
1423
|
resourcePriceByCurrency: bookingData?.resourcePriceByCurrency,
|
|
1415
1424
|
resourcePriceByOption: bookingData?.resourcePriceByOption,
|
|
1425
|
+
responseMode: bookingData?.responseMode,
|
|
1416
1426
|
};
|
|
1417
1427
|
}
|
|
1418
1428
|
|
|
@@ -1649,6 +1659,16 @@ export interface CheckoutBreakdown {
|
|
|
1649
1659
|
currency: string;
|
|
1650
1660
|
}
|
|
1651
1661
|
|
|
1662
|
+
export interface CheckoutDependentAddOnSelection {
|
|
1663
|
+
dependentAddOnProductId: string;
|
|
1664
|
+
dependentAddOnProductOptionId?: string;
|
|
1665
|
+
offeringId: string;
|
|
1666
|
+
slotStart: string;
|
|
1667
|
+
slotEnd: string;
|
|
1668
|
+
quantity: number;
|
|
1669
|
+
checkoutAnswers?: Record<string, string>;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1652
1672
|
export interface CreatePaymentIntentRequest {
|
|
1653
1673
|
productId: string;
|
|
1654
1674
|
optionId: string;
|
|
@@ -1667,6 +1687,7 @@ export interface CreatePaymentIntentRequest {
|
|
|
1667
1687
|
cancellationPolicyId?: string;
|
|
1668
1688
|
termsAcceptedAt?: string;
|
|
1669
1689
|
checkoutBreakdown?: CheckoutBreakdown;
|
|
1690
|
+
dependentAddOnSelection?: CheckoutDependentAddOnSelection;
|
|
1670
1691
|
itineraryDisplay?: ItineraryDisplayStep[];
|
|
1671
1692
|
skipConfirmationCommunications?: boolean;
|
|
1672
1693
|
disableAutoCommunications?: boolean;
|
|
@@ -1765,6 +1786,7 @@ export interface ConfirmFreeBookingRequest {
|
|
|
1765
1786
|
pickupLocationId?: string;
|
|
1766
1787
|
termsAcceptedAt?: string;
|
|
1767
1788
|
itineraryDisplay?: ItineraryDisplayStep[];
|
|
1789
|
+
dependentAddOnSelection?: CheckoutDependentAddOnSelection;
|
|
1768
1790
|
skipConfirmationCommunications?: boolean;
|
|
1769
1791
|
disableAutoCommunications?: boolean;
|
|
1770
1792
|
source?: string;
|
|
@@ -1846,6 +1868,7 @@ export interface ConfirmBookingWithoutPaymentRequest {
|
|
|
1846
1868
|
skipConfirmationCommunications?: boolean;
|
|
1847
1869
|
disableAutoCommunications?: boolean;
|
|
1848
1870
|
checkoutBreakdown: CheckoutBreakdown;
|
|
1871
|
+
dependentAddOnSelection?: CheckoutDependentAddOnSelection;
|
|
1849
1872
|
depositAmount: number;
|
|
1850
1873
|
balanceAmount: number;
|
|
1851
1874
|
totalAmount: number;
|
package/src/runtime/types.ts
CHANGED
|
@@ -74,6 +74,8 @@ export interface BookingRuntimeCatalog {
|
|
|
74
74
|
getProductDescription: (...args: any[]) => any;
|
|
75
75
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
76
|
buildMinimalProductFromConfig?: (...args: any[]) => any;
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
getStaticProductByIdOrSlug?: (...args: any[]) => any;
|
|
77
79
|
/** Image URL helper from the host catalog (e.g. Via Via `constants/images`). */
|
|
78
80
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
81
|
getImageUrl?: (...args: any[]) => any;
|