@ticketboothapp/booking 1.2.66 → 1.2.68
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
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
getAvailabilities,
|
|
8
8
|
cancelReservation,
|
|
9
9
|
cancelReservationBestEffort,
|
|
10
|
+
createPaymentIntent,
|
|
10
11
|
quoteChangeBooking,
|
|
11
12
|
confirmFreeChangeBooking,
|
|
12
13
|
createChangeBookingPaymentIntent,
|
|
14
|
+
confirmBookingWithoutPayment,
|
|
13
15
|
getAddOns,
|
|
14
16
|
validatePromoCode,
|
|
15
17
|
getPromoDiscount,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
} from '../../lib/booking-constants';
|
|
32
34
|
import { getSundayOfWeek } from '../../lib/booking/sunday-week';
|
|
33
35
|
import { Calendar } from './Calendar';
|
|
36
|
+
import { AdminPaymentChoiceModal } from './AdminPaymentChoiceModal';
|
|
34
37
|
import { ItineraryBox } from './ItineraryBox';
|
|
35
38
|
import { ItineraryPlaceholder } from './ItineraryPlaceholder';
|
|
36
39
|
import { PickupTimeSelector } from './PickupTimeSelector';
|
|
@@ -96,6 +99,7 @@ import { getItineraryStepLabel } from '../../lib/booking/itinerary-display';
|
|
|
96
99
|
import { MANAGE_BOOKING_FROM_CHANGE_PAYMENT, MANAGE_BOOKING_QUERY_FROM } from '../../lib/manage-booking-post-checkout';
|
|
97
100
|
import { useBookingHost } from '../../runtime';
|
|
98
101
|
import type { BookingFlowUiOptions } from './booking-flow-ui';
|
|
102
|
+
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
99
103
|
import type { ChangeBookingFlowProps, ChangeFlowSelectionPreview } from './booking-flow-types';
|
|
100
104
|
import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provider';
|
|
101
105
|
|
|
@@ -107,6 +111,8 @@ import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provi
|
|
|
107
111
|
* `serverPreview.priceSummaryLines` when the quote includes rows; otherwise falls back to FE-built lines after totals confirm.
|
|
108
112
|
*
|
|
109
113
|
* Until the first successful quote, `selfServeCheckoutPlaceholder` still avoids showing unchecked totals.
|
|
114
|
+
*
|
|
115
|
+
* **Provider dashboard** (`onChangeBooking`) — unchanged; manual pricing via `flowUi.providerDashboardChangePricingUi`.
|
|
110
116
|
*/
|
|
111
117
|
function mergeQuoteSliceWithServerPreview(
|
|
112
118
|
slice: ReturnType<typeof sliceChangeQuoteForUi>,
|
|
@@ -403,8 +409,9 @@ function pickOutboundMatchingPreviousSelection(
|
|
|
403
409
|
anchor: { productOptionId: string | null; minutesFromMidnight: number },
|
|
404
410
|
companyTimezone: string,
|
|
405
411
|
optionsMap: Map<string, { mostPopular?: boolean }>,
|
|
412
|
+
isAdmin: boolean
|
|
406
413
|
): Availability | null {
|
|
407
|
-
const selectable = timesForSelectedDate.filter((a) => (a.vacancies ?? 0) > 0);
|
|
414
|
+
const selectable = timesForSelectedDate.filter((a) => (a.vacancies ?? 0) > 0 || isAdmin);
|
|
408
415
|
if (selectable.length === 0) return null;
|
|
409
416
|
|
|
410
417
|
const withOption =
|
|
@@ -438,8 +445,9 @@ function pickReturnMatchingPreviousSelection(
|
|
|
438
445
|
sortedReturnOptions: ReturnOption[],
|
|
439
446
|
anchor: { returnLocation: string; minutesFromMidnight: number },
|
|
440
447
|
companyTimezone: string,
|
|
448
|
+
isAdmin: boolean
|
|
441
449
|
): ReturnOption | null {
|
|
442
|
-
const eligible = sortedReturnOptions.filter((o) => (o.vacancies ?? 0) > 0);
|
|
450
|
+
const eligible = sortedReturnOptions.filter((o) => (o.vacancies ?? 0) > 0 || isAdmin);
|
|
443
451
|
if (eligible.length === 0) return null;
|
|
444
452
|
|
|
445
453
|
const sameLoc = eligible.filter((o) => o.returnLocation === anchor.returnLocation);
|
|
@@ -628,7 +636,8 @@ function resolveInitialAvailabilityFromBooking(
|
|
|
628
636
|
}
|
|
629
637
|
|
|
630
638
|
/**
|
|
631
|
-
*
|
|
639
|
+
* Admin / provider-dashboard **change booking** — literal duplicate of {@link ChangeBookingFlow} for now
|
|
640
|
+
* so ticketbooth can diverge without affecting the public site flow.
|
|
632
641
|
*/
|
|
633
642
|
export function AdminChangeBookingFlow({
|
|
634
643
|
product,
|
|
@@ -652,6 +661,7 @@ export function AdminChangeBookingFlow({
|
|
|
652
661
|
partnerPortalBooking = false,
|
|
653
662
|
availabilityPricingProfileId,
|
|
654
663
|
availabilityCancellationPolicyProfileId,
|
|
664
|
+
onChangeBooking,
|
|
655
665
|
}: ChangeBookingFlowProps) {
|
|
656
666
|
/** Always the booking’s sold currency — not the site currency switcher / parent default. */
|
|
657
667
|
const currency = useMemo((): Currency => {
|
|
@@ -663,6 +673,16 @@ export function AdminChangeBookingFlow({
|
|
|
663
673
|
return DEFAULT_CURRENCY;
|
|
664
674
|
}, [originalReceipt?.currency, initialValues?.currency, currencyFromParent]);
|
|
665
675
|
|
|
676
|
+
const isManualOverrideEligibleLine = (line: { editable: boolean; type?: string; label?: string }): boolean => {
|
|
677
|
+
if (!line.editable) return false;
|
|
678
|
+
const type = (line.type ?? '').toUpperCase();
|
|
679
|
+
const label = (line.label ?? '').toLowerCase();
|
|
680
|
+
const isPromoLikeType =
|
|
681
|
+
type.includes('PROMO') || type.includes('DISCOUNT') || type.includes('VOUCHER') || type.includes('GIFT');
|
|
682
|
+
const isPromoLikeLabel =
|
|
683
|
+
label.includes('promo') || label.includes('discount') || label.includes('voucher') || label.includes('gift');
|
|
684
|
+
return !(isPromoLikeType || isPromoLikeLabel);
|
|
685
|
+
};
|
|
666
686
|
const { env, analytics } = useBookingHost();
|
|
667
687
|
const { t } = useTranslations();
|
|
668
688
|
const { locale } = useLocale();
|
|
@@ -671,6 +691,7 @@ export function AdminChangeBookingFlow({
|
|
|
671
691
|
const cancellationPolicyProfileIdForAvailabilities =
|
|
672
692
|
(availabilityCancellationPolicyProfileId ?? '').trim() || null;
|
|
673
693
|
const {
|
|
694
|
+
permissions,
|
|
674
695
|
isSimplifiedPricingView,
|
|
675
696
|
onShowManage,
|
|
676
697
|
getSuccessUrl,
|
|
@@ -678,6 +699,7 @@ export function AdminChangeBookingFlow({
|
|
|
678
699
|
mode: bookingAppMode,
|
|
679
700
|
} = useBookingApp();
|
|
680
701
|
const availabilitiesCache = useAvailabilitiesCache();
|
|
702
|
+
const isAdmin = permissions.viewerRole === 'admin';
|
|
681
703
|
const [availabilities, setAvailabilities] = useState<Availability[]>([]);
|
|
682
704
|
const [selectedAvailability, setSelectedAvailability] = useState<Availability | null>(null);
|
|
683
705
|
const [selectedReturnOption, setSelectedReturnOption] = useState<ReturnOption | null>(null);
|
|
@@ -776,6 +798,12 @@ export function AdminChangeBookingFlow({
|
|
|
776
798
|
differenceTotal: number;
|
|
777
799
|
};
|
|
778
800
|
} | null>(null);
|
|
801
|
+
/** Admin only: skip sending confirmation at creation (provider dashboard). */
|
|
802
|
+
const [skipConfirmationCommunications, setSkipConfirmationCommunications] = useState(false);
|
|
803
|
+
/** Admin only: disable all auto communications for this booking (provider dashboard). */
|
|
804
|
+
const [disableAutoCommunications, setDisableAutoCommunications] = useState(false);
|
|
805
|
+
/** Admin only: show choice to pay now or confirm without payment (full balance owed). */
|
|
806
|
+
const [showAdminPaymentChoice, setShowAdminPaymentChoice] = useState(false);
|
|
779
807
|
const hasAppliedInitialValuesRef = useRef(false);
|
|
780
808
|
const hasAppliedInitialQuantitiesRef = useRef(false);
|
|
781
809
|
const hasHydratedAddOnsFromReceiptRef = useRef(false);
|
|
@@ -794,11 +822,23 @@ export function AdminChangeBookingFlow({
|
|
|
794
822
|
return null;
|
|
795
823
|
}
|
|
796
824
|
}, [initialValues?.dateTime, companyTimezone]);
|
|
825
|
+
const isProviderDashboardChange = Boolean(onChangeBooking);
|
|
826
|
+
/** Any change from an existing booking (public or provider). */
|
|
827
|
+
const isChangeBookingContext = Boolean(initialValues?.bookingReference?.trim());
|
|
828
|
+
/**
|
|
829
|
+
* When the provider passes `onChangeBooking` we normally apply the change immediately on checkout.
|
|
830
|
+
* In provider-dashboard + admin + change mode we need the "Pay now / confirm without payment" step
|
|
831
|
+
* first, so we run the public change-intent quote + PI, then call `onChangeBooking` from
|
|
832
|
+
* `handleConfirmWithoutPayment` (or after paying in the checkout modal).
|
|
833
|
+
*/
|
|
834
|
+
const deferProviderApplyToAdminPaymentChoice =
|
|
835
|
+
isAdmin &&
|
|
836
|
+
isChangeBookingContext &&
|
|
837
|
+
bookingAppMode === 'provider-dashboard' &&
|
|
838
|
+
isProviderDashboardChange;
|
|
839
|
+
const isCustomerSelfServeChange = !isProviderDashboardChange || deferProviderApplyToAdminPaymentChoice;
|
|
797
840
|
/** Do not render catalog-/FE-derived dollar amounts in UI until `quoteChangeBooking` returns `serverDisplay`. */
|
|
798
|
-
const suppressSelfServeCurrencyUi =
|
|
799
|
-
/** Self-serve change flow only (no provider/admin pricing paths). */
|
|
800
|
-
const isCustomerSelfServeChange = true;
|
|
801
|
-
const isAdmin = false;
|
|
841
|
+
const suppressSelfServeCurrencyUi = isCustomerSelfServeChange;
|
|
802
842
|
|
|
803
843
|
useEffect(() => {
|
|
804
844
|
setPartnerAttributionConfirmed(false);
|
|
@@ -808,13 +848,13 @@ export function AdminChangeBookingFlow({
|
|
|
808
848
|
* user picks a different return time — baseline is the first auto-selected return for this outbound.
|
|
809
849
|
*/
|
|
810
850
|
const [implicitReturnBaselineId, setImplicitReturnBaselineId] = useState<string | null>(null);
|
|
811
|
-
/**
|
|
851
|
+
/** Any change flow (self-serve or provider): promo from booking is fixed — show read-only, never add new. */
|
|
812
852
|
const lockedPromoCode = initialValues?.promoCode?.trim()
|
|
813
853
|
? initialValues.promoCode.trim().toUpperCase()
|
|
814
854
|
: null;
|
|
815
855
|
/** Public self-serve only: cannot reduce tickets below original counts. */
|
|
816
856
|
const changeBookingMinimumQuantities = useMemo(() => {
|
|
817
|
-
if (!initialValues?.bookingItems?.length) return undefined;
|
|
857
|
+
if (!isCustomerSelfServeChange || !initialValues?.bookingItems?.length) return undefined;
|
|
818
858
|
const m: Record<string, number> = {};
|
|
819
859
|
for (const item of initialValues.bookingItems) {
|
|
820
860
|
const key = item.category?.trim();
|
|
@@ -822,7 +862,30 @@ export function AdminChangeBookingFlow({
|
|
|
822
862
|
m[key] = Math.max(0, Number(item.count) || 0);
|
|
823
863
|
}
|
|
824
864
|
return m;
|
|
825
|
-
}, [initialValues?.bookingItems]);
|
|
865
|
+
}, [isCustomerSelfServeChange, initialValues?.bookingItems]);
|
|
866
|
+
const [adminChoiceData, setAdminChoiceData] = useState<{
|
|
867
|
+
reservationReference: string;
|
|
868
|
+
reservationExpiration?: string;
|
|
869
|
+
checkoutBreakdown: { lineItems: Array<{ label: string; amount: number; type?: string; quantity?: number }>; totalAmount: number; currency: string };
|
|
870
|
+
totalAmount: number;
|
|
871
|
+
datePart: string;
|
|
872
|
+
timePart: string;
|
|
873
|
+
availabilityProductOptionId: string;
|
|
874
|
+
itineraryDisplay?: ItineraryDisplayStep[] | null;
|
|
875
|
+
clientSecret: string;
|
|
876
|
+
ticketLinesForModal: CheckoutModalLineItem[];
|
|
877
|
+
feeLineItems: OrderSummary['feeLineItems'];
|
|
878
|
+
returnPriceAdjustment: number;
|
|
879
|
+
cancellationPolicyFee: number;
|
|
880
|
+
cancellationPolicyLabel?: string;
|
|
881
|
+
subtotal: number;
|
|
882
|
+
tax: number;
|
|
883
|
+
totalQuantity: number;
|
|
884
|
+
isTaxIncludedInPrice: boolean;
|
|
885
|
+
taxRate: number;
|
|
886
|
+
promoDiscountAmount: number;
|
|
887
|
+
discountLabel?: string | null;
|
|
888
|
+
} | null>(null);
|
|
826
889
|
const [latestChangeQuote, setLatestChangeQuote] = useState<{
|
|
827
890
|
priceDiff: number;
|
|
828
891
|
currency: Currency;
|
|
@@ -1659,12 +1722,14 @@ export function AdminChangeBookingFlow({
|
|
|
1659
1722
|
}, [initialValues?.productId, product.productId]);
|
|
1660
1723
|
|
|
1661
1724
|
/**
|
|
1662
|
-
* Receipt pricing on protected seats/fees
|
|
1663
|
-
* + same product option) vs Rule B (`max(receipt, live)` when date or option changes).
|
|
1725
|
+
* Receipt pricing on protected seats/fees — **customer self-serve only**: Rule A (exact receipt unit when same calendar day
|
|
1726
|
+
* + same product option) vs Rule B (`max(receipt, live)` when date or option changes). Provider dashboard uses live catalog.
|
|
1664
1727
|
*/
|
|
1665
1728
|
const changeFlowApplyReceiptPaidFloors = useMemo(
|
|
1666
|
-
() =>
|
|
1667
|
-
|
|
1729
|
+
() =>
|
|
1730
|
+
!isProviderDashboardChange &&
|
|
1731
|
+
changeFlowBookingParentProductIdForFloors === product.productId.trim(),
|
|
1732
|
+
[isProviderDashboardChange, changeFlowBookingParentProductIdForFloors, product.productId],
|
|
1668
1733
|
);
|
|
1669
1734
|
|
|
1670
1735
|
useEffect(() => {
|
|
@@ -2774,6 +2839,7 @@ export function AdminChangeBookingFlow({
|
|
|
2774
2839
|
*/
|
|
2775
2840
|
const changeFlowProtectedReturnAdjustment = useMemo(() => {
|
|
2776
2841
|
if (totalQuantity <= 0) return returnPriceAdjustment;
|
|
2842
|
+
if (isProviderDashboardChange) return returnPriceAdjustment;
|
|
2777
2843
|
if (effectiveChangeFlowReturnUnitFloorPerPerson == null) return returnPriceAdjustment;
|
|
2778
2844
|
const livePerPerson =
|
|
2779
2845
|
returnOptionCatalogPerPerson ?? (selectedReturnOption?.priceAdjustmentByCurrency?.[currency] ?? 0);
|
|
@@ -2787,6 +2853,7 @@ export function AdminChangeBookingFlow({
|
|
|
2787
2853
|
}, [
|
|
2788
2854
|
totalQuantity,
|
|
2789
2855
|
returnPriceAdjustment,
|
|
2856
|
+
isProviderDashboardChange,
|
|
2790
2857
|
effectiveChangeFlowReturnUnitFloorPerPerson,
|
|
2791
2858
|
selectedReturnOption,
|
|
2792
2859
|
returnOptionCatalogPerPerson,
|
|
@@ -2877,6 +2944,29 @@ export function AdminChangeBookingFlow({
|
|
|
2877
2944
|
return [...feeLineItems, ...addOnLines];
|
|
2878
2945
|
}, [feeLineItems, addOnSelections, addOns]);
|
|
2879
2946
|
|
|
2947
|
+
const providerPricingUi = flowUi?.providerDashboardChangePricingUi;
|
|
2948
|
+
const providerQuotedLines = providerPricingUi?.quotedLines ?? [];
|
|
2949
|
+
const providerEditableLines = providerQuotedLines.filter((line) => isManualOverrideEligibleLine(line));
|
|
2950
|
+
const showProviderPricingInlineEditor =
|
|
2951
|
+
isProviderDashboardChange && isAdmin && (
|
|
2952
|
+
providerPricingUi?.loading ||
|
|
2953
|
+
providerPricingUi?.error != null ||
|
|
2954
|
+
providerQuotedLines.length > 0
|
|
2955
|
+
);
|
|
2956
|
+
const normalizeReceiptLabel = (value: string): string =>
|
|
2957
|
+
value
|
|
2958
|
+
.toLowerCase()
|
|
2959
|
+
.replace(/\([^)]*\)/g, '')
|
|
2960
|
+
.replace(/[^a-z0-9]+/g, '')
|
|
2961
|
+
.trim();
|
|
2962
|
+
const providerEditableLineByNormalizedLabel = useMemo(() => {
|
|
2963
|
+
const m = new Map<string, (typeof providerEditableLines)[number]>();
|
|
2964
|
+
for (const line of providerEditableLines) {
|
|
2965
|
+
const key = normalizeReceiptLabel(line.label ?? '');
|
|
2966
|
+
if (key) m.set(key, line);
|
|
2967
|
+
}
|
|
2968
|
+
return m;
|
|
2969
|
+
}, [providerEditableLines]);
|
|
2880
2970
|
|
|
2881
2971
|
const checkoutPriceSummaryLines = useMemo((): PriceSummaryLine[] => {
|
|
2882
2972
|
if (!selectedAvailability) return [];
|
|
@@ -2894,6 +2984,13 @@ export function AdminChangeBookingFlow({
|
|
|
2894
2984
|
);
|
|
2895
2985
|
return {
|
|
2896
2986
|
kind: 'ticket',
|
|
2987
|
+
lineKey:
|
|
2988
|
+
showProviderPricingInlineEditor
|
|
2989
|
+
? providerEditableLineByNormalizedLabel.get(normalizeReceiptLabel(line.category))?.lineKey
|
|
2990
|
+
: undefined,
|
|
2991
|
+
editable:
|
|
2992
|
+
showProviderPricingInlineEditor &&
|
|
2993
|
+
providerEditableLineByNormalizedLabel.has(normalizeReceiptLabel(line.category)),
|
|
2897
2994
|
category: line.category,
|
|
2898
2995
|
qty: line.qty,
|
|
2899
2996
|
itemTotal: line.itemTotal,
|
|
@@ -2928,6 +3025,25 @@ export function AdminChangeBookingFlow({
|
|
|
2928
3025
|
fee.name.toLowerCase().includes('license'));
|
|
2929
3026
|
return {
|
|
2930
3027
|
kind: 'line' as const,
|
|
3028
|
+
lineKey:
|
|
3029
|
+
showProviderPricingInlineEditor
|
|
3030
|
+
? providerEditableLineByNormalizedLabel.get(
|
|
3031
|
+
normalizeReceiptLabel(
|
|
3032
|
+
feeLineItems.some((f) => f.name === fee.name)
|
|
3033
|
+
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
3034
|
+
: fee.name
|
|
3035
|
+
)
|
|
3036
|
+
)?.lineKey
|
|
3037
|
+
: undefined,
|
|
3038
|
+
editable:
|
|
3039
|
+
showProviderPricingInlineEditor &&
|
|
3040
|
+
providerEditableLineByNormalizedLabel.has(
|
|
3041
|
+
normalizeReceiptLabel(
|
|
3042
|
+
feeLineItems.some((f) => f.name === fee.name)
|
|
3043
|
+
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
3044
|
+
: fee.name
|
|
3045
|
+
)
|
|
3046
|
+
),
|
|
2931
3047
|
label: feeLineItems.some((f) => f.name === fee.name)
|
|
2932
3048
|
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
2933
3049
|
: fee.name,
|
|
@@ -2953,6 +3069,8 @@ export function AdminChangeBookingFlow({
|
|
|
2953
3069
|
effectiveCancellationPolicyLabel,
|
|
2954
3070
|
feeLineItemsWithAddOns,
|
|
2955
3071
|
feeLineItems,
|
|
3072
|
+
showProviderPricingInlineEditor,
|
|
3073
|
+
providerEditableLineByNormalizedLabel,
|
|
2956
3074
|
]);
|
|
2957
3075
|
|
|
2958
3076
|
const checkoutPriceSummaryLinesForCheckout = useMemo(() => {
|
|
@@ -3456,7 +3574,8 @@ export function AdminChangeBookingFlow({
|
|
|
3456
3574
|
countsChanged ||
|
|
3457
3575
|
addOnsChanged ||
|
|
3458
3576
|
returnChanged,
|
|
3459
|
-
// Authoritative for "real user change" gating
|
|
3577
|
+
// Authoritative for "real user change" gating in provider dashboard:
|
|
3578
|
+
// ignore option-id noise and only consider user-visible booking deltas.
|
|
3460
3579
|
hasOperationalChangesFromInitial:
|
|
3461
3580
|
dateChanged ||
|
|
3462
3581
|
pickupChanged ||
|
|
@@ -3483,28 +3602,62 @@ export function AdminChangeBookingFlow({
|
|
|
3483
3602
|
addOnSelections,
|
|
3484
3603
|
initialAddOnMinQtyByKey,
|
|
3485
3604
|
]);
|
|
3486
|
-
const hasChangeSelection =
|
|
3605
|
+
const hasChangeSelection =
|
|
3606
|
+
isProviderDashboardChange
|
|
3607
|
+
? changeSelectionDetails.hasOperationalChangesFromInitial
|
|
3608
|
+
: changeSelectionDetails.hasChangesFromInitial;
|
|
3487
3609
|
|
|
3488
3610
|
const changeFlowNeedsServerPrice =
|
|
3611
|
+
isCustomerSelfServeChange &&
|
|
3489
3612
|
hasChangeSelection &&
|
|
3490
3613
|
!!initialValues?.bookingReference?.trim() &&
|
|
3491
3614
|
!!lastName.trim();
|
|
3492
3615
|
|
|
3493
|
-
const isChangeQuoteBlocked = latestChangeQuote?.canProceed === false;
|
|
3494
|
-
const requiresReturnInChangeFlow = !!initialValues?.returnAvailabilityId?.trim();
|
|
3616
|
+
const isChangeQuoteBlocked = isCustomerSelfServeChange && latestChangeQuote?.canProceed === false;
|
|
3617
|
+
const requiresReturnInChangeFlow = isCustomerSelfServeChange && !!initialValues?.returnAvailabilityId?.trim();
|
|
3495
3618
|
const missingRequiredReturnSelection = requiresReturnInChangeFlow && !selectedReturnOption;
|
|
3496
3619
|
|
|
3497
3620
|
const changeFlowSubmitDisabled =
|
|
3498
3621
|
missingRequiredReturnSelection ||
|
|
3499
|
-
(
|
|
3622
|
+
(isCustomerSelfServeChange &&
|
|
3623
|
+
changeFlowNeedsServerPrice &&
|
|
3500
3624
|
(changeQuoteLoading || (!latestChangeQuote && !changeQuoteFetchError)));
|
|
3501
3625
|
|
|
3502
|
-
const
|
|
3626
|
+
const providerTotalsPreview =
|
|
3627
|
+
showProviderPricingInlineEditor && providerPricingUi?.totalsPreview
|
|
3628
|
+
? providerPricingUi.totalsPreview
|
|
3629
|
+
: null;
|
|
3630
|
+
const providerHasEditedLineOverrides =
|
|
3631
|
+
isProviderDashboardChange &&
|
|
3632
|
+
providerQuotedLines.some((line) => {
|
|
3633
|
+
if (!isManualOverrideEligibleLine(line)) return false;
|
|
3634
|
+
const raw = providerPricingUi?.lineAmountInputs?.[line.lineKey];
|
|
3635
|
+
const parsed = raw == null || raw.trim() === '' ? line.amount : Number(raw);
|
|
3636
|
+
if (!Number.isFinite(parsed)) return false;
|
|
3637
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
3638
|
+
return Math.abs(rounded - line.amount) > 0.0001;
|
|
3639
|
+
});
|
|
3640
|
+
const providerHasAdditionalAdjustments =
|
|
3641
|
+
isProviderDashboardChange &&
|
|
3642
|
+
(providerPricingUi?.additionalAdjustments ?? []).some((adj) => {
|
|
3643
|
+
const parsed = Number((adj.amountInput ?? '').trim());
|
|
3644
|
+
const hasAmount = Number.isFinite(parsed) && parsed > 0;
|
|
3645
|
+
const currentAmount = hasAmount ? (Math.round(parsed * 100) / 100).toFixed(2) : '';
|
|
3646
|
+
const originalAmount = adj.originalAmountInput ?? '';
|
|
3647
|
+
const currentLabel = (adj.label ?? '').trim();
|
|
3648
|
+
const originalLabel = (adj.originalLabel ?? '').trim();
|
|
3649
|
+
const currentMode = adj.mode;
|
|
3650
|
+
const originalMode = adj.originalMode;
|
|
3651
|
+
if (!originalMode) return hasAmount || currentLabel.length > 0;
|
|
3652
|
+
return currentAmount !== originalAmount || currentLabel !== originalLabel || currentMode !== originalMode;
|
|
3653
|
+
});
|
|
3654
|
+
const hasEffectiveChangeSelection =
|
|
3655
|
+
hasChangeSelection || providerHasEditedLineOverrides || providerHasAdditionalAdjustments;
|
|
3503
3656
|
|
|
3504
3657
|
const displayedChangeAmounts = resolveChangeFlowDisplayedAmounts({
|
|
3505
|
-
providerPreview:
|
|
3658
|
+
providerPreview: providerTotalsPreview,
|
|
3506
3659
|
serverQuotePreview:
|
|
3507
|
-
latestChangeQuote?.serverDisplay
|
|
3660
|
+
isCustomerSelfServeChange && latestChangeQuote?.serverDisplay
|
|
3508
3661
|
? latestChangeQuote.serverDisplay
|
|
3509
3662
|
: null,
|
|
3510
3663
|
fromCart: {
|
|
@@ -3520,13 +3673,13 @@ export function AdminChangeBookingFlow({
|
|
|
3520
3673
|
const changeFlowClientEstimateDue = (() => {
|
|
3521
3674
|
if (!originalReceipt) return totalPrice;
|
|
3522
3675
|
// Customer self-serve: amount due comes from POST .../change/quote (`amountDueCents` / priceDiff), not FE delta math.
|
|
3523
|
-
if (latestChangeQuote != null && !changeQuoteFetchError) {
|
|
3676
|
+
if (isCustomerSelfServeChange && latestChangeQuote != null && !changeQuoteFetchError) {
|
|
3524
3677
|
return normalizeNearZeroOwed(latestChangeQuote.priceDiff);
|
|
3525
3678
|
}
|
|
3526
3679
|
return changeFlowBalanceVsOriginal({
|
|
3527
3680
|
newTotal: displayChangeFlowProposedTotal,
|
|
3528
3681
|
originalReceiptTotal: originalReceipt.total,
|
|
3529
|
-
audience: 'customer',
|
|
3682
|
+
audience: isProviderDashboardChange ? 'provider' : 'customer',
|
|
3530
3683
|
});
|
|
3531
3684
|
})();
|
|
3532
3685
|
|
|
@@ -3535,6 +3688,14 @@ export function AdminChangeBookingFlow({
|
|
|
3535
3688
|
|
|
3536
3689
|
const changeCheckoutButtonLabel = (() => {
|
|
3537
3690
|
if (!hasEffectiveChangeSelection) return undefined;
|
|
3691
|
+
if (isProviderDashboardChange) {
|
|
3692
|
+
const est = Math.round(changeFlowClientEstimateDue * 100) / 100;
|
|
3693
|
+
return est > 0
|
|
3694
|
+
? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
|
|
3695
|
+
: est < 0
|
|
3696
|
+
? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
|
|
3697
|
+
: 'Change booking (no charge)';
|
|
3698
|
+
}
|
|
3538
3699
|
if (changeFlowNeedsServerPrice) {
|
|
3539
3700
|
if (changeQuoteLoading) {
|
|
3540
3701
|
const tr = t('booking.updatingPrice');
|
|
@@ -3568,8 +3729,39 @@ export function AdminChangeBookingFlow({
|
|
|
3568
3729
|
const checkoutFormError =
|
|
3569
3730
|
(error || '') ||
|
|
3570
3731
|
(missingRequiredReturnSelection ? 'Removing return option in self-serve is not available. Please contact support.' : '') ||
|
|
3571
|
-
(isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
|
|
3572
|
-
(changeQuoteFetchError ?? '');
|
|
3732
|
+
(isCustomerSelfServeChange && isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
|
|
3733
|
+
(isCustomerSelfServeChange ? changeQuoteFetchError ?? '' : '');
|
|
3734
|
+
|
|
3735
|
+
const providerPricingOverrides =
|
|
3736
|
+
isProviderDashboardChange && providerQuotedLines.length > 0
|
|
3737
|
+
? providerQuotedLines
|
|
3738
|
+
.filter((line) => isManualOverrideEligibleLine(line))
|
|
3739
|
+
.map((line) => {
|
|
3740
|
+
const raw = providerPricingUi?.lineAmountInputs?.[line.lineKey];
|
|
3741
|
+
const parsed = raw == null || raw.trim() === '' ? line.amount : Number(raw);
|
|
3742
|
+
if (!Number.isFinite(parsed)) return null;
|
|
3743
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
3744
|
+
return Math.abs(rounded - line.amount) > 0.0001
|
|
3745
|
+
? { lineKey: line.lineKey, amount: rounded, reason: 'Provider dashboard override' }
|
|
3746
|
+
: null;
|
|
3747
|
+
})
|
|
3748
|
+
.filter((v): v is { lineKey: string; amount: number; reason: string } => v != null)
|
|
3749
|
+
: [];
|
|
3750
|
+
const providerAdditionalAdjustments =
|
|
3751
|
+
isProviderDashboardChange
|
|
3752
|
+
? (providerPricingUi?.additionalAdjustments ?? [])
|
|
3753
|
+
.map((adj) => {
|
|
3754
|
+
const parsed = Number((adj.amountInput ?? '').trim());
|
|
3755
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
3756
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
3757
|
+
const signed = adj.mode === 'DISCOUNT' ? -rounded : rounded;
|
|
3758
|
+
return {
|
|
3759
|
+
label: adj.label?.trim() || (signed < 0 ? 'Provider discount' : 'Provider charge'),
|
|
3760
|
+
amount: signed,
|
|
3761
|
+
};
|
|
3762
|
+
})
|
|
3763
|
+
.filter((v): v is { label: string; amount: number } => v != null)
|
|
3764
|
+
: [];
|
|
3573
3765
|
|
|
3574
3766
|
const changeFlowSelectionPreview = useMemo((): ChangeFlowSelectionPreview | null => {
|
|
3575
3767
|
if (!selectedAvailability || totalQuantity <= 0) return null;
|
|
@@ -3817,7 +4009,8 @@ export function AdminChangeBookingFlow({
|
|
|
3817
4009
|
timesForSelectedDate,
|
|
3818
4010
|
anchor,
|
|
3819
4011
|
companyTimezone,
|
|
3820
|
-
optionsMap
|
|
4012
|
+
optionsMap,
|
|
4013
|
+
isAdmin
|
|
3821
4014
|
);
|
|
3822
4015
|
changeFlowOutboundAnchorRef.current = null;
|
|
3823
4016
|
if (matched) {
|
|
@@ -3908,7 +4101,8 @@ export function AdminChangeBookingFlow({
|
|
|
3908
4101
|
const fromAnchor = pickReturnMatchingPreviousSelection(
|
|
3909
4102
|
sorted,
|
|
3910
4103
|
returnAnchor,
|
|
3911
|
-
companyTimezone
|
|
4104
|
+
companyTimezone,
|
|
4105
|
+
isAdmin
|
|
3912
4106
|
);
|
|
3913
4107
|
changeFlowReturnAnchorRef.current = null;
|
|
3914
4108
|
if (fromAnchor) {
|
|
@@ -4231,6 +4425,8 @@ export function AdminChangeBookingFlow({
|
|
|
4231
4425
|
return;
|
|
4232
4426
|
}
|
|
4233
4427
|
|
|
4428
|
+
const skipContactFields = isProviderDashboardChange;
|
|
4429
|
+
if (!skipContactFields) {
|
|
4234
4430
|
// Validate email (required)
|
|
4235
4431
|
if (!email) {
|
|
4236
4432
|
setError(t('booking.enterEmail') || 'Please enter your email address');
|
|
@@ -4253,6 +4449,7 @@ export function AdminChangeBookingFlow({
|
|
|
4253
4449
|
setError(t('booking.enterLastName') || 'Please enter your last name');
|
|
4254
4450
|
return;
|
|
4255
4451
|
}
|
|
4452
|
+
}
|
|
4256
4453
|
|
|
4257
4454
|
// Allow checkout if pickup location is selected OR if user chose "I don't know"
|
|
4258
4455
|
if (product.pickupLocations && product.pickupLocations.length > 0 && !pickupLocationId && !pickupLocationSkipped) {
|
|
@@ -4279,6 +4476,45 @@ export function AdminChangeBookingFlow({
|
|
|
4279
4476
|
return;
|
|
4280
4477
|
}
|
|
4281
4478
|
|
|
4479
|
+
if (onChangeBooking && !deferProviderApplyToAdminPaymentChoice) {
|
|
4480
|
+
const pickupForChange = pickupLocationId
|
|
4481
|
+
? product.pickupLocations?.find((loc) => loc.id === pickupLocationId)
|
|
4482
|
+
: null;
|
|
4483
|
+
await onChangeBooking({
|
|
4484
|
+
productId: availabilityProductOptionId,
|
|
4485
|
+
dateTime: selectedAvailability.dateTime,
|
|
4486
|
+
bookingItems,
|
|
4487
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
4488
|
+
pickupLocationId: pickupLocationId ?? null,
|
|
4489
|
+
travelerHotel: pickupForChange?.name ?? null,
|
|
4490
|
+
startTime: selectedAvailability.dateTime ?? null,
|
|
4491
|
+
passengerCount: null,
|
|
4492
|
+
childSafetySeatsCount: null,
|
|
4493
|
+
foodRestrictions: null,
|
|
4494
|
+
addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
|
|
4495
|
+
cancellationPolicyId: cancellationPolicyId ?? initialValues?.cancellationPolicyId ?? null,
|
|
4496
|
+
promoCode: appliedPromoCode ?? null,
|
|
4497
|
+
newTotalAmount: displayChangeFlowProposedTotal,
|
|
4498
|
+
additionalHoursCount: null,
|
|
4499
|
+
pricingAdjustment:
|
|
4500
|
+
providerPricingOverrides.length > 0 || providerAdditionalAdjustments.length > 0
|
|
4501
|
+
? {
|
|
4502
|
+
mode: 'MANUAL_LINES',
|
|
4503
|
+
lineOverrides: providerPricingOverrides,
|
|
4504
|
+
additionalAdjustments: providerAdditionalAdjustments,
|
|
4505
|
+
}
|
|
4506
|
+
: undefined,
|
|
4507
|
+
capacitySeatCredit: {
|
|
4508
|
+
enabled: true,
|
|
4509
|
+
previousPassengerCount: changeFlowInitialTicketCount,
|
|
4510
|
+
previousAvailabilityId: initialValues?.availabilityId ?? null,
|
|
4511
|
+
previousReturnAvailabilityId: initialValues?.returnAvailabilityId ?? null,
|
|
4512
|
+
},
|
|
4513
|
+
});
|
|
4514
|
+
setLoading(false);
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4282
4518
|
const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
|
|
4283
4519
|
clientChannelSource: inferClientBookingSourceFromProductIds(
|
|
4284
4520
|
product.productId,
|
|
@@ -4296,6 +4532,7 @@ export function AdminChangeBookingFlow({
|
|
|
4296
4532
|
let changeIntentIdForCheckout: string | undefined;
|
|
4297
4533
|
let changeBookingReferenceForPaidFlow: string | undefined;
|
|
4298
4534
|
|
|
4535
|
+
if (isCustomerSelfServeChange) {
|
|
4299
4536
|
const changeBookingReference = initialValues?.bookingReference?.trim();
|
|
4300
4537
|
const changeLastName = lastName.trim();
|
|
4301
4538
|
if (!changeBookingReference || !changeLastName) {
|
|
@@ -4383,12 +4620,13 @@ export function AdminChangeBookingFlow({
|
|
|
4383
4620
|
if (!changeIntentIdForCheckout) {
|
|
4384
4621
|
throw new Error('Missing change intent for payment.');
|
|
4385
4622
|
}
|
|
4623
|
+
}
|
|
4386
4624
|
|
|
4387
4625
|
pendingReservationRef.current = null;
|
|
4388
4626
|
|
|
4389
4627
|
// Note: Do NOT call onSuccess here for paid bookings — we're about to show the Stripe
|
|
4390
4628
|
// CheckoutModal. onSuccess (e.g. closing the parent dialog) should only run when we're
|
|
4391
|
-
// actually done (free booking redirect). Calling it here
|
|
4629
|
+
// actually done (free booking redirect, admin confirm-without-payment). Calling it here
|
|
4392
4630
|
// would close the dialog before the payment modal opens.
|
|
4393
4631
|
|
|
4394
4632
|
// Update stored booking data (no holds reservation — change flow keys off booking reference elsewhere).
|
|
@@ -4414,11 +4652,13 @@ export function AdminChangeBookingFlow({
|
|
|
4414
4652
|
// Build checkout breakdown from the exact same values we show in the UI and Stripe modal.
|
|
4415
4653
|
// Backend will charge totalAmount and store this as the receipt so /manage matches.
|
|
4416
4654
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
4417
|
-
const amountDueForCheckout =
|
|
4655
|
+
const amountDueForCheckout = isCustomerSelfServeChange
|
|
4656
|
+
? changeFlowBalanceVsOriginal({
|
|
4418
4657
|
newTotal: changeFlowNewBookingTotal,
|
|
4419
4658
|
originalReceiptTotal: originalReceipt?.total ?? 0,
|
|
4420
4659
|
audience: 'customer',
|
|
4421
|
-
})
|
|
4660
|
+
})
|
|
4661
|
+
: totalPrice;
|
|
4422
4662
|
const lines = [
|
|
4423
4663
|
...ticketLineItemsForChangeFlowDisplay.map((line) => ({
|
|
4424
4664
|
label: line.category,
|
|
@@ -4495,7 +4735,8 @@ export function AdminChangeBookingFlow({
|
|
|
4495
4735
|
roundingLabel: t('booking.rounding') || 'Rounding',
|
|
4496
4736
|
});
|
|
4497
4737
|
|
|
4498
|
-
const paymentIntent =
|
|
4738
|
+
const paymentIntent = isCustomerSelfServeChange
|
|
4739
|
+
? await createChangeBookingPaymentIntent(
|
|
4499
4740
|
(() => {
|
|
4500
4741
|
const id = changeIntentIdForCheckout ?? latestChangeQuote?.changeIntentId;
|
|
4501
4742
|
if (!id) {
|
|
@@ -4503,7 +4744,76 @@ export function AdminChangeBookingFlow({
|
|
|
4503
4744
|
}
|
|
4504
4745
|
return id;
|
|
4505
4746
|
})()
|
|
4506
|
-
)
|
|
4747
|
+
)
|
|
4748
|
+
: await createPaymentIntent({
|
|
4749
|
+
productId: product.productId,
|
|
4750
|
+
optionId: availabilityProductOptionId,
|
|
4751
|
+
date: datePart,
|
|
4752
|
+
time: timePart,
|
|
4753
|
+
quantity: totalQuantity,
|
|
4754
|
+
customerEmail: email,
|
|
4755
|
+
customerFirstName: firstName.trim() || undefined,
|
|
4756
|
+
customerLastName: lastName.trim() || undefined,
|
|
4757
|
+
currency: currency,
|
|
4758
|
+
travelerHotel: selectedPickupLocation?.name || undefined,
|
|
4759
|
+
pickupLocationId: pickupLocationId || undefined,
|
|
4760
|
+
itineraryDisplay: itineraryDisplay ?? undefined,
|
|
4761
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId,
|
|
4762
|
+
promoCode: (lockedPromoCode || appliedPromoCode) || undefined,
|
|
4763
|
+
cancellationPolicyId: cancellationPolicyId || undefined,
|
|
4764
|
+
termsAcceptedAt: termsAcceptedAt ?? undefined,
|
|
4765
|
+
checkoutBreakdown,
|
|
4766
|
+
skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
|
|
4767
|
+
disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
|
|
4768
|
+
...bookingSourceContext,
|
|
4769
|
+
});
|
|
4770
|
+
|
|
4771
|
+
// Admin: show choice to pay now or confirm without payment (customer owes full balance)
|
|
4772
|
+
if (isAdmin) {
|
|
4773
|
+
const adminReservationRef =
|
|
4774
|
+
initialValues?.bookingReference?.trim() ??
|
|
4775
|
+
changeBookingReferenceForPaidFlow ??
|
|
4776
|
+
'';
|
|
4777
|
+
if (!adminReservationRef) {
|
|
4778
|
+
throw new Error('Missing reservation reference for admin payment flow');
|
|
4779
|
+
}
|
|
4780
|
+
setError('');
|
|
4781
|
+
setAdminChoiceData({
|
|
4782
|
+
reservationReference: adminReservationRef,
|
|
4783
|
+
reservationExpiration: undefined,
|
|
4784
|
+
checkoutBreakdown,
|
|
4785
|
+
totalAmount: amountDueForCheckout,
|
|
4786
|
+
datePart,
|
|
4787
|
+
timePart,
|
|
4788
|
+
availabilityProductOptionId,
|
|
4789
|
+
itineraryDisplay: itineraryDisplay ?? undefined,
|
|
4790
|
+
clientSecret: paymentIntent.clientSecret ?? '',
|
|
4791
|
+
ticketLinesForModal: ticketLineItemsForChangeFlowDisplay.map((line) => {
|
|
4792
|
+
const rate = pricing.find((r) => r.category === line.category);
|
|
4793
|
+
const breakdown = getPriceBreakdown(
|
|
4794
|
+
line.category,
|
|
4795
|
+
rate?.priceCAD ?? 0,
|
|
4796
|
+
rate?.baseInDisplayCurrency,
|
|
4797
|
+
rate?.appliedAdjustments ?? []
|
|
4798
|
+
);
|
|
4799
|
+
return { line, breakdown };
|
|
4800
|
+
}),
|
|
4801
|
+
feeLineItems: feeLineItemsWithAddOns,
|
|
4802
|
+
returnPriceAdjustment: checkoutReturnLineAmount,
|
|
4803
|
+
cancellationPolicyFee,
|
|
4804
|
+
cancellationPolicyLabel: effectiveCancellationPolicyLabel,
|
|
4805
|
+
subtotal: effectiveSubtotal,
|
|
4806
|
+
tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
|
|
4807
|
+
totalQuantity,
|
|
4808
|
+
isTaxIncludedInPrice,
|
|
4809
|
+
taxRate: pricingConfig?.taxRate ?? 0,
|
|
4810
|
+
promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
|
|
4811
|
+
discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : (originalReceipt?.promoLabel || undefined),
|
|
4812
|
+
});
|
|
4813
|
+
setShowAdminPaymentChoice(true);
|
|
4814
|
+
setLoading(false);
|
|
4815
|
+
return;
|
|
4816
|
+
}
|
|
4507
4817
|
|
|
4508
4818
|
const ticketLinesForModal: CheckoutModalLineItem[] = ticketLineItemsForChangeFlowDisplay.map((line) => {
|
|
4509
4819
|
const rate = pricing.find((r) => r.category === line.category);
|
|
@@ -4525,7 +4835,7 @@ export function AdminChangeBookingFlow({
|
|
|
4525
4835
|
// Paid change: always return to stable ref+lastName + explicit intent (not reservationRef).
|
|
4526
4836
|
// /manage-booking runs bounded refresh only when `from=change_payment` (see manage-booking page).
|
|
4527
4837
|
successUrlOverride:
|
|
4528
|
-
changeBookingReferenceForPaidFlow
|
|
4838
|
+
isCustomerSelfServeChange && changeBookingReferenceForPaidFlow
|
|
4529
4839
|
? (() => {
|
|
4530
4840
|
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
4531
4841
|
const ref = encodeURIComponent(
|
|
@@ -4553,7 +4863,7 @@ export function AdminChangeBookingFlow({
|
|
|
4553
4863
|
promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
|
|
4554
4864
|
discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : (originalReceipt?.promoLabel || undefined),
|
|
4555
4865
|
changeTotals:
|
|
4556
|
-
originalReceipt
|
|
4866
|
+
isCustomerSelfServeChange && originalReceipt
|
|
4557
4867
|
? {
|
|
4558
4868
|
previousTotal: originalReceipt.total,
|
|
4559
4869
|
newTotal: displayChangeFlowProposedTotal,
|
|
@@ -4622,6 +4932,162 @@ export function AdminChangeBookingFlow({
|
|
|
4622
4932
|
}
|
|
4623
4933
|
};
|
|
4624
4934
|
|
|
4935
|
+
const handleConfirmWithoutPayment = async () => {
|
|
4936
|
+
if (!adminChoiceData) return;
|
|
4937
|
+
setLoading(true);
|
|
4938
|
+
setError('');
|
|
4939
|
+
try {
|
|
4940
|
+
if (onChangeBooking) {
|
|
4941
|
+
if (!selectedAvailability) {
|
|
4942
|
+
setError('No availability selected');
|
|
4943
|
+
setLoading(false);
|
|
4944
|
+
return;
|
|
4945
|
+
}
|
|
4946
|
+
const bookingItems = Object.entries(quantities)
|
|
4947
|
+
.filter(([, count]) => count > 0)
|
|
4948
|
+
.map(([category, count]) => ({ category, count }));
|
|
4949
|
+
const availabilityProductOptionId =
|
|
4950
|
+
adminChoiceData.availabilityProductOptionId ||
|
|
4951
|
+
selectedAvailability.productOptionId ||
|
|
4952
|
+
activeOptions[0]?.optionId;
|
|
4953
|
+
if (!availabilityProductOptionId) {
|
|
4954
|
+
setError('No product option selected');
|
|
4955
|
+
setLoading(false);
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
const pickupForChange = pickupLocationId
|
|
4959
|
+
? product.pickupLocations?.find((loc) => loc.id === pickupLocationId)
|
|
4960
|
+
: null;
|
|
4961
|
+
await onChangeBooking({
|
|
4962
|
+
productId: availabilityProductOptionId,
|
|
4963
|
+
dateTime: selectedAvailability.dateTime,
|
|
4964
|
+
bookingItems,
|
|
4965
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
4966
|
+
pickupLocationId: pickupLocationId ?? null,
|
|
4967
|
+
travelerHotel: pickupForChange?.name ?? null,
|
|
4968
|
+
startTime: selectedAvailability.dateTime ?? null,
|
|
4969
|
+
passengerCount: null,
|
|
4970
|
+
childSafetySeatsCount: null,
|
|
4971
|
+
foodRestrictions: null,
|
|
4972
|
+
addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
|
|
4973
|
+
cancellationPolicyId: cancellationPolicyId ?? initialValues?.cancellationPolicyId ?? null,
|
|
4974
|
+
promoCode: appliedPromoCode ?? null,
|
|
4975
|
+
newTotalAmount: displayChangeFlowProposedTotal,
|
|
4976
|
+
additionalHoursCount: null,
|
|
4977
|
+
pricingAdjustment:
|
|
4978
|
+
providerPricingOverrides.length > 0 || providerAdditionalAdjustments.length > 0
|
|
4979
|
+
? {
|
|
4980
|
+
mode: 'MANUAL_LINES',
|
|
4981
|
+
lineOverrides: providerPricingOverrides,
|
|
4982
|
+
additionalAdjustments: providerAdditionalAdjustments,
|
|
4983
|
+
}
|
|
4984
|
+
: undefined,
|
|
4985
|
+
capacitySeatCredit: {
|
|
4986
|
+
enabled: true,
|
|
4987
|
+
previousPassengerCount: changeFlowInitialTicketCount,
|
|
4988
|
+
previousAvailabilityId: initialValues?.availabilityId ?? null,
|
|
4989
|
+
previousReturnAvailabilityId: initialValues?.returnAvailabilityId ?? null,
|
|
4990
|
+
},
|
|
4991
|
+
});
|
|
4992
|
+
const bookRef = initialValues?.bookingReference?.trim() || adminChoiceData.reservationReference;
|
|
4993
|
+
setShowAdminPaymentChoice(false);
|
|
4994
|
+
setAdminChoiceData(null);
|
|
4995
|
+
onSuccess?.({ reservationReference: bookRef });
|
|
4996
|
+
const ref = formatBookingRefForDisplay(bookRef);
|
|
4997
|
+
const ln = lastName.trim();
|
|
4998
|
+
if (onShowManage) {
|
|
4999
|
+
onShowManage({ ref, lastName: ln });
|
|
5000
|
+
} else {
|
|
5001
|
+
const params = new URLSearchParams({ ref, lastName: ln, booking_complete: '1' });
|
|
5002
|
+
window.location.href = `/manage-booking?${params.toString()}`;
|
|
5003
|
+
}
|
|
5004
|
+
setLoading(false);
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
|
|
5008
|
+
if (isChangeBookingContext) {
|
|
5009
|
+
setError(
|
|
5010
|
+
'Confirming a booking change without payment requires your dashboard to pass the provider change handler (onChangeBooking). Use Pay now to complete this change, or wire onChangeBooking in the embed.',
|
|
5011
|
+
);
|
|
5012
|
+
setLoading(false);
|
|
5013
|
+
return;
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
|
|
5017
|
+
clientChannelSource: inferClientBookingSourceFromProductIds(
|
|
5018
|
+
product.productId,
|
|
5019
|
+
adminChoiceData.availabilityProductOptionId,
|
|
5020
|
+
),
|
|
5021
|
+
forcePartnerPortalChannel: partnerPortalBooking,
|
|
5022
|
+
forceDashboardSource: bookingAppMode === 'provider-dashboard',
|
|
5023
|
+
});
|
|
5024
|
+
const result = await confirmBookingWithoutPayment({
|
|
5025
|
+
reservationReference: adminChoiceData.reservationReference,
|
|
5026
|
+
productId: product.productId,
|
|
5027
|
+
optionId: adminChoiceData.availabilityProductOptionId,
|
|
5028
|
+
date: adminChoiceData.datePart,
|
|
5029
|
+
time: adminChoiceData.timePart,
|
|
5030
|
+
customerEmail: email || undefined,
|
|
5031
|
+
customerFirstName: firstName.trim() || undefined,
|
|
5032
|
+
customerLastName: lastName.trim() || undefined,
|
|
5033
|
+
currency: currency,
|
|
5034
|
+
travelerHotel: product.pickupLocations?.find(loc => loc.id === pickupLocationId)?.name || undefined,
|
|
5035
|
+
pickupLocationId: pickupLocationId || undefined,
|
|
5036
|
+
itineraryDisplay: adminChoiceData.itineraryDisplay ?? undefined,
|
|
5037
|
+
termsAcceptedAt: termsAcceptedAt ?? undefined,
|
|
5038
|
+
skipConfirmationCommunications: skipConfirmationCommunications ? true : undefined,
|
|
5039
|
+
disableAutoCommunications: disableAutoCommunications ? true : undefined,
|
|
5040
|
+
checkoutBreakdown: adminChoiceData.checkoutBreakdown,
|
|
5041
|
+
depositAmount: 0,
|
|
5042
|
+
balanceAmount: adminChoiceData.totalAmount,
|
|
5043
|
+
totalAmount: adminChoiceData.totalAmount,
|
|
5044
|
+
...bookingSourceContext,
|
|
5045
|
+
});
|
|
5046
|
+
pendingReservationRef.current = null;
|
|
5047
|
+
const ref = formatBookingRefForDisplay(result.bookingReference);
|
|
5048
|
+
const ln = lastName.trim();
|
|
5049
|
+
setShowAdminPaymentChoice(false);
|
|
5050
|
+
setAdminChoiceData(null);
|
|
5051
|
+
onSuccess?.({ reservationReference: adminChoiceData.reservationReference });
|
|
5052
|
+
if (onShowManage) {
|
|
5053
|
+
onShowManage({ ref, lastName: ln });
|
|
5054
|
+
} else {
|
|
5055
|
+
const params = new URLSearchParams({ ref, lastName: ln, booking_complete: '1' });
|
|
5056
|
+
window.location.href = `/manage-booking?${params.toString()}`;
|
|
5057
|
+
}
|
|
5058
|
+
} catch (err) {
|
|
5059
|
+
setError(err instanceof Error ? err.message : 'Failed to confirm booking');
|
|
5060
|
+
} finally {
|
|
5061
|
+
setLoading(false);
|
|
5062
|
+
}
|
|
5063
|
+
};
|
|
5064
|
+
|
|
5065
|
+
const handlePayNow = () => {
|
|
5066
|
+
if (!adminChoiceData) return;
|
|
5067
|
+
setShowAdminPaymentChoice(false);
|
|
5068
|
+
setCheckoutClientSecret(adminChoiceData.clientSecret);
|
|
5069
|
+
setCheckoutModalData({
|
|
5070
|
+
reservationReference: adminChoiceData.reservationReference,
|
|
5071
|
+
reservationExpiration: adminChoiceData.reservationExpiration,
|
|
5072
|
+
customerLastName: lastName.trim(),
|
|
5073
|
+
ticketLines: adminChoiceData.ticketLinesForModal,
|
|
5074
|
+
feeLineItems: adminChoiceData.feeLineItems,
|
|
5075
|
+
returnPriceAdjustment: adminChoiceData.returnPriceAdjustment,
|
|
5076
|
+
cancellationPolicyFee: adminChoiceData.cancellationPolicyFee,
|
|
5077
|
+
cancellationPolicyLabel: adminChoiceData.cancellationPolicyLabel,
|
|
5078
|
+
subtotal: adminChoiceData.subtotal,
|
|
5079
|
+
tax: adminChoiceData.tax,
|
|
5080
|
+
total: adminChoiceData.totalAmount,
|
|
5081
|
+
totalQuantity: adminChoiceData.totalQuantity,
|
|
5082
|
+
isTaxIncludedInPrice: adminChoiceData.isTaxIncludedInPrice,
|
|
5083
|
+
taxRate: adminChoiceData.taxRate,
|
|
5084
|
+
promoDiscountAmount: adminChoiceData.promoDiscountAmount,
|
|
5085
|
+
discountLabel: adminChoiceData.discountLabel,
|
|
5086
|
+
});
|
|
5087
|
+
setShowCheckoutModal(true);
|
|
5088
|
+
setAdminChoiceData(null);
|
|
5089
|
+
};
|
|
5090
|
+
|
|
4625
5091
|
if (activeOptions.length === 0) {
|
|
4626
5092
|
return (
|
|
4627
5093
|
<div className="flex items-center justify-center py-16">
|
|
@@ -4632,6 +5098,17 @@ export function AdminChangeBookingFlow({
|
|
|
4632
5098
|
|
|
4633
5099
|
return (
|
|
4634
5100
|
<div className="booking-flow-root space-y-8">
|
|
5101
|
+
{/* Admin: choose to pay now or confirm without payment (full balance owed) */}
|
|
5102
|
+
<AdminPaymentChoiceModal
|
|
5103
|
+
open={!!(showAdminPaymentChoice && adminChoiceData)}
|
|
5104
|
+
totalAmount={adminChoiceData?.totalAmount ?? 0}
|
|
5105
|
+
currency={currency}
|
|
5106
|
+
loading={loading}
|
|
5107
|
+
error={error}
|
|
5108
|
+
onPayNow={handlePayNow}
|
|
5109
|
+
onConfirmWithoutPayment={handleConfirmWithoutPayment}
|
|
5110
|
+
onCancel={() => { setShowAdminPaymentChoice(false); setAdminChoiceData(null); setError(''); }}
|
|
5111
|
+
/>
|
|
4635
5112
|
{checkoutModalData && (
|
|
4636
5113
|
<CheckoutModal
|
|
4637
5114
|
open={showCheckoutModal}
|
|
@@ -4886,8 +5363,92 @@ export function AdminChangeBookingFlow({
|
|
|
4886
5363
|
currency={currency}
|
|
4887
5364
|
locale={locale}
|
|
4888
5365
|
t={t}
|
|
4889
|
-
extraBetweenTaxAndTotal={
|
|
4890
|
-
|
|
5366
|
+
extraBetweenTaxAndTotal={
|
|
5367
|
+
<>
|
|
5368
|
+
{showProviderPricingInlineEditor && providerPricingUi?.error ? (
|
|
5369
|
+
<div className="mt-2 text-sm text-red-700">{providerPricingUi.error}</div>
|
|
5370
|
+
) : null}
|
|
5371
|
+
{showProviderPricingInlineEditor &&
|
|
5372
|
+
providerPricingUi?.loading &&
|
|
5373
|
+
providerQuotedLines.length === 0 ? (
|
|
5374
|
+
<div className="mt-2 text-sm text-stone-500">Loading price lines...</div>
|
|
5375
|
+
) : null}
|
|
5376
|
+
{showProviderPricingInlineEditor &&
|
|
5377
|
+
providerPricingUi?.helperText &&
|
|
5378
|
+
!providerPricingUi.error ? (
|
|
5379
|
+
<div className="mt-2 text-xs text-stone-500">{providerPricingUi.helperText}</div>
|
|
5380
|
+
) : null}
|
|
5381
|
+
</>
|
|
5382
|
+
}
|
|
5383
|
+
extraBeforeSubtotal={
|
|
5384
|
+
showProviderPricingInlineEditor && (providerPricingUi?.additionalAdjustments?.length ?? 0) > 0 ? (
|
|
5385
|
+
<div className="space-y-1">
|
|
5386
|
+
{providerPricingUi?.additionalAdjustments?.map((adj) => (
|
|
5387
|
+
<div key={adj.id} className="flex items-center justify-between gap-2 text-sm">
|
|
5388
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
5389
|
+
<button
|
|
5390
|
+
type="button"
|
|
5391
|
+
className="rounded border border-stone-300 px-1 text-xs text-stone-600 hover:bg-stone-100"
|
|
5392
|
+
onClick={() => providerPricingUi?.onRemoveAdditionalAdjustment?.(adj.id)}
|
|
5393
|
+
>
|
|
5394
|
+
-
|
|
5395
|
+
</button>
|
|
5396
|
+
<input
|
|
5397
|
+
type="text"
|
|
5398
|
+
className="w-40 rounded border border-stone-300 px-2 py-0.5 text-sm"
|
|
5399
|
+
placeholder="Line description"
|
|
5400
|
+
value={adj.label}
|
|
5401
|
+
onChange={(e) =>
|
|
5402
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, { label: e.target.value })
|
|
5403
|
+
}
|
|
5404
|
+
/>
|
|
5405
|
+
</div>
|
|
5406
|
+
<div className="flex items-center gap-1">
|
|
5407
|
+
<select
|
|
5408
|
+
className="rounded border border-stone-300 px-1 py-0.5 text-xs"
|
|
5409
|
+
value={adj.mode}
|
|
5410
|
+
onChange={(e) =>
|
|
5411
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
5412
|
+
mode: e.target.value as 'DISCOUNT' | 'CHARGE',
|
|
5413
|
+
})
|
|
5414
|
+
}
|
|
5415
|
+
>
|
|
5416
|
+
<option value="DISCOUNT">-</option>
|
|
5417
|
+
<option value="CHARGE">+</option>
|
|
5418
|
+
</select>
|
|
5419
|
+
<input
|
|
5420
|
+
type="text"
|
|
5421
|
+
inputMode="decimal"
|
|
5422
|
+
className="h-6 w-24 rounded border border-stone-300 bg-white px-2 py-0.5 text-right text-sm font-medium leading-none text-stone-700"
|
|
5423
|
+
placeholder="0.00"
|
|
5424
|
+
value={adj.amountInput}
|
|
5425
|
+
onChange={(e) =>
|
|
5426
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
5427
|
+
amountInput: e.target.value,
|
|
5428
|
+
})
|
|
5429
|
+
}
|
|
5430
|
+
/>
|
|
5431
|
+
</div>
|
|
5432
|
+
</div>
|
|
5433
|
+
))}
|
|
5434
|
+
<button
|
|
5435
|
+
type="button"
|
|
5436
|
+
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
5437
|
+
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
5438
|
+
>
|
|
5439
|
+
+ add line item
|
|
5440
|
+
</button>
|
|
5441
|
+
</div>
|
|
5442
|
+
) : showProviderPricingInlineEditor ? (
|
|
5443
|
+
<button
|
|
5444
|
+
type="button"
|
|
5445
|
+
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
5446
|
+
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
5447
|
+
>
|
|
5448
|
+
+ add line item
|
|
5449
|
+
</button>
|
|
5450
|
+
) : undefined
|
|
5451
|
+
}
|
|
4891
5452
|
firstName={firstName}
|
|
4892
5453
|
lastName={lastName}
|
|
4893
5454
|
email={email}
|
|
@@ -4928,12 +5489,12 @@ export function AdminChangeBookingFlow({
|
|
|
4928
5489
|
setTermsAccepted(checked);
|
|
4929
5490
|
setTermsAcceptedAt(checked ? new Date().toISOString() : null);
|
|
4930
5491
|
}}
|
|
4931
|
-
isAdmin={
|
|
5492
|
+
isAdmin={isAdmin}
|
|
4932
5493
|
showCommunicationAdminSection={false}
|
|
4933
|
-
skipConfirmationCommunications={
|
|
4934
|
-
disableAutoCommunications={
|
|
4935
|
-
onSkipConfirmationChange={
|
|
4936
|
-
onDisableCommunicationsChange={
|
|
5494
|
+
skipConfirmationCommunications={skipConfirmationCommunications}
|
|
5495
|
+
disableAutoCommunications={disableAutoCommunications}
|
|
5496
|
+
onSkipConfirmationChange={setSkipConfirmationCommunications}
|
|
5497
|
+
onDisableCommunicationsChange={setDisableAutoCommunications}
|
|
4937
5498
|
error={checkoutFormError}
|
|
4938
5499
|
loading={loading}
|
|
4939
5500
|
totalQuantity={totalQuantity}
|
|
@@ -4941,6 +5502,7 @@ export function AdminChangeBookingFlow({
|
|
|
4941
5502
|
submitLabel={changeCheckoutButtonLabel ?? deferredInvoiceSubmitLabel}
|
|
4942
5503
|
hideSubmitButton={
|
|
4943
5504
|
showCheckoutModal ||
|
|
5505
|
+
showAdminPaymentChoice ||
|
|
4944
5506
|
!hasEffectiveChangeSelection ||
|
|
4945
5507
|
isChangeQuoteBlocked
|
|
4946
5508
|
}
|
|
@@ -4949,10 +5511,16 @@ export function AdminChangeBookingFlow({
|
|
|
4949
5511
|
attributionConfirmLabel={flowUi?.partnerAttributionConfirmLabel}
|
|
4950
5512
|
attributionConfirmed={partnerAttributionConfirmed}
|
|
4951
5513
|
onAttributionConfirmedChange={setPartnerAttributionConfirmed}
|
|
4952
|
-
lineAmountInputs={undefined}
|
|
4953
|
-
onLineAmountInputChange={
|
|
4954
|
-
|
|
4955
|
-
|
|
5514
|
+
lineAmountInputs={showProviderPricingInlineEditor ? providerPricingUi?.lineAmountInputs : undefined}
|
|
5515
|
+
onLineAmountInputChange={
|
|
5516
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountInputChange : undefined
|
|
5517
|
+
}
|
|
5518
|
+
onLineAmountInputBlur={
|
|
5519
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountInputBlur : undefined
|
|
5520
|
+
}
|
|
5521
|
+
onLineAmountReset={
|
|
5522
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountReset : undefined
|
|
5523
|
+
}
|
|
4956
5524
|
/>
|
|
4957
5525
|
</>
|
|
4958
5526
|
)}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useMemo, type ReactNode } from 'react';
|
|
4
|
-
import { createPortal } from 'react-dom';
|
|
5
4
|
import { loadStripe } from '@stripe/stripe-js';
|
|
6
5
|
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
|
7
6
|
import { useBookingHost } from '../../runtime';
|
|
@@ -238,10 +237,12 @@ export function CheckoutModal({
|
|
|
238
237
|
if (!env.STRIPE_PUBLISHABLE_KEY) {
|
|
239
238
|
const noStripe = (
|
|
240
239
|
<div
|
|
241
|
-
className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
|
|
240
|
+
className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50 pointer-events-auto"
|
|
242
241
|
style={{ zIndex: 100_000 }}
|
|
242
|
+
role="dialog"
|
|
243
|
+
aria-modal="true"
|
|
243
244
|
>
|
|
244
|
-
<div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6">
|
|
245
|
+
<div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6 pointer-events-auto" onClick={(e) => e.stopPropagation()}>
|
|
245
246
|
<h3 className="text-lg font-semibold text-stone-900 mb-2">
|
|
246
247
|
{t('booking.checkout') || 'Checkout'}
|
|
247
248
|
</h3>
|
|
@@ -258,7 +259,7 @@ export function CheckoutModal({
|
|
|
258
259
|
</div>
|
|
259
260
|
</div>
|
|
260
261
|
);
|
|
261
|
-
return
|
|
262
|
+
return noStripe;
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
const options = {
|
|
@@ -274,10 +275,15 @@ export function CheckoutModal({
|
|
|
274
275
|
|
|
275
276
|
const checkout = (
|
|
276
277
|
<div
|
|
277
|
-
className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
|
|
278
|
+
className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50 pointer-events-auto"
|
|
278
279
|
style={{ zIndex: 100_000 }}
|
|
280
|
+
role="dialog"
|
|
281
|
+
aria-modal="true"
|
|
279
282
|
>
|
|
280
|
-
<div
|
|
283
|
+
<div
|
|
284
|
+
className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col pointer-events-auto"
|
|
285
|
+
onClick={(e) => e.stopPropagation()}
|
|
286
|
+
>
|
|
281
287
|
<div className="p-6 border-b border-stone-200 flex-shrink-0">
|
|
282
288
|
<div className="flex justify-between items-start">
|
|
283
289
|
<h3 className="text-lg font-semibold text-stone-900">
|
|
@@ -449,5 +455,5 @@ export function CheckoutModal({
|
|
|
449
455
|
</div>
|
|
450
456
|
</div>
|
|
451
457
|
);
|
|
452
|
-
return
|
|
458
|
+
return checkout;
|
|
453
459
|
}
|
|
@@ -3,6 +3,7 @@ import type { Product } from '../../lib/booking-api';
|
|
|
3
3
|
import type { BookingSourceMetadata } from '../../lib/booking/source-metadata';
|
|
4
4
|
import type { Currency } from './CurrencySwitcher';
|
|
5
5
|
import type { BookingFlowUiOptions } from './booking-flow-ui';
|
|
6
|
+
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
6
7
|
|
|
7
8
|
/** Live selection snapshot for change-booking compare UI (parent dialog). */
|
|
8
9
|
export interface ChangeFlowSelectionPreview {
|
|
@@ -126,6 +127,8 @@ export interface ChangeBookingFlowProps extends BookingFlowBaseProps {
|
|
|
126
127
|
quantity?: number;
|
|
127
128
|
}>;
|
|
128
129
|
} | null;
|
|
130
|
+
/** Admin/provider dashboard integration hook (used by AdminChangeBookingFlow). */
|
|
131
|
+
onChangeBooking?: (data: ProviderDashboardChangeBookingPayload) => Promise<void>;
|
|
129
132
|
/**
|
|
130
133
|
* Embed compatibility (e.g. ticketbooth provider dashboard): ignored — same flow as the public site.
|
|
131
134
|
* When true, {@link BookingFlow} still renders the fork {@link AdminChangeBookingFlow} for future divergence.
|