@ticketboothapp/booking 1.2.63 → 1.2.64
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 +652 -121
- package/src/components/booking/BookingFlow.tsx +2 -3
- package/src/components/booking/ChangeBookingFlow.tsx +651 -118
- package/src/components/booking/ChangeBookingPricingDriftPanel.tsx +130 -5
- package/src/components/booking/booking-flow-types.ts +9 -2
- package/src/lib/booking/change-booking-pricing-drift.ts +157 -9
- package/src/lib/booking/change-flow-pricing.ts +5 -6
- package/src/lib/booking-api.ts +17 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
quoteChangeBooking,
|
|
12
12
|
confirmFreeChangeBooking,
|
|
13
13
|
createChangeBookingPaymentIntent,
|
|
14
|
+
confirmBookingWithoutPayment,
|
|
14
15
|
getAddOns,
|
|
15
16
|
validatePromoCode,
|
|
16
17
|
getPromoDiscount,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
} from '../../lib/booking-constants';
|
|
33
34
|
import { getSundayOfWeek } from '../../lib/booking/sunday-week';
|
|
34
35
|
import { Calendar } from './Calendar';
|
|
36
|
+
import { AdminPaymentChoiceModal } from './AdminPaymentChoiceModal';
|
|
35
37
|
import { ItineraryBox } from './ItineraryBox';
|
|
36
38
|
import { ItineraryPlaceholder } from './ItineraryPlaceholder';
|
|
37
39
|
import { PickupTimeSelector } from './PickupTimeSelector';
|
|
@@ -62,10 +64,11 @@ import {
|
|
|
62
64
|
} from '../../lib/booking/change-flow-pricing';
|
|
63
65
|
import {
|
|
64
66
|
mergePriceSummaryLinesForDrift,
|
|
67
|
+
mergeLineComparisonsWithFullDrift,
|
|
65
68
|
normalizePricingDriftDetailFromQuote,
|
|
66
69
|
normalizeTicketPricingTraceFromQuote,
|
|
67
70
|
sumPriceSummaryLinesMajorUnits,
|
|
68
|
-
|
|
71
|
+
computePricingDriftDelta,
|
|
69
72
|
} from '../../lib/booking/change-booking-pricing-drift';
|
|
70
73
|
import { ChangeBookingPricingDriftPanel } from './ChangeBookingPricingDriftPanel';
|
|
71
74
|
import {
|
|
@@ -96,11 +99,10 @@ 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
|
|
|
102
|
-
export type { ChangeBookingFlowProps } from './booking-flow-types';
|
|
103
|
-
|
|
104
106
|
/**
|
|
105
107
|
* ## Pricing contract (customer self-serve)
|
|
106
108
|
*
|
|
@@ -110,6 +112,7 @@ export type { ChangeBookingFlowProps } from './booking-flow-types';
|
|
|
110
112
|
*
|
|
111
113
|
* Until the first successful quote, `selfServeCheckoutPlaceholder` still avoids showing unchecked totals.
|
|
112
114
|
*
|
|
115
|
+
* **Provider dashboard** (`onChangeBooking`) — unchanged; manual pricing via `flowUi.providerDashboardChangePricingUi`.
|
|
113
116
|
*/
|
|
114
117
|
function mergeQuoteSliceWithServerPreview(
|
|
115
118
|
slice: ReturnType<typeof sliceChangeQuoteForUi>,
|
|
@@ -406,8 +409,9 @@ function pickOutboundMatchingPreviousSelection(
|
|
|
406
409
|
anchor: { productOptionId: string | null; minutesFromMidnight: number },
|
|
407
410
|
companyTimezone: string,
|
|
408
411
|
optionsMap: Map<string, { mostPopular?: boolean }>,
|
|
412
|
+
isAdmin: boolean
|
|
409
413
|
): Availability | null {
|
|
410
|
-
const selectable = timesForSelectedDate.filter((a) => (a.vacancies ?? 0) > 0);
|
|
414
|
+
const selectable = timesForSelectedDate.filter((a) => (a.vacancies ?? 0) > 0 || isAdmin);
|
|
411
415
|
if (selectable.length === 0) return null;
|
|
412
416
|
|
|
413
417
|
const withOption =
|
|
@@ -441,8 +445,9 @@ function pickReturnMatchingPreviousSelection(
|
|
|
441
445
|
sortedReturnOptions: ReturnOption[],
|
|
442
446
|
anchor: { returnLocation: string; minutesFromMidnight: number },
|
|
443
447
|
companyTimezone: string,
|
|
448
|
+
isAdmin: boolean
|
|
444
449
|
): ReturnOption | null {
|
|
445
|
-
const eligible = sortedReturnOptions.filter((o) => (o.vacancies ?? 0) > 0);
|
|
450
|
+
const eligible = sortedReturnOptions.filter((o) => (o.vacancies ?? 0) > 0 || isAdmin);
|
|
446
451
|
if (eligible.length === 0) return null;
|
|
447
452
|
|
|
448
453
|
const sameLoc = eligible.filter((o) => o.returnLocation === anchor.returnLocation);
|
|
@@ -631,8 +636,8 @@ function resolveInitialAvailabilityFromBooking(
|
|
|
631
636
|
}
|
|
632
637
|
|
|
633
638
|
/**
|
|
634
|
-
*
|
|
635
|
-
*
|
|
639
|
+
* Admin / provider-dashboard **change booking** — literal duplicate of {@link ChangeBookingFlow} for now
|
|
640
|
+
* so ticketbooth can diverge without affecting the public site flow.
|
|
636
641
|
*/
|
|
637
642
|
export function AdminChangeBookingFlow({
|
|
638
643
|
product,
|
|
@@ -656,6 +661,7 @@ export function AdminChangeBookingFlow({
|
|
|
656
661
|
partnerPortalBooking = false,
|
|
657
662
|
availabilityPricingProfileId,
|
|
658
663
|
availabilityCancellationPolicyProfileId,
|
|
664
|
+
onChangeBooking,
|
|
659
665
|
}: ChangeBookingFlowProps) {
|
|
660
666
|
/** Always the booking’s sold currency — not the site currency switcher / parent default. */
|
|
661
667
|
const currency = useMemo((): Currency => {
|
|
@@ -667,6 +673,16 @@ export function AdminChangeBookingFlow({
|
|
|
667
673
|
return DEFAULT_CURRENCY;
|
|
668
674
|
}, [originalReceipt?.currency, initialValues?.currency, currencyFromParent]);
|
|
669
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
|
+
};
|
|
670
686
|
const { env, analytics } = useBookingHost();
|
|
671
687
|
const { t } = useTranslations();
|
|
672
688
|
const { locale } = useLocale();
|
|
@@ -675,12 +691,15 @@ export function AdminChangeBookingFlow({
|
|
|
675
691
|
const cancellationPolicyProfileIdForAvailabilities =
|
|
676
692
|
(availabilityCancellationPolicyProfileId ?? '').trim() || null;
|
|
677
693
|
const {
|
|
694
|
+
permissions,
|
|
678
695
|
isSimplifiedPricingView,
|
|
679
696
|
onShowManage,
|
|
680
697
|
getSuccessUrl,
|
|
681
698
|
suppressCalendarDateScroll,
|
|
699
|
+
mode: bookingAppMode,
|
|
682
700
|
} = useBookingApp();
|
|
683
701
|
const availabilitiesCache = useAvailabilitiesCache();
|
|
702
|
+
const isAdmin = permissions.viewerRole === 'admin';
|
|
684
703
|
const [availabilities, setAvailabilities] = useState<Availability[]>([]);
|
|
685
704
|
const [selectedAvailability, setSelectedAvailability] = useState<Availability | null>(null);
|
|
686
705
|
const [selectedReturnOption, setSelectedReturnOption] = useState<ReturnOption | null>(null);
|
|
@@ -779,6 +798,12 @@ export function AdminChangeBookingFlow({
|
|
|
779
798
|
differenceTotal: number;
|
|
780
799
|
};
|
|
781
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);
|
|
782
807
|
const hasAppliedInitialValuesRef = useRef(false);
|
|
783
808
|
const hasAppliedInitialQuantitiesRef = useRef(false);
|
|
784
809
|
const hasHydratedAddOnsFromReceiptRef = useRef(false);
|
|
@@ -797,8 +822,10 @@ export function AdminChangeBookingFlow({
|
|
|
797
822
|
return null;
|
|
798
823
|
}
|
|
799
824
|
}, [initialValues?.dateTime, companyTimezone]);
|
|
825
|
+
const isProviderDashboardChange = Boolean(onChangeBooking);
|
|
826
|
+
const isCustomerSelfServeChange = !isProviderDashboardChange;
|
|
800
827
|
/** Do not render catalog-/FE-derived dollar amounts in UI until `quoteChangeBooking` returns `serverDisplay`. */
|
|
801
|
-
const suppressSelfServeCurrencyUi =
|
|
828
|
+
const suppressSelfServeCurrencyUi = isCustomerSelfServeChange;
|
|
802
829
|
|
|
803
830
|
useEffect(() => {
|
|
804
831
|
setPartnerAttributionConfirmed(false);
|
|
@@ -808,13 +835,13 @@ export function AdminChangeBookingFlow({
|
|
|
808
835
|
* user picks a different return time — baseline is the first auto-selected return for this outbound.
|
|
809
836
|
*/
|
|
810
837
|
const [implicitReturnBaselineId, setImplicitReturnBaselineId] = useState<string | null>(null);
|
|
811
|
-
/**
|
|
838
|
+
/** Any change flow (self-serve or provider): promo from booking is fixed — show read-only, never add new. */
|
|
812
839
|
const lockedPromoCode = initialValues?.promoCode?.trim()
|
|
813
840
|
? initialValues.promoCode.trim().toUpperCase()
|
|
814
841
|
: null;
|
|
815
842
|
/** Public self-serve only: cannot reduce tickets below original counts. */
|
|
816
843
|
const changeBookingMinimumQuantities = useMemo(() => {
|
|
817
|
-
if (!initialValues?.bookingItems?.length) return undefined;
|
|
844
|
+
if (!isCustomerSelfServeChange || !initialValues?.bookingItems?.length) return undefined;
|
|
818
845
|
const m: Record<string, number> = {};
|
|
819
846
|
for (const item of initialValues.bookingItems) {
|
|
820
847
|
const key = item.category?.trim();
|
|
@@ -822,7 +849,30 @@ export function AdminChangeBookingFlow({
|
|
|
822
849
|
m[key] = Math.max(0, Number(item.count) || 0);
|
|
823
850
|
}
|
|
824
851
|
return m;
|
|
825
|
-
}, [initialValues?.bookingItems]);
|
|
852
|
+
}, [isCustomerSelfServeChange, initialValues?.bookingItems]);
|
|
853
|
+
const [adminChoiceData, setAdminChoiceData] = useState<{
|
|
854
|
+
reservationReference: string;
|
|
855
|
+
reservationExpiration?: string;
|
|
856
|
+
checkoutBreakdown: { lineItems: Array<{ label: string; amount: number; type?: string; quantity?: number }>; totalAmount: number; currency: string };
|
|
857
|
+
totalAmount: number;
|
|
858
|
+
datePart: string;
|
|
859
|
+
timePart: string;
|
|
860
|
+
availabilityProductOptionId: string;
|
|
861
|
+
itineraryDisplay?: ItineraryDisplayStep[] | null;
|
|
862
|
+
clientSecret: string;
|
|
863
|
+
ticketLinesForModal: CheckoutModalLineItem[];
|
|
864
|
+
feeLineItems: OrderSummary['feeLineItems'];
|
|
865
|
+
returnPriceAdjustment: number;
|
|
866
|
+
cancellationPolicyFee: number;
|
|
867
|
+
cancellationPolicyLabel?: string;
|
|
868
|
+
subtotal: number;
|
|
869
|
+
tax: number;
|
|
870
|
+
totalQuantity: number;
|
|
871
|
+
isTaxIncludedInPrice: boolean;
|
|
872
|
+
taxRate: number;
|
|
873
|
+
promoDiscountAmount: number;
|
|
874
|
+
discountLabel?: string | null;
|
|
875
|
+
} | null>(null);
|
|
826
876
|
const [latestChangeQuote, setLatestChangeQuote] = useState<{
|
|
827
877
|
priceDiff: number;
|
|
828
878
|
currency: Currency;
|
|
@@ -1659,12 +1709,14 @@ export function AdminChangeBookingFlow({
|
|
|
1659
1709
|
}, [initialValues?.productId, product.productId]);
|
|
1660
1710
|
|
|
1661
1711
|
/**
|
|
1662
|
-
* Receipt pricing on protected seats/fees
|
|
1663
|
-
* + same product option) vs Rule B (`max(receipt, live)` when date or option changes).
|
|
1712
|
+
* Receipt pricing on protected seats/fees — **customer self-serve only**: Rule A (exact receipt unit when same calendar day
|
|
1713
|
+
* + same product option) vs Rule B (`max(receipt, live)` when date or option changes). Provider dashboard uses live catalog.
|
|
1664
1714
|
*/
|
|
1665
1715
|
const changeFlowApplyReceiptPaidFloors = useMemo(
|
|
1666
|
-
() =>
|
|
1667
|
-
|
|
1716
|
+
() =>
|
|
1717
|
+
!isProviderDashboardChange &&
|
|
1718
|
+
changeFlowBookingParentProductIdForFloors === product.productId.trim(),
|
|
1719
|
+
[isProviderDashboardChange, changeFlowBookingParentProductIdForFloors, product.productId],
|
|
1668
1720
|
);
|
|
1669
1721
|
|
|
1670
1722
|
useEffect(() => {
|
|
@@ -1790,27 +1842,27 @@ export function AdminChangeBookingFlow({
|
|
|
1790
1842
|
|
|
1791
1843
|
const initialAddOnMinQtyByKey = useMemo(() => {
|
|
1792
1844
|
const map = new Map<string, number>();
|
|
1793
|
-
if (
|
|
1845
|
+
if (!isCustomerSelfServeChange) return map;
|
|
1794
1846
|
for (const sel of initialAddOnBaselineSelections) {
|
|
1795
1847
|
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
1796
1848
|
map.set(key, (map.get(key) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
1797
1849
|
}
|
|
1798
1850
|
return map;
|
|
1799
|
-
}, [
|
|
1851
|
+
}, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
|
|
1800
1852
|
|
|
1801
1853
|
const initialAddOnMinTotalByAddOnId = useMemo(() => {
|
|
1802
1854
|
const map = new Map<string, number>();
|
|
1803
|
-
if (
|
|
1855
|
+
if (!isCustomerSelfServeChange) return map;
|
|
1804
1856
|
for (const sel of initialAddOnBaselineSelections) {
|
|
1805
1857
|
const addOnId = sel.addOnId.trim();
|
|
1806
1858
|
map.set(addOnId, (map.get(addOnId) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
1807
1859
|
}
|
|
1808
1860
|
return map;
|
|
1809
|
-
}, [
|
|
1861
|
+
}, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
|
|
1810
1862
|
|
|
1811
1863
|
const applyChangeFlowAddOnFloor = useCallback(
|
|
1812
1864
|
(nextSelections: Array<{ addOnId: string; variantId?: string; quantity?: number }>) => {
|
|
1813
|
-
if (
|
|
1865
|
+
if (!isCustomerSelfServeChange || initialAddOnMinQtyByKey.size === 0) return nextSelections;
|
|
1814
1866
|
const qtyByKey = new Map<string, number>();
|
|
1815
1867
|
for (const sel of nextSelections) {
|
|
1816
1868
|
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
@@ -1837,7 +1889,7 @@ export function AdminChangeBookingFlow({
|
|
|
1837
1889
|
return { addOnId, variantId, quantity: qty };
|
|
1838
1890
|
});
|
|
1839
1891
|
},
|
|
1840
|
-
[
|
|
1892
|
+
[isCustomerSelfServeChange, initialAddOnMinQtyByKey, initialAddOnMinTotalByAddOnId]
|
|
1841
1893
|
);
|
|
1842
1894
|
|
|
1843
1895
|
const updateAddOnSelections = useCallback(
|
|
@@ -2451,14 +2503,14 @@ export function AdminChangeBookingFlow({
|
|
|
2451
2503
|
const options = selectedAvailability?.returnOptions ?? [];
|
|
2452
2504
|
const serverReturnMap = latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId;
|
|
2453
2505
|
const floor = effectiveChangeFlowReturnUnitFloorPerPerson;
|
|
2454
|
-
const applyReturnFloor = floor != null &&
|
|
2506
|
+
const applyReturnFloor = floor != null && isCustomerSelfServeChange;
|
|
2455
2507
|
return options.map((opt) => {
|
|
2456
2508
|
const vacancyCredit = changeFlowSeatCreditForReturnAvailabilityId(opt.returnAvailabilityId);
|
|
2457
2509
|
const adjustedVacancies = Math.max(0, opt.vacancies ?? 0) + vacancyCredit;
|
|
2458
2510
|
const rawPerPerson = opt.priceAdjustmentByCurrency?.[currency] ?? 0;
|
|
2459
2511
|
const flooredPerPerson = applyReturnFloor ? Math.max(rawPerPerson, floor) : rawPerPerson;
|
|
2460
2512
|
let perPerson = flooredPerPerson;
|
|
2461
|
-
if (
|
|
2513
|
+
if (isCustomerSelfServeChange && serverReturnMap && opt.returnAvailabilityId) {
|
|
2462
2514
|
const sid = opt.returnAvailabilityId.trim();
|
|
2463
2515
|
const sp = serverReturnMap[sid];
|
|
2464
2516
|
if (sp != null && Number.isFinite(sp)) {
|
|
@@ -2481,7 +2533,7 @@ export function AdminChangeBookingFlow({
|
|
|
2481
2533
|
}, [
|
|
2482
2534
|
selectedAvailability?.returnOptions,
|
|
2483
2535
|
effectiveChangeFlowReturnUnitFloorPerPerson,
|
|
2484
|
-
|
|
2536
|
+
isCustomerSelfServeChange,
|
|
2485
2537
|
currency,
|
|
2486
2538
|
changeFlowSeatCreditForReturnAvailabilityId,
|
|
2487
2539
|
latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId,
|
|
@@ -2490,11 +2542,11 @@ export function AdminChangeBookingFlow({
|
|
|
2490
2542
|
const selectedReturnOptionWithFloor = useMemo(() => {
|
|
2491
2543
|
if (!selectedReturnOption) return selectedReturnOption;
|
|
2492
2544
|
const floor = effectiveChangeFlowReturnUnitFloorPerPerson;
|
|
2493
|
-
const applyReturnFloor = floor != null &&
|
|
2545
|
+
const applyReturnFloor = floor != null && isCustomerSelfServeChange;
|
|
2494
2546
|
const rawPerPerson = selectedReturnOption.priceAdjustmentByCurrency?.[currency] ?? 0;
|
|
2495
2547
|
let perPerson = rawPerPerson;
|
|
2496
2548
|
const serverReturnMap = latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId;
|
|
2497
|
-
if (
|
|
2549
|
+
if (isCustomerSelfServeChange && serverReturnMap && selectedReturnOption.returnAvailabilityId) {
|
|
2498
2550
|
const sid = selectedReturnOption.returnAvailabilityId.trim();
|
|
2499
2551
|
const sp = serverReturnMap[sid];
|
|
2500
2552
|
if (sp != null && Number.isFinite(sp)) {
|
|
@@ -2515,7 +2567,7 @@ export function AdminChangeBookingFlow({
|
|
|
2515
2567
|
}, [
|
|
2516
2568
|
selectedReturnOption,
|
|
2517
2569
|
effectiveChangeFlowReturnUnitFloorPerPerson,
|
|
2518
|
-
|
|
2570
|
+
isCustomerSelfServeChange,
|
|
2519
2571
|
currency,
|
|
2520
2572
|
latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId,
|
|
2521
2573
|
]);
|
|
@@ -2556,7 +2608,7 @@ export function AdminChangeBookingFlow({
|
|
|
2556
2608
|
/** Quote `ticketUnitPriceByCategory` overrides catalog units for ticket rows (customer self-serve only). */
|
|
2557
2609
|
const pricingForTicketSelector = useMemo(() => {
|
|
2558
2610
|
const m = latestChangeQuote?.serverPreview?.ticketUnitPriceByCategory;
|
|
2559
|
-
if (
|
|
2611
|
+
if (!isCustomerSelfServeChange || !m) return pricing;
|
|
2560
2612
|
return pricing.map((r) => {
|
|
2561
2613
|
const u = m[r.category.toUpperCase()];
|
|
2562
2614
|
if (u == null || !Number.isFinite(u)) return r;
|
|
@@ -2567,7 +2619,7 @@ export function AdminChangeBookingFlow({
|
|
|
2567
2619
|
baseInDisplayCurrency: u,
|
|
2568
2620
|
};
|
|
2569
2621
|
});
|
|
2570
|
-
}, [pricing, latestChangeQuote?.serverPreview?.ticketUnitPriceByCategory,
|
|
2622
|
+
}, [pricing, latestChangeQuote?.serverPreview?.ticketUnitPriceByCategory, isCustomerSelfServeChange]);
|
|
2571
2623
|
|
|
2572
2624
|
// Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
|
|
2573
2625
|
const getPriceBreakdown = useCallback((category: string, priceCAD: number, baseInDisplayCurrency: number | undefined, appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }> = []): PriceBreakdownData | null => {
|
|
@@ -2672,7 +2724,7 @@ export function AdminChangeBookingFlow({
|
|
|
2672
2724
|
|
|
2673
2725
|
// When return selection (or refreshed availabilities) caps the party below current ticket counts, trim tickets (non-admin).
|
|
2674
2726
|
useEffect(() => {
|
|
2675
|
-
if (
|
|
2727
|
+
if (isAdmin || !selectedAvailability) return;
|
|
2676
2728
|
const cap = effectivePartySizeCap;
|
|
2677
2729
|
if (totalQuantity <= cap) return;
|
|
2678
2730
|
const over = totalQuantity - cap;
|
|
@@ -2700,7 +2752,7 @@ export function AdminChangeBookingFlow({
|
|
|
2700
2752
|
effectivePartySizeCap,
|
|
2701
2753
|
totalQuantity,
|
|
2702
2754
|
selectedAvailability,
|
|
2703
|
-
|
|
2755
|
+
isAdmin,
|
|
2704
2756
|
changeBookingMinimumQuantities,
|
|
2705
2757
|
]);
|
|
2706
2758
|
|
|
@@ -2774,7 +2826,7 @@ export function AdminChangeBookingFlow({
|
|
|
2774
2826
|
*/
|
|
2775
2827
|
const changeFlowProtectedReturnAdjustment = useMemo(() => {
|
|
2776
2828
|
if (totalQuantity <= 0) return returnPriceAdjustment;
|
|
2777
|
-
if (
|
|
2829
|
+
if (isProviderDashboardChange) return returnPriceAdjustment;
|
|
2778
2830
|
if (effectiveChangeFlowReturnUnitFloorPerPerson == null) return returnPriceAdjustment;
|
|
2779
2831
|
const livePerPerson =
|
|
2780
2832
|
returnOptionCatalogPerPerson ?? (selectedReturnOption?.priceAdjustmentByCurrency?.[currency] ?? 0);
|
|
@@ -2788,7 +2840,7 @@ export function AdminChangeBookingFlow({
|
|
|
2788
2840
|
}, [
|
|
2789
2841
|
totalQuantity,
|
|
2790
2842
|
returnPriceAdjustment,
|
|
2791
|
-
|
|
2843
|
+
isProviderDashboardChange,
|
|
2792
2844
|
effectiveChangeFlowReturnUnitFloorPerPerson,
|
|
2793
2845
|
selectedReturnOption,
|
|
2794
2846
|
returnOptionCatalogPerPerson,
|
|
@@ -2799,7 +2851,7 @@ export function AdminChangeBookingFlow({
|
|
|
2799
2851
|
|
|
2800
2852
|
/** Return row amount for PriceSummary, Stripe breakdown, and CheckoutModal (catalog vs protected same-product-option). */
|
|
2801
2853
|
const checkoutReturnLineAmount = useMemo(() => {
|
|
2802
|
-
if (
|
|
2854
|
+
if (isCustomerSelfServeChange) {
|
|
2803
2855
|
return changeFlowProtectedReturnAdjustment;
|
|
2804
2856
|
}
|
|
2805
2857
|
if (changeFlowApplyReceiptPaidFloors) {
|
|
@@ -2807,7 +2859,7 @@ export function AdminChangeBookingFlow({
|
|
|
2807
2859
|
}
|
|
2808
2860
|
return returnPriceAdjustment;
|
|
2809
2861
|
}, [
|
|
2810
|
-
|
|
2862
|
+
isCustomerSelfServeChange,
|
|
2811
2863
|
changeFlowApplyReceiptPaidFloors,
|
|
2812
2864
|
changeFlowProtectedReturnAdjustment,
|
|
2813
2865
|
returnPriceAdjustment,
|
|
@@ -2879,13 +2931,35 @@ export function AdminChangeBookingFlow({
|
|
|
2879
2931
|
return [...feeLineItems, ...addOnLines];
|
|
2880
2932
|
}, [feeLineItems, addOnSelections, addOns]);
|
|
2881
2933
|
|
|
2934
|
+
const providerPricingUi = flowUi?.providerDashboardChangePricingUi;
|
|
2935
|
+
const providerQuotedLines = providerPricingUi?.quotedLines ?? [];
|
|
2936
|
+
const providerEditableLines = providerQuotedLines.filter((line) => isManualOverrideEligibleLine(line));
|
|
2937
|
+
const showProviderPricingInlineEditor =
|
|
2938
|
+
isProviderDashboardChange && isAdmin && (
|
|
2939
|
+
providerPricingUi?.loading ||
|
|
2940
|
+
providerPricingUi?.error != null ||
|
|
2941
|
+
providerQuotedLines.length > 0
|
|
2942
|
+
);
|
|
2943
|
+
const normalizeReceiptLabel = (value: string): string =>
|
|
2944
|
+
value
|
|
2945
|
+
.toLowerCase()
|
|
2946
|
+
.replace(/\([^)]*\)/g, '')
|
|
2947
|
+
.replace(/[^a-z0-9]+/g, '')
|
|
2948
|
+
.trim();
|
|
2949
|
+
const providerEditableLineByNormalizedLabel = useMemo(() => {
|
|
2950
|
+
const m = new Map<string, (typeof providerEditableLines)[number]>();
|
|
2951
|
+
for (const line of providerEditableLines) {
|
|
2952
|
+
const key = normalizeReceiptLabel(line.label ?? '');
|
|
2953
|
+
if (key) m.set(key, line);
|
|
2954
|
+
}
|
|
2955
|
+
return m;
|
|
2956
|
+
}, [providerEditableLines]);
|
|
2957
|
+
|
|
2882
2958
|
const checkoutPriceSummaryLines = useMemo((): PriceSummaryLine[] => {
|
|
2883
2959
|
if (!selectedAvailability) return [];
|
|
2884
2960
|
const returnLineAmount = checkoutReturnLineAmount;
|
|
2885
|
-
/** Show row when a return is selected and either the priced amount is non-zero or a receipt return floor exists (catalog slot can be $0 while floored total is not). */
|
|
2886
2961
|
const showReturnLine =
|
|
2887
|
-
Boolean(selectedReturnOption) &&
|
|
2888
|
-
(Math.abs(returnLineAmount) > 0.0005 || effectiveChangeFlowReturnUnitFloorPerPerson != null);
|
|
2962
|
+
Boolean(selectedReturnOption) && Math.abs(returnLineAmount) > 0.0005;
|
|
2889
2963
|
return [
|
|
2890
2964
|
...ticketLineItemsForChangeFlowDisplay.map((line): PriceSummaryLine => {
|
|
2891
2965
|
const rate = pricing.find((r) => r.category === line.category);
|
|
@@ -2897,8 +2971,13 @@ export function AdminChangeBookingFlow({
|
|
|
2897
2971
|
);
|
|
2898
2972
|
return {
|
|
2899
2973
|
kind: 'ticket',
|
|
2900
|
-
lineKey:
|
|
2901
|
-
|
|
2974
|
+
lineKey:
|
|
2975
|
+
showProviderPricingInlineEditor
|
|
2976
|
+
? providerEditableLineByNormalizedLabel.get(normalizeReceiptLabel(line.category))?.lineKey
|
|
2977
|
+
: undefined,
|
|
2978
|
+
editable:
|
|
2979
|
+
showProviderPricingInlineEditor &&
|
|
2980
|
+
providerEditableLineByNormalizedLabel.has(normalizeReceiptLabel(line.category)),
|
|
2902
2981
|
category: line.category,
|
|
2903
2982
|
qty: line.qty,
|
|
2904
2983
|
itemTotal: line.itemTotal,
|
|
@@ -2933,8 +3012,25 @@ export function AdminChangeBookingFlow({
|
|
|
2933
3012
|
fee.name.toLowerCase().includes('license'));
|
|
2934
3013
|
return {
|
|
2935
3014
|
kind: 'line' as const,
|
|
2936
|
-
lineKey:
|
|
2937
|
-
|
|
3015
|
+
lineKey:
|
|
3016
|
+
showProviderPricingInlineEditor
|
|
3017
|
+
? providerEditableLineByNormalizedLabel.get(
|
|
3018
|
+
normalizeReceiptLabel(
|
|
3019
|
+
feeLineItems.some((f) => f.name === fee.name)
|
|
3020
|
+
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
3021
|
+
: fee.name
|
|
3022
|
+
)
|
|
3023
|
+
)?.lineKey
|
|
3024
|
+
: undefined,
|
|
3025
|
+
editable:
|
|
3026
|
+
showProviderPricingInlineEditor &&
|
|
3027
|
+
providerEditableLineByNormalizedLabel.has(
|
|
3028
|
+
normalizeReceiptLabel(
|
|
3029
|
+
feeLineItems.some((f) => f.name === fee.name)
|
|
3030
|
+
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
3031
|
+
: fee.name
|
|
3032
|
+
)
|
|
3033
|
+
),
|
|
2938
3034
|
label: feeLineItems.some((f) => f.name === fee.name)
|
|
2939
3035
|
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
2940
3036
|
: fee.name,
|
|
@@ -2960,7 +3056,8 @@ export function AdminChangeBookingFlow({
|
|
|
2960
3056
|
effectiveCancellationPolicyLabel,
|
|
2961
3057
|
feeLineItemsWithAddOns,
|
|
2962
3058
|
feeLineItems,
|
|
2963
|
-
|
|
3059
|
+
showProviderPricingInlineEditor,
|
|
3060
|
+
providerEditableLineByNormalizedLabel,
|
|
2964
3061
|
]);
|
|
2965
3062
|
|
|
2966
3063
|
const checkoutPriceSummaryLinesForCheckout = useMemo(() => {
|
|
@@ -3131,7 +3228,45 @@ export function AdminChangeBookingFlow({
|
|
|
3131
3228
|
|
|
3132
3229
|
const clientMappedFromApi = mapQuoteLineItemsToPriceSummaryLines(api?.clientLineItems);
|
|
3133
3230
|
const useBeClientLines = clientMappedFromApi.length > 0;
|
|
3134
|
-
|
|
3231
|
+
/**
|
|
3232
|
+
* Checkout passes tax/discount via PriceSummary props, not always as `priceSummaryLines`. Include them here so
|
|
3233
|
+
* drift rows and their sums align with `changeFlowNewBookingTotal` (subtotal + tax − promo).
|
|
3234
|
+
*/
|
|
3235
|
+
const clientLinesForMerge: PriceSummaryLine[] = (() => {
|
|
3236
|
+
if (useBeClientLines) return clientMappedFromApi;
|
|
3237
|
+
const lines: PriceSummaryLine[] = [...checkoutPriceSummaryLines];
|
|
3238
|
+
const hasTaxLine = lines.some(
|
|
3239
|
+
(l) => l.kind === 'line' && String(l.type ?? '').toUpperCase() === 'TAX',
|
|
3240
|
+
);
|
|
3241
|
+
if (!hasTaxLine && !isTaxIncludedInPrice && Math.abs(effectiveTax) >= 0.005) {
|
|
3242
|
+
lines.push({
|
|
3243
|
+
kind: 'line',
|
|
3244
|
+
label: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
|
|
3245
|
+
amount: effectiveTax,
|
|
3246
|
+
type: 'TAX',
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
const hasPromoSummaryLine = lines.some(
|
|
3250
|
+
(l) =>
|
|
3251
|
+
l.kind === 'line' &&
|
|
3252
|
+
(/PROMO|DISCOUNT|VOUCHER|GIFT/i.test(String(l.type ?? '')) ||
|
|
3253
|
+
(l.amount < -0.005 && /promo|discount/i.test(l.label))),
|
|
3254
|
+
);
|
|
3255
|
+
if (!hasPromoSummaryLine && Math.abs(effectivePromoDiscountAmount) >= 0.005) {
|
|
3256
|
+
const trimmedPromoCode = appliedPromoCode?.trim() ?? '';
|
|
3257
|
+
const promoLabel =
|
|
3258
|
+
trimmedPromoCode.length > 0
|
|
3259
|
+
? `Promo: ${trimmedPromoCode}`
|
|
3260
|
+
: originalReceipt?.promoLabel?.trim() || 'Discount';
|
|
3261
|
+
lines.push({
|
|
3262
|
+
kind: 'line',
|
|
3263
|
+
label: promoLabel,
|
|
3264
|
+
amount: -effectivePromoDiscountAmount,
|
|
3265
|
+
type: 'PROMO_CODE',
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
return lines;
|
|
3269
|
+
})();
|
|
3135
3270
|
|
|
3136
3271
|
const serverMappedFromApi = mapQuoteLineItemsToPriceSummaryLines(api?.serverLineItems);
|
|
3137
3272
|
const useBeServerLines = serverMappedFromApi.length > 0;
|
|
@@ -3175,14 +3310,35 @@ export function AdminChangeBookingFlow({
|
|
|
3175
3310
|
}
|
|
3176
3311
|
|
|
3177
3312
|
const mergedForDrift = mergePriceSummaryLinesForDrift(clientLinesForMerge, serverLinesForMerge);
|
|
3178
|
-
|
|
3313
|
+
const rows =
|
|
3179
3314
|
api?.lineComparisons && api.lineComparisons.length > 0
|
|
3180
|
-
?
|
|
3315
|
+
? mergeLineComparisonsWithFullDrift(api.lineComparisons, mergedForDrift)
|
|
3181
3316
|
: mergedForDrift;
|
|
3182
3317
|
|
|
3318
|
+
const receiptAnchoring = api?.receiptAnchoring;
|
|
3319
|
+
let driftRows = rows;
|
|
3320
|
+
if (receiptAnchoring) {
|
|
3321
|
+
const adj = roundMoney(
|
|
3322
|
+
receiptAnchoring.reconciledTotalMajorUnits -
|
|
3323
|
+
receiptAnchoring.requestedCatalogLineSumMajorUnits,
|
|
3324
|
+
);
|
|
3325
|
+
if (Math.abs(adj) >= 0.005) {
|
|
3326
|
+
driftRows = [
|
|
3327
|
+
...rows,
|
|
3328
|
+
{
|
|
3329
|
+
key: 'meta:receipt-anchoring',
|
|
3330
|
+
label: 'Receipt anchoring adjustment (BE only — not included in app total)',
|
|
3331
|
+
clientAmount: null,
|
|
3332
|
+
serverAmount: adj,
|
|
3333
|
+
delta: computePricingDriftDelta(null, adj),
|
|
3334
|
+
},
|
|
3335
|
+
];
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3183
3339
|
const hasTotalDelta = totalDelta != null && Math.abs(totalDelta) >= 0.005;
|
|
3184
3340
|
|
|
3185
|
-
if (
|
|
3341
|
+
if (driftRows.length === 0 && !hasTotalDelta) {
|
|
3186
3342
|
return null;
|
|
3187
3343
|
}
|
|
3188
3344
|
|
|
@@ -3211,7 +3367,7 @@ export function AdminChangeBookingFlow({
|
|
|
3211
3367
|
|
|
3212
3368
|
return (
|
|
3213
3369
|
<ChangeBookingPricingDriftPanel
|
|
3214
|
-
rows={
|
|
3370
|
+
rows={driftRows}
|
|
3215
3371
|
clientTotal={clientTotalForDrift}
|
|
3216
3372
|
serverTotal={serverTotalFromQuote}
|
|
3217
3373
|
totalDelta={totalDelta}
|
|
@@ -3219,6 +3375,7 @@ export function AdminChangeBookingFlow({
|
|
|
3219
3375
|
locale={locale}
|
|
3220
3376
|
ticketCartDetail={ticketCartDetail.length > 0 ? ticketCartDetail : undefined}
|
|
3221
3377
|
serverTicketPricingTrace={latestChangeQuote.ticketPricingTrace ?? undefined}
|
|
3378
|
+
receiptAnchoring={receiptAnchoring}
|
|
3222
3379
|
footnote={
|
|
3223
3380
|
usesBeLinePayload
|
|
3224
3381
|
? 'Lines use pricingDriftDetail.clientLineItems / serverLineItems when the quote includes them; totals prefer explicit major-unit fields, then sums of those lines, then the live cart / receipt preview.'
|
|
@@ -3237,6 +3394,12 @@ export function AdminChangeBookingFlow({
|
|
|
3237
3394
|
locale,
|
|
3238
3395
|
ticketLineItemsForChangeFlowDisplay,
|
|
3239
3396
|
pricingForTicketSelector,
|
|
3397
|
+
isTaxIncludedInPrice,
|
|
3398
|
+
effectiveTax,
|
|
3399
|
+
effectivePromoDiscountAmount,
|
|
3400
|
+
appliedPromoCode,
|
|
3401
|
+
originalReceipt?.promoLabel,
|
|
3402
|
+
t,
|
|
3240
3403
|
]);
|
|
3241
3404
|
|
|
3242
3405
|
/** Replaces PriceSummary with non-numeric status until quote returns authoritative totals (no FE dollar amounts). */
|
|
@@ -3426,31 +3589,62 @@ export function AdminChangeBookingFlow({
|
|
|
3426
3589
|
addOnSelections,
|
|
3427
3590
|
initialAddOnMinQtyByKey,
|
|
3428
3591
|
]);
|
|
3429
|
-
const hasChangeSelection =
|
|
3592
|
+
const hasChangeSelection =
|
|
3593
|
+
isProviderDashboardChange
|
|
3594
|
+
? changeSelectionDetails.hasOperationalChangesFromInitial
|
|
3595
|
+
: changeSelectionDetails.hasChangesFromInitial;
|
|
3430
3596
|
|
|
3431
3597
|
const changeFlowNeedsServerPrice =
|
|
3432
|
-
|
|
3598
|
+
isCustomerSelfServeChange &&
|
|
3433
3599
|
hasChangeSelection &&
|
|
3434
3600
|
!!initialValues?.bookingReference?.trim() &&
|
|
3435
3601
|
!!lastName.trim();
|
|
3436
3602
|
|
|
3437
|
-
const isChangeQuoteBlocked =
|
|
3438
|
-
const requiresReturnInChangeFlow =
|
|
3603
|
+
const isChangeQuoteBlocked = isCustomerSelfServeChange && latestChangeQuote?.canProceed === false;
|
|
3604
|
+
const requiresReturnInChangeFlow = isCustomerSelfServeChange && !!initialValues?.returnAvailabilityId?.trim();
|
|
3439
3605
|
const missingRequiredReturnSelection = requiresReturnInChangeFlow && !selectedReturnOption;
|
|
3440
3606
|
|
|
3441
3607
|
const changeFlowSubmitDisabled =
|
|
3442
3608
|
missingRequiredReturnSelection ||
|
|
3443
|
-
(
|
|
3609
|
+
(isCustomerSelfServeChange &&
|
|
3444
3610
|
changeFlowNeedsServerPrice &&
|
|
3445
3611
|
(changeQuoteLoading || (!latestChangeQuote && !changeQuoteFetchError)));
|
|
3446
3612
|
|
|
3447
|
-
const providerTotalsPreview =
|
|
3448
|
-
|
|
3613
|
+
const providerTotalsPreview =
|
|
3614
|
+
showProviderPricingInlineEditor && providerPricingUi?.totalsPreview
|
|
3615
|
+
? providerPricingUi.totalsPreview
|
|
3616
|
+
: null;
|
|
3617
|
+
const providerHasEditedLineOverrides =
|
|
3618
|
+
isProviderDashboardChange &&
|
|
3619
|
+
providerQuotedLines.some((line) => {
|
|
3620
|
+
if (!isManualOverrideEligibleLine(line)) return false;
|
|
3621
|
+
const raw = providerPricingUi?.lineAmountInputs?.[line.lineKey];
|
|
3622
|
+
const parsed = raw == null || raw.trim() === '' ? line.amount : Number(raw);
|
|
3623
|
+
if (!Number.isFinite(parsed)) return false;
|
|
3624
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
3625
|
+
return Math.abs(rounded - line.amount) > 0.0001;
|
|
3626
|
+
});
|
|
3627
|
+
const providerHasAdditionalAdjustments =
|
|
3628
|
+
isProviderDashboardChange &&
|
|
3629
|
+
(providerPricingUi?.additionalAdjustments ?? []).some((adj) => {
|
|
3630
|
+
const parsed = Number((adj.amountInput ?? '').trim());
|
|
3631
|
+
const hasAmount = Number.isFinite(parsed) && parsed > 0;
|
|
3632
|
+
const currentAmount = hasAmount ? (Math.round(parsed * 100) / 100).toFixed(2) : '';
|
|
3633
|
+
const originalAmount = adj.originalAmountInput ?? '';
|
|
3634
|
+
const currentLabel = (adj.label ?? '').trim();
|
|
3635
|
+
const originalLabel = (adj.originalLabel ?? '').trim();
|
|
3636
|
+
const currentMode = adj.mode;
|
|
3637
|
+
const originalMode = adj.originalMode;
|
|
3638
|
+
if (!originalMode) return hasAmount || currentLabel.length > 0;
|
|
3639
|
+
return currentAmount !== originalAmount || currentLabel !== originalLabel || currentMode !== originalMode;
|
|
3640
|
+
});
|
|
3641
|
+
const hasEffectiveChangeSelection =
|
|
3642
|
+
hasChangeSelection || providerHasEditedLineOverrides || providerHasAdditionalAdjustments;
|
|
3449
3643
|
|
|
3450
3644
|
const displayedChangeAmounts = resolveChangeFlowDisplayedAmounts({
|
|
3451
3645
|
providerPreview: providerTotalsPreview,
|
|
3452
3646
|
serverQuotePreview:
|
|
3453
|
-
|
|
3647
|
+
isCustomerSelfServeChange && latestChangeQuote?.serverDisplay
|
|
3454
3648
|
? latestChangeQuote.serverDisplay
|
|
3455
3649
|
: null,
|
|
3456
3650
|
fromCart: {
|
|
@@ -3466,13 +3660,13 @@ export function AdminChangeBookingFlow({
|
|
|
3466
3660
|
const changeFlowClientEstimateDue = (() => {
|
|
3467
3661
|
if (!originalReceipt) return totalPrice;
|
|
3468
3662
|
// Customer self-serve: amount due comes from POST .../change/quote (`amountDueCents` / priceDiff), not FE delta math.
|
|
3469
|
-
if (
|
|
3663
|
+
if (isCustomerSelfServeChange && latestChangeQuote != null && !changeQuoteFetchError) {
|
|
3470
3664
|
return normalizeNearZeroOwed(latestChangeQuote.priceDiff);
|
|
3471
3665
|
}
|
|
3472
3666
|
return changeFlowBalanceVsOriginal({
|
|
3473
3667
|
newTotal: displayChangeFlowProposedTotal,
|
|
3474
3668
|
originalReceiptTotal: originalReceipt.total,
|
|
3475
|
-
audience: 'customer',
|
|
3669
|
+
audience: isProviderDashboardChange ? 'provider' : 'customer',
|
|
3476
3670
|
});
|
|
3477
3671
|
})();
|
|
3478
3672
|
|
|
@@ -3481,6 +3675,14 @@ export function AdminChangeBookingFlow({
|
|
|
3481
3675
|
|
|
3482
3676
|
const changeCheckoutButtonLabel = (() => {
|
|
3483
3677
|
if (!hasEffectiveChangeSelection) return undefined;
|
|
3678
|
+
if (isProviderDashboardChange) {
|
|
3679
|
+
const est = Math.round(changeFlowClientEstimateDue * 100) / 100;
|
|
3680
|
+
return est > 0
|
|
3681
|
+
? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
|
|
3682
|
+
: est < 0
|
|
3683
|
+
? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
|
|
3684
|
+
: 'Change booking (no charge)';
|
|
3685
|
+
}
|
|
3484
3686
|
if (changeFlowNeedsServerPrice) {
|
|
3485
3687
|
if (changeQuoteLoading) {
|
|
3486
3688
|
const tr = t('booking.updatingPrice');
|
|
@@ -3514,8 +3716,39 @@ export function AdminChangeBookingFlow({
|
|
|
3514
3716
|
const checkoutFormError =
|
|
3515
3717
|
(error || '') ||
|
|
3516
3718
|
(missingRequiredReturnSelection ? 'Removing return option in self-serve is not available. Please contact support.' : '') ||
|
|
3517
|
-
(
|
|
3518
|
-
(
|
|
3719
|
+
(isCustomerSelfServeChange && isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
|
|
3720
|
+
(isCustomerSelfServeChange ? changeQuoteFetchError ?? '' : '');
|
|
3721
|
+
|
|
3722
|
+
const providerPricingOverrides =
|
|
3723
|
+
isProviderDashboardChange && providerQuotedLines.length > 0
|
|
3724
|
+
? providerQuotedLines
|
|
3725
|
+
.filter((line) => isManualOverrideEligibleLine(line))
|
|
3726
|
+
.map((line) => {
|
|
3727
|
+
const raw = providerPricingUi?.lineAmountInputs?.[line.lineKey];
|
|
3728
|
+
const parsed = raw == null || raw.trim() === '' ? line.amount : Number(raw);
|
|
3729
|
+
if (!Number.isFinite(parsed)) return null;
|
|
3730
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
3731
|
+
return Math.abs(rounded - line.amount) > 0.0001
|
|
3732
|
+
? { lineKey: line.lineKey, amount: rounded, reason: 'Provider dashboard override' }
|
|
3733
|
+
: null;
|
|
3734
|
+
})
|
|
3735
|
+
.filter((v): v is { lineKey: string; amount: number; reason: string } => v != null)
|
|
3736
|
+
: [];
|
|
3737
|
+
const providerAdditionalAdjustments =
|
|
3738
|
+
isProviderDashboardChange
|
|
3739
|
+
? (providerPricingUi?.additionalAdjustments ?? [])
|
|
3740
|
+
.map((adj) => {
|
|
3741
|
+
const parsed = Number((adj.amountInput ?? '').trim());
|
|
3742
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
3743
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
3744
|
+
const signed = adj.mode === 'DISCOUNT' ? -rounded : rounded;
|
|
3745
|
+
return {
|
|
3746
|
+
label: adj.label?.trim() || (signed < 0 ? 'Provider discount' : 'Provider charge'),
|
|
3747
|
+
amount: signed,
|
|
3748
|
+
};
|
|
3749
|
+
})
|
|
3750
|
+
.filter((v): v is { label: string; amount: number } => v != null)
|
|
3751
|
+
: [];
|
|
3519
3752
|
|
|
3520
3753
|
const changeFlowSelectionPreview = useMemo((): ChangeFlowSelectionPreview | null => {
|
|
3521
3754
|
if (!selectedAvailability || totalQuantity <= 0) return null;
|
|
@@ -3621,7 +3854,7 @@ export function AdminChangeBookingFlow({
|
|
|
3621
3854
|
|
|
3622
3855
|
/** Debounced server quote so CTA + “amount owed” match PaymentIntent; avoids free confirm when FE estimate ≠ BE. */
|
|
3623
3856
|
useEffect(() => {
|
|
3624
|
-
if (
|
|
3857
|
+
if (!isCustomerSelfServeChange) {
|
|
3625
3858
|
setChangeQuoteLoading(false);
|
|
3626
3859
|
setChangeQuoteFetchError(null);
|
|
3627
3860
|
setLatestChangeQuote(null);
|
|
@@ -3712,7 +3945,7 @@ export function AdminChangeBookingFlow({
|
|
|
3712
3945
|
window.clearTimeout(timer);
|
|
3713
3946
|
};
|
|
3714
3947
|
}, [
|
|
3715
|
-
|
|
3948
|
+
isCustomerSelfServeChange,
|
|
3716
3949
|
hasChangeSelection,
|
|
3717
3950
|
selectedAvailability,
|
|
3718
3951
|
selectedAvailability?.dateTime,
|
|
@@ -3763,7 +3996,8 @@ export function AdminChangeBookingFlow({
|
|
|
3763
3996
|
timesForSelectedDate,
|
|
3764
3997
|
anchor,
|
|
3765
3998
|
companyTimezone,
|
|
3766
|
-
optionsMap
|
|
3999
|
+
optionsMap,
|
|
4000
|
+
isAdmin
|
|
3767
4001
|
);
|
|
3768
4002
|
changeFlowOutboundAnchorRef.current = null;
|
|
3769
4003
|
if (matched) {
|
|
@@ -3854,7 +4088,8 @@ export function AdminChangeBookingFlow({
|
|
|
3854
4088
|
const fromAnchor = pickReturnMatchingPreviousSelection(
|
|
3855
4089
|
sorted,
|
|
3856
4090
|
returnAnchor,
|
|
3857
|
-
companyTimezone
|
|
4091
|
+
companyTimezone,
|
|
4092
|
+
isAdmin
|
|
3858
4093
|
);
|
|
3859
4094
|
changeFlowReturnAnchorRef.current = null;
|
|
3860
4095
|
if (fromAnchor) {
|
|
@@ -3900,7 +4135,7 @@ export function AdminChangeBookingFlow({
|
|
|
3900
4135
|
selectedAvailability,
|
|
3901
4136
|
selectedReturnOption,
|
|
3902
4137
|
companyTimezone,
|
|
3903
|
-
|
|
4138
|
+
isAdmin,
|
|
3904
4139
|
initialValues?.returnAvailabilityId,
|
|
3905
4140
|
initialValues?.returnDateTime,
|
|
3906
4141
|
]);
|
|
@@ -3963,9 +4198,9 @@ export function AdminChangeBookingFlow({
|
|
|
3963
4198
|
const firstWithInventory = dates.find((d) => {
|
|
3964
4199
|
const rows = availabilitiesByDate[d] ?? [];
|
|
3965
4200
|
if (rows.length === 0) return false;
|
|
3966
|
-
if (
|
|
4201
|
+
if (isAdmin) return rows.some((a) => (a.vacancies ?? 0) > 0);
|
|
3967
4202
|
if (
|
|
3968
|
-
|
|
4203
|
+
isCustomerSelfServeChange &&
|
|
3969
4204
|
changeFlowInitialTicketCount > 0
|
|
3970
4205
|
) {
|
|
3971
4206
|
return rows.some(
|
|
@@ -3974,7 +4209,7 @@ export function AdminChangeBookingFlow({
|
|
|
3974
4209
|
}
|
|
3975
4210
|
return rows.some((a) => (a.vacancies ?? 0) > 0);
|
|
3976
4211
|
});
|
|
3977
|
-
const first = firstWithInventory ?? (
|
|
4212
|
+
const first = firstWithInventory ?? (isAdmin && dates[0] ? dates[0] : undefined);
|
|
3978
4213
|
if (!first) return;
|
|
3979
4214
|
|
|
3980
4215
|
hasAutoSelectedPartnerDateRef.current = true;
|
|
@@ -3997,8 +4232,8 @@ export function AdminChangeBookingFlow({
|
|
|
3997
4232
|
selectedDate,
|
|
3998
4233
|
dates,
|
|
3999
4234
|
availabilitiesByDate,
|
|
4000
|
-
|
|
4001
|
-
|
|
4235
|
+
isAdmin,
|
|
4236
|
+
isCustomerSelfServeChange,
|
|
4002
4237
|
changeFlowInitialTicketCount,
|
|
4003
4238
|
getCalendarEffectiveOutboundVacancies,
|
|
4004
4239
|
useWindowScroll,
|
|
@@ -4036,7 +4271,7 @@ export function AdminChangeBookingFlow({
|
|
|
4036
4271
|
};
|
|
4037
4272
|
|
|
4038
4273
|
const handleQuantityChange = (category: string, delta: number) => {
|
|
4039
|
-
const maxAvailable =
|
|
4274
|
+
const maxAvailable = isAdmin ? Number.MAX_SAFE_INTEGER : effectivePartySizeCap;
|
|
4040
4275
|
const currentQty = quantities[category] || 0;
|
|
4041
4276
|
const minQ =
|
|
4042
4277
|
changeBookingMinimumQuantities != null
|
|
@@ -4044,7 +4279,7 @@ export function AdminChangeBookingFlow({
|
|
|
4044
4279
|
: 0;
|
|
4045
4280
|
const newQty = Math.max(minQ, currentQty + delta);
|
|
4046
4281
|
// Admin can overbook; non-admin cannot exceed vacancies
|
|
4047
|
-
if (delta > 0 && !
|
|
4282
|
+
if (delta > 0 && !isAdmin && orderSummary.totalQuantity >= maxAvailable) {
|
|
4048
4283
|
return;
|
|
4049
4284
|
}
|
|
4050
4285
|
setQuantities(prev => ({
|
|
@@ -4177,27 +4412,30 @@ export function AdminChangeBookingFlow({
|
|
|
4177
4412
|
return;
|
|
4178
4413
|
}
|
|
4179
4414
|
|
|
4180
|
-
|
|
4181
|
-
if (!
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4415
|
+
const skipContactFields = isProviderDashboardChange;
|
|
4416
|
+
if (!skipContactFields) {
|
|
4417
|
+
// Validate email (required)
|
|
4418
|
+
if (!email) {
|
|
4419
|
+
setError(t('booking.enterEmail') || 'Please enter your email address');
|
|
4420
|
+
return;
|
|
4421
|
+
}
|
|
4185
4422
|
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4423
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
4424
|
+
setError(t('booking.invalidEmail') || 'Please enter a valid email address');
|
|
4425
|
+
return;
|
|
4426
|
+
}
|
|
4190
4427
|
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4428
|
+
// Validate first name (required)
|
|
4429
|
+
if (!firstName?.trim()) {
|
|
4430
|
+
setError(t('booking.enterFirstName') || 'Please enter your first name');
|
|
4431
|
+
return;
|
|
4432
|
+
}
|
|
4196
4433
|
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4434
|
+
// Validate last name (required for manage booking lookup)
|
|
4435
|
+
if (!lastName?.trim()) {
|
|
4436
|
+
setError(t('booking.enterLastName') || 'Please enter your last name');
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4201
4439
|
}
|
|
4202
4440
|
|
|
4203
4441
|
// Allow checkout if pickup location is selected OR if user chose "I don't know"
|
|
@@ -4225,6 +4463,44 @@ export function AdminChangeBookingFlow({
|
|
|
4225
4463
|
return;
|
|
4226
4464
|
}
|
|
4227
4465
|
|
|
4466
|
+
if (onChangeBooking) {
|
|
4467
|
+
const pickupForChange = pickupLocationId
|
|
4468
|
+
? product.pickupLocations?.find((loc) => loc.id === pickupLocationId)
|
|
4469
|
+
: null;
|
|
4470
|
+
await onChangeBooking({
|
|
4471
|
+
productId: availabilityProductOptionId,
|
|
4472
|
+
dateTime: selectedAvailability.dateTime,
|
|
4473
|
+
bookingItems,
|
|
4474
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
4475
|
+
pickupLocationId: pickupLocationId ?? null,
|
|
4476
|
+
travelerHotel: pickupForChange?.name ?? null,
|
|
4477
|
+
startTime: selectedAvailability.dateTime ?? null,
|
|
4478
|
+
passengerCount: null,
|
|
4479
|
+
childSafetySeatsCount: null,
|
|
4480
|
+
foodRestrictions: null,
|
|
4481
|
+
addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
|
|
4482
|
+
cancellationPolicyId: cancellationPolicyId ?? initialValues?.cancellationPolicyId ?? null,
|
|
4483
|
+
promoCode: appliedPromoCode ?? null,
|
|
4484
|
+
newTotalAmount: displayChangeFlowProposedTotal,
|
|
4485
|
+
additionalHoursCount: null,
|
|
4486
|
+
pricingAdjustment:
|
|
4487
|
+
providerPricingOverrides.length > 0 || providerAdditionalAdjustments.length > 0
|
|
4488
|
+
? {
|
|
4489
|
+
mode: 'MANUAL_LINES',
|
|
4490
|
+
lineOverrides: providerPricingOverrides,
|
|
4491
|
+
additionalAdjustments: providerAdditionalAdjustments,
|
|
4492
|
+
}
|
|
4493
|
+
: undefined,
|
|
4494
|
+
capacitySeatCredit: {
|
|
4495
|
+
enabled: true,
|
|
4496
|
+
previousPassengerCount: changeFlowInitialTicketCount,
|
|
4497
|
+
previousAvailabilityId: initialValues?.availabilityId ?? null,
|
|
4498
|
+
previousReturnAvailabilityId: initialValues?.returnAvailabilityId ?? null,
|
|
4499
|
+
},
|
|
4500
|
+
});
|
|
4501
|
+
setLoading(false);
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4228
4504
|
|
|
4229
4505
|
const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
|
|
4230
4506
|
clientChannelSource: inferClientBookingSourceFromProductIds(
|
|
@@ -4232,17 +4508,18 @@ export function AdminChangeBookingFlow({
|
|
|
4232
4508
|
availabilityProductOptionId,
|
|
4233
4509
|
),
|
|
4234
4510
|
forcePartnerPortalChannel: partnerPortalBooking,
|
|
4235
|
-
forceDashboardSource:
|
|
4511
|
+
forceDashboardSource: bookingAppMode === 'provider-dashboard',
|
|
4236
4512
|
});
|
|
4237
4513
|
|
|
4238
4514
|
// Get the hotel name if a pickup location was selected
|
|
4239
4515
|
const selectedPickupLocation = pickupLocationId
|
|
4240
4516
|
? product.pickupLocations?.find(loc => loc.id === pickupLocationId)
|
|
4241
4517
|
: null;
|
|
4518
|
+
let quotedPriceDiff: number | null = null;
|
|
4242
4519
|
let changeIntentIdForCheckout: string | undefined;
|
|
4243
4520
|
let changeBookingReferenceForPaidFlow: string | undefined;
|
|
4244
4521
|
|
|
4245
|
-
{
|
|
4522
|
+
if (isCustomerSelfServeChange) {
|
|
4246
4523
|
const changeBookingReference = initialValues?.bookingReference?.trim();
|
|
4247
4524
|
const changeLastName = lastName.trim();
|
|
4248
4525
|
if (!changeBookingReference || !changeLastName) {
|
|
@@ -4270,6 +4547,7 @@ export function AdminChangeBookingFlow({
|
|
|
4270
4547
|
},
|
|
4271
4548
|
currency
|
|
4272
4549
|
);
|
|
4550
|
+
quotedPriceDiff = quoteSlice.priceDiff;
|
|
4273
4551
|
changeBookingReferenceForPaidFlow = changeBookingReference;
|
|
4274
4552
|
changeIntentIdForCheckout = quoteSlice.changeIntentId ?? undefined;
|
|
4275
4553
|
setLatestChangeQuote(
|
|
@@ -4361,11 +4639,13 @@ export function AdminChangeBookingFlow({
|
|
|
4361
4639
|
// Build checkout breakdown from the exact same values we show in the UI and Stripe modal.
|
|
4362
4640
|
// Backend will charge totalAmount and store this as the receipt so /manage matches.
|
|
4363
4641
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
4364
|
-
const amountDueForCheckout =
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4642
|
+
const amountDueForCheckout = isCustomerSelfServeChange
|
|
4643
|
+
? changeFlowBalanceVsOriginal({
|
|
4644
|
+
newTotal: changeFlowNewBookingTotal,
|
|
4645
|
+
originalReceiptTotal: originalReceipt?.total ?? 0,
|
|
4646
|
+
audience: 'customer',
|
|
4647
|
+
})
|
|
4648
|
+
: totalPrice;
|
|
4369
4649
|
const lines = [
|
|
4370
4650
|
...ticketLineItemsForChangeFlowDisplay.map((line) => ({
|
|
4371
4651
|
label: line.category,
|
|
@@ -4442,7 +4722,8 @@ export function AdminChangeBookingFlow({
|
|
|
4442
4722
|
roundingLabel: t('booking.rounding') || 'Rounding',
|
|
4443
4723
|
});
|
|
4444
4724
|
|
|
4445
|
-
const paymentIntent =
|
|
4725
|
+
const paymentIntent = isCustomerSelfServeChange
|
|
4726
|
+
? await createChangeBookingPaymentIntent(
|
|
4446
4727
|
(() => {
|
|
4447
4728
|
const id = changeIntentIdForCheckout ?? latestChangeQuote?.changeIntentId;
|
|
4448
4729
|
if (!id) {
|
|
@@ -4450,8 +4731,76 @@ export function AdminChangeBookingFlow({
|
|
|
4450
4731
|
}
|
|
4451
4732
|
return id;
|
|
4452
4733
|
})()
|
|
4453
|
-
)
|
|
4734
|
+
)
|
|
4735
|
+
: await createPaymentIntent({
|
|
4736
|
+
productId: product.productId,
|
|
4737
|
+
optionId: availabilityProductOptionId,
|
|
4738
|
+
date: datePart,
|
|
4739
|
+
time: timePart,
|
|
4740
|
+
quantity: totalQuantity,
|
|
4741
|
+
customerEmail: email,
|
|
4742
|
+
customerFirstName: firstName.trim() || undefined,
|
|
4743
|
+
customerLastName: lastName.trim() || undefined,
|
|
4744
|
+
currency: currency,
|
|
4745
|
+
travelerHotel: selectedPickupLocation?.name || undefined,
|
|
4746
|
+
pickupLocationId: pickupLocationId || undefined,
|
|
4747
|
+
itineraryDisplay: itineraryDisplay ?? undefined,
|
|
4748
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId,
|
|
4749
|
+
promoCode: (lockedPromoCode || appliedPromoCode) || undefined,
|
|
4750
|
+
cancellationPolicyId: cancellationPolicyId || undefined,
|
|
4751
|
+
termsAcceptedAt: termsAcceptedAt ?? undefined,
|
|
4752
|
+
checkoutBreakdown,
|
|
4753
|
+
skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
|
|
4754
|
+
disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
|
|
4755
|
+
...bookingSourceContext,
|
|
4756
|
+
});
|
|
4454
4757
|
|
|
4758
|
+
// Admin: show choice to pay now or confirm without payment (customer owes full balance)
|
|
4759
|
+
if (isAdmin) {
|
|
4760
|
+
const adminReservationRef =
|
|
4761
|
+
initialValues?.bookingReference?.trim() ??
|
|
4762
|
+
changeBookingReferenceForPaidFlow ??
|
|
4763
|
+
'';
|
|
4764
|
+
if (!adminReservationRef) {
|
|
4765
|
+
throw new Error('Missing reservation reference for admin payment flow');
|
|
4766
|
+
}
|
|
4767
|
+
setError('');
|
|
4768
|
+
setAdminChoiceData({
|
|
4769
|
+
reservationReference: adminReservationRef,
|
|
4770
|
+
reservationExpiration: undefined,
|
|
4771
|
+
checkoutBreakdown,
|
|
4772
|
+
totalAmount: amountDueForCheckout,
|
|
4773
|
+
datePart,
|
|
4774
|
+
timePart,
|
|
4775
|
+
availabilityProductOptionId,
|
|
4776
|
+
itineraryDisplay: itineraryDisplay ?? undefined,
|
|
4777
|
+
clientSecret: paymentIntent.clientSecret ?? '',
|
|
4778
|
+
ticketLinesForModal: ticketLineItemsForChangeFlowDisplay.map((line) => {
|
|
4779
|
+
const rate = pricing.find((r) => r.category === line.category);
|
|
4780
|
+
const breakdown = getPriceBreakdown(
|
|
4781
|
+
line.category,
|
|
4782
|
+
rate?.priceCAD ?? 0,
|
|
4783
|
+
rate?.baseInDisplayCurrency,
|
|
4784
|
+
rate?.appliedAdjustments ?? []
|
|
4785
|
+
);
|
|
4786
|
+
return { line, breakdown };
|
|
4787
|
+
}),
|
|
4788
|
+
feeLineItems: feeLineItemsWithAddOns,
|
|
4789
|
+
returnPriceAdjustment: checkoutReturnLineAmount,
|
|
4790
|
+
cancellationPolicyFee,
|
|
4791
|
+
cancellationPolicyLabel: effectiveCancellationPolicyLabel,
|
|
4792
|
+
subtotal: effectiveSubtotal,
|
|
4793
|
+
tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
|
|
4794
|
+
totalQuantity,
|
|
4795
|
+
isTaxIncludedInPrice,
|
|
4796
|
+
taxRate: pricingConfig?.taxRate ?? 0,
|
|
4797
|
+
promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
|
|
4798
|
+
discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : (originalReceipt?.promoLabel || undefined),
|
|
4799
|
+
});
|
|
4800
|
+
setShowAdminPaymentChoice(true);
|
|
4801
|
+
setLoading(false);
|
|
4802
|
+
return;
|
|
4803
|
+
}
|
|
4455
4804
|
|
|
4456
4805
|
const ticketLinesForModal: CheckoutModalLineItem[] = ticketLineItemsForChangeFlowDisplay.map((line) => {
|
|
4457
4806
|
const rate = pricing.find((r) => r.category === line.category);
|
|
@@ -4473,7 +4822,7 @@ export function AdminChangeBookingFlow({
|
|
|
4473
4822
|
// Paid change: always return to stable ref+lastName + explicit intent (not reservationRef).
|
|
4474
4823
|
// /manage-booking runs bounded refresh only when `from=change_payment` (see manage-booking page).
|
|
4475
4824
|
successUrlOverride:
|
|
4476
|
-
|
|
4825
|
+
isCustomerSelfServeChange && changeBookingReferenceForPaidFlow
|
|
4477
4826
|
? (() => {
|
|
4478
4827
|
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
4479
4828
|
const ref = encodeURIComponent(
|
|
@@ -4501,7 +4850,7 @@ export function AdminChangeBookingFlow({
|
|
|
4501
4850
|
promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
|
|
4502
4851
|
discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : (originalReceipt?.promoLabel || undefined),
|
|
4503
4852
|
changeTotals:
|
|
4504
|
-
|
|
4853
|
+
isCustomerSelfServeChange && originalReceipt
|
|
4505
4854
|
? {
|
|
4506
4855
|
previousTotal: originalReceipt.total,
|
|
4507
4856
|
newTotal: displayChangeFlowProposedTotal,
|
|
@@ -4570,7 +4919,85 @@ export function AdminChangeBookingFlow({
|
|
|
4570
4919
|
}
|
|
4571
4920
|
};
|
|
4572
4921
|
|
|
4922
|
+
const handleConfirmWithoutPayment = async () => {
|
|
4923
|
+
if (!adminChoiceData) return;
|
|
4924
|
+
setLoading(true);
|
|
4925
|
+
setError('');
|
|
4926
|
+
try {
|
|
4927
|
+
const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
|
|
4928
|
+
clientChannelSource: inferClientBookingSourceFromProductIds(
|
|
4929
|
+
product.productId,
|
|
4930
|
+
adminChoiceData.availabilityProductOptionId,
|
|
4931
|
+
),
|
|
4932
|
+
forcePartnerPortalChannel: partnerPortalBooking,
|
|
4933
|
+
forceDashboardSource: bookingAppMode === 'provider-dashboard',
|
|
4934
|
+
});
|
|
4935
|
+
const result = await confirmBookingWithoutPayment({
|
|
4936
|
+
reservationReference: adminChoiceData.reservationReference,
|
|
4937
|
+
productId: product.productId,
|
|
4938
|
+
optionId: adminChoiceData.availabilityProductOptionId,
|
|
4939
|
+
date: adminChoiceData.datePart,
|
|
4940
|
+
time: adminChoiceData.timePart,
|
|
4941
|
+
customerEmail: email || undefined,
|
|
4942
|
+
customerFirstName: firstName.trim() || undefined,
|
|
4943
|
+
customerLastName: lastName.trim() || undefined,
|
|
4944
|
+
currency: currency,
|
|
4945
|
+
travelerHotel: product.pickupLocations?.find(loc => loc.id === pickupLocationId)?.name || undefined,
|
|
4946
|
+
pickupLocationId: pickupLocationId || undefined,
|
|
4947
|
+
itineraryDisplay: adminChoiceData.itineraryDisplay ?? undefined,
|
|
4948
|
+
termsAcceptedAt: termsAcceptedAt ?? undefined,
|
|
4949
|
+
skipConfirmationCommunications: skipConfirmationCommunications ? true : undefined,
|
|
4950
|
+
disableAutoCommunications: disableAutoCommunications ? true : undefined,
|
|
4951
|
+
checkoutBreakdown: adminChoiceData.checkoutBreakdown,
|
|
4952
|
+
depositAmount: 0,
|
|
4953
|
+
balanceAmount: adminChoiceData.totalAmount,
|
|
4954
|
+
totalAmount: adminChoiceData.totalAmount,
|
|
4955
|
+
...bookingSourceContext,
|
|
4956
|
+
});
|
|
4957
|
+
pendingReservationRef.current = null;
|
|
4958
|
+
const ref = formatBookingRefForDisplay(result.bookingReference);
|
|
4959
|
+
const ln = lastName.trim();
|
|
4960
|
+
setShowAdminPaymentChoice(false);
|
|
4961
|
+
setAdminChoiceData(null);
|
|
4962
|
+
onSuccess?.({ reservationReference: adminChoiceData.reservationReference });
|
|
4963
|
+
if (onShowManage) {
|
|
4964
|
+
onShowManage({ ref, lastName: ln });
|
|
4965
|
+
} else {
|
|
4966
|
+
const params = new URLSearchParams({ ref, lastName: ln, booking_complete: '1' });
|
|
4967
|
+
window.location.href = `/manage-booking?${params.toString()}`;
|
|
4968
|
+
}
|
|
4969
|
+
} catch (err) {
|
|
4970
|
+
setError(err instanceof Error ? err.message : 'Failed to confirm booking');
|
|
4971
|
+
} finally {
|
|
4972
|
+
setLoading(false);
|
|
4973
|
+
}
|
|
4974
|
+
};
|
|
4573
4975
|
|
|
4976
|
+
const handlePayNow = () => {
|
|
4977
|
+
if (!adminChoiceData) return;
|
|
4978
|
+
setShowAdminPaymentChoice(false);
|
|
4979
|
+
setCheckoutClientSecret(adminChoiceData.clientSecret);
|
|
4980
|
+
setCheckoutModalData({
|
|
4981
|
+
reservationReference: adminChoiceData.reservationReference,
|
|
4982
|
+
reservationExpiration: adminChoiceData.reservationExpiration,
|
|
4983
|
+
customerLastName: lastName.trim(),
|
|
4984
|
+
ticketLines: adminChoiceData.ticketLinesForModal,
|
|
4985
|
+
feeLineItems: adminChoiceData.feeLineItems,
|
|
4986
|
+
returnPriceAdjustment: adminChoiceData.returnPriceAdjustment,
|
|
4987
|
+
cancellationPolicyFee: adminChoiceData.cancellationPolicyFee,
|
|
4988
|
+
cancellationPolicyLabel: adminChoiceData.cancellationPolicyLabel,
|
|
4989
|
+
subtotal: adminChoiceData.subtotal,
|
|
4990
|
+
tax: adminChoiceData.tax,
|
|
4991
|
+
total: adminChoiceData.totalAmount,
|
|
4992
|
+
totalQuantity: adminChoiceData.totalQuantity,
|
|
4993
|
+
isTaxIncludedInPrice: adminChoiceData.isTaxIncludedInPrice,
|
|
4994
|
+
taxRate: adminChoiceData.taxRate,
|
|
4995
|
+
promoDiscountAmount: adminChoiceData.promoDiscountAmount,
|
|
4996
|
+
discountLabel: adminChoiceData.discountLabel,
|
|
4997
|
+
});
|
|
4998
|
+
setShowCheckoutModal(true);
|
|
4999
|
+
setAdminChoiceData(null);
|
|
5000
|
+
};
|
|
4574
5001
|
|
|
4575
5002
|
if (activeOptions.length === 0) {
|
|
4576
5003
|
return (
|
|
@@ -4582,6 +5009,17 @@ export function AdminChangeBookingFlow({
|
|
|
4582
5009
|
|
|
4583
5010
|
return (
|
|
4584
5011
|
<div className="booking-flow-root space-y-8">
|
|
5012
|
+
{/* Admin: choose to pay now or confirm without payment (full balance owed) */}
|
|
5013
|
+
<AdminPaymentChoiceModal
|
|
5014
|
+
open={!!(showAdminPaymentChoice && adminChoiceData)}
|
|
5015
|
+
totalAmount={adminChoiceData?.totalAmount ?? 0}
|
|
5016
|
+
currency={currency}
|
|
5017
|
+
loading={loading}
|
|
5018
|
+
error={error}
|
|
5019
|
+
onPayNow={handlePayNow}
|
|
5020
|
+
onConfirmWithoutPayment={handleConfirmWithoutPayment}
|
|
5021
|
+
onCancel={() => { setShowAdminPaymentChoice(false); setAdminChoiceData(null); setError(''); }}
|
|
5022
|
+
/>
|
|
4585
5023
|
{checkoutModalData && (
|
|
4586
5024
|
<CheckoutModal
|
|
4587
5025
|
open={showCheckoutModal}
|
|
@@ -4655,12 +5093,12 @@ export function AdminChangeBookingFlow({
|
|
|
4655
5093
|
syncVisibleWeekToSelectedDate={true}
|
|
4656
5094
|
selectableSoldOutDate={changeFlowOriginalDate}
|
|
4657
5095
|
partySizeRequiredForCalendarSelection={
|
|
4658
|
-
|
|
5096
|
+
isCustomerSelfServeChange && !isAdmin && changeFlowInitialTicketCount > 0
|
|
4659
5097
|
? changeFlowInitialTicketCount
|
|
4660
5098
|
: undefined
|
|
4661
5099
|
}
|
|
4662
5100
|
getEffectiveVacancies={
|
|
4663
|
-
|
|
5101
|
+
isCustomerSelfServeChange && !isAdmin && changeFlowInitialTicketCount > 0
|
|
4664
5102
|
? getCalendarEffectiveOutboundVacancies
|
|
4665
5103
|
: undefined
|
|
4666
5104
|
}
|
|
@@ -4684,7 +5122,7 @@ export function AdminChangeBookingFlow({
|
|
|
4684
5122
|
earliestDate={earliestAvailabilityDate}
|
|
4685
5123
|
onVisibleRangeChange={handleVisibleRangeChange}
|
|
4686
5124
|
currency={currency}
|
|
4687
|
-
showCapacity={
|
|
5125
|
+
showCapacity={isAdmin}
|
|
4688
5126
|
extraDiscountPercent={calendarDiscountPercent}
|
|
4689
5127
|
capDiscountBadgesToBookingDate={changeFlowOriginalDate}
|
|
4690
5128
|
/>
|
|
@@ -4740,7 +5178,7 @@ export function AdminChangeBookingFlow({
|
|
|
4740
5178
|
selectedTicketCount={totalQuantity}
|
|
4741
5179
|
optionsMap={optionsMap}
|
|
4742
5180
|
hasAnyMostPopular={hasAnyMostPopular}
|
|
4743
|
-
isAdmin={
|
|
5181
|
+
isAdmin={isAdmin}
|
|
4744
5182
|
pickupLocationSkipped={pickupLocationSkipped}
|
|
4745
5183
|
t={t}
|
|
4746
5184
|
onTimeSelect={handleTimeSelect}
|
|
@@ -4756,7 +5194,7 @@ export function AdminChangeBookingFlow({
|
|
|
4756
5194
|
companyTimezone={companyTimezone}
|
|
4757
5195
|
currency={currency}
|
|
4758
5196
|
locale={locale}
|
|
4759
|
-
isAdmin={
|
|
5197
|
+
isAdmin={isAdmin}
|
|
4760
5198
|
t={t}
|
|
4761
5199
|
onReturnSelect={(option) => {
|
|
4762
5200
|
const raw = selectedAvailability.returnOptions?.find(
|
|
@@ -4784,7 +5222,7 @@ export function AdminChangeBookingFlow({
|
|
|
4784
5222
|
resourceCount={selectedReturnOption ? null : (selectedAvailability.resourceCount ?? null)}
|
|
4785
5223
|
currency={currency}
|
|
4786
5224
|
locale={locale}
|
|
4787
|
-
isAdmin={
|
|
5225
|
+
isAdmin={isAdmin}
|
|
4788
5226
|
isSimplifiedPricingView={isSimplifiedPricingView}
|
|
4789
5227
|
t={t}
|
|
4790
5228
|
onQuantityChange={handleQuantityChange}
|
|
@@ -4806,7 +5244,7 @@ export function AdminChangeBookingFlow({
|
|
|
4806
5244
|
currency={currency}
|
|
4807
5245
|
locale={locale}
|
|
4808
5246
|
onSelectionsChange={updateAddOnSelections}
|
|
4809
|
-
minimumTotalByAddOnId={initialAddOnMinTotalByAddOnId}
|
|
5247
|
+
minimumTotalByAddOnId={isCustomerSelfServeChange ? initialAddOnMinTotalByAddOnId : undefined}
|
|
4810
5248
|
suppressPrices={false}
|
|
4811
5249
|
/>
|
|
4812
5250
|
)}
|
|
@@ -4836,15 +5274,99 @@ export function AdminChangeBookingFlow({
|
|
|
4836
5274
|
currency={currency}
|
|
4837
5275
|
locale={locale}
|
|
4838
5276
|
t={t}
|
|
4839
|
-
extraBetweenTaxAndTotal={
|
|
4840
|
-
|
|
5277
|
+
extraBetweenTaxAndTotal={
|
|
5278
|
+
<>
|
|
5279
|
+
{showProviderPricingInlineEditor && providerPricingUi?.error ? (
|
|
5280
|
+
<div className="mt-2 text-sm text-red-700">{providerPricingUi.error}</div>
|
|
5281
|
+
) : null}
|
|
5282
|
+
{showProviderPricingInlineEditor &&
|
|
5283
|
+
providerPricingUi?.loading &&
|
|
5284
|
+
providerQuotedLines.length === 0 ? (
|
|
5285
|
+
<div className="mt-2 text-sm text-stone-500">Loading price lines...</div>
|
|
5286
|
+
) : null}
|
|
5287
|
+
{showProviderPricingInlineEditor &&
|
|
5288
|
+
providerPricingUi?.helperText &&
|
|
5289
|
+
!providerPricingUi.error ? (
|
|
5290
|
+
<div className="mt-2 text-xs text-stone-500">{providerPricingUi.helperText}</div>
|
|
5291
|
+
) : null}
|
|
5292
|
+
</>
|
|
5293
|
+
}
|
|
5294
|
+
extraBeforeSubtotal={
|
|
5295
|
+
showProviderPricingInlineEditor && (providerPricingUi?.additionalAdjustments?.length ?? 0) > 0 ? (
|
|
5296
|
+
<div className="space-y-1">
|
|
5297
|
+
{providerPricingUi?.additionalAdjustments?.map((adj) => (
|
|
5298
|
+
<div key={adj.id} className="flex items-center justify-between gap-2 text-sm">
|
|
5299
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
5300
|
+
<button
|
|
5301
|
+
type="button"
|
|
5302
|
+
className="rounded border border-stone-300 px-1 text-xs text-stone-600 hover:bg-stone-100"
|
|
5303
|
+
onClick={() => providerPricingUi?.onRemoveAdditionalAdjustment?.(adj.id)}
|
|
5304
|
+
>
|
|
5305
|
+
-
|
|
5306
|
+
</button>
|
|
5307
|
+
<input
|
|
5308
|
+
type="text"
|
|
5309
|
+
className="w-40 rounded border border-stone-300 px-2 py-0.5 text-sm"
|
|
5310
|
+
placeholder="Line description"
|
|
5311
|
+
value={adj.label}
|
|
5312
|
+
onChange={(e) =>
|
|
5313
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, { label: e.target.value })
|
|
5314
|
+
}
|
|
5315
|
+
/>
|
|
5316
|
+
</div>
|
|
5317
|
+
<div className="flex items-center gap-1">
|
|
5318
|
+
<select
|
|
5319
|
+
className="rounded border border-stone-300 px-1 py-0.5 text-xs"
|
|
5320
|
+
value={adj.mode}
|
|
5321
|
+
onChange={(e) =>
|
|
5322
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
5323
|
+
mode: e.target.value as 'DISCOUNT' | 'CHARGE',
|
|
5324
|
+
})
|
|
5325
|
+
}
|
|
5326
|
+
>
|
|
5327
|
+
<option value="DISCOUNT">-</option>
|
|
5328
|
+
<option value="CHARGE">+</option>
|
|
5329
|
+
</select>
|
|
5330
|
+
<input
|
|
5331
|
+
type="text"
|
|
5332
|
+
inputMode="decimal"
|
|
5333
|
+
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"
|
|
5334
|
+
placeholder="0.00"
|
|
5335
|
+
value={adj.amountInput}
|
|
5336
|
+
onChange={(e) =>
|
|
5337
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
5338
|
+
amountInput: e.target.value,
|
|
5339
|
+
})
|
|
5340
|
+
}
|
|
5341
|
+
/>
|
|
5342
|
+
</div>
|
|
5343
|
+
</div>
|
|
5344
|
+
))}
|
|
5345
|
+
<button
|
|
5346
|
+
type="button"
|
|
5347
|
+
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
5348
|
+
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
5349
|
+
>
|
|
5350
|
+
+ add line item
|
|
5351
|
+
</button>
|
|
5352
|
+
</div>
|
|
5353
|
+
) : showProviderPricingInlineEditor ? (
|
|
5354
|
+
<button
|
|
5355
|
+
type="button"
|
|
5356
|
+
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
5357
|
+
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
5358
|
+
>
|
|
5359
|
+
+ add line item
|
|
5360
|
+
</button>
|
|
5361
|
+
) : undefined
|
|
5362
|
+
}
|
|
4841
5363
|
firstName={firstName}
|
|
4842
5364
|
lastName={lastName}
|
|
4843
5365
|
email={email}
|
|
4844
5366
|
onFirstNameChange={(v) => { setFirstName(v); setError(''); }}
|
|
4845
5367
|
onLastNameChange={(v) => { setLastName(v); setError(''); }}
|
|
4846
5368
|
onEmailChange={(v) => { setEmail(v); setError(''); }}
|
|
4847
|
-
readOnlyContactFields
|
|
5369
|
+
readOnlyContactFields
|
|
4848
5370
|
pickupLocations={
|
|
4849
5371
|
selectedDate && product.pickupLocations && product.pickupLocations.length > 0
|
|
4850
5372
|
? product.pickupLocations
|
|
@@ -4878,29 +5400,38 @@ export function AdminChangeBookingFlow({
|
|
|
4878
5400
|
setTermsAccepted(checked);
|
|
4879
5401
|
setTermsAcceptedAt(checked ? new Date().toISOString() : null);
|
|
4880
5402
|
}}
|
|
4881
|
-
isAdmin={
|
|
5403
|
+
isAdmin={isAdmin}
|
|
4882
5404
|
showCommunicationAdminSection={false}
|
|
4883
|
-
skipConfirmationCommunications={
|
|
4884
|
-
disableAutoCommunications={
|
|
4885
|
-
onSkipConfirmationChange={
|
|
4886
|
-
onDisableCommunicationsChange={
|
|
5405
|
+
skipConfirmationCommunications={skipConfirmationCommunications}
|
|
5406
|
+
disableAutoCommunications={disableAutoCommunications}
|
|
5407
|
+
onSkipConfirmationChange={setSkipConfirmationCommunications}
|
|
5408
|
+
onDisableCommunicationsChange={setDisableAutoCommunications}
|
|
4887
5409
|
error={checkoutFormError}
|
|
4888
5410
|
loading={loading}
|
|
4889
5411
|
totalQuantity={totalQuantity}
|
|
4890
5412
|
onCheckout={handleCheckout}
|
|
4891
5413
|
submitLabel={changeCheckoutButtonLabel ?? deferredInvoiceSubmitLabel}
|
|
4892
5414
|
hideSubmitButton={
|
|
4893
|
-
showCheckoutModal ||
|
|
5415
|
+
showCheckoutModal ||
|
|
5416
|
+
showAdminPaymentChoice ||
|
|
5417
|
+
!hasEffectiveChangeSelection ||
|
|
5418
|
+
isChangeQuoteBlocked
|
|
4894
5419
|
}
|
|
4895
5420
|
submitDisabled={changeFlowSubmitDisabled}
|
|
4896
5421
|
attributionSummary={flowUi?.partnerAttributionSummary}
|
|
4897
5422
|
attributionConfirmLabel={flowUi?.partnerAttributionConfirmLabel}
|
|
4898
5423
|
attributionConfirmed={partnerAttributionConfirmed}
|
|
4899
5424
|
onAttributionConfirmedChange={setPartnerAttributionConfirmed}
|
|
4900
|
-
lineAmountInputs={undefined}
|
|
4901
|
-
onLineAmountInputChange={
|
|
4902
|
-
|
|
4903
|
-
|
|
5425
|
+
lineAmountInputs={showProviderPricingInlineEditor ? providerPricingUi?.lineAmountInputs : undefined}
|
|
5426
|
+
onLineAmountInputChange={
|
|
5427
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountInputChange : undefined
|
|
5428
|
+
}
|
|
5429
|
+
onLineAmountInputBlur={
|
|
5430
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountInputBlur : undefined
|
|
5431
|
+
}
|
|
5432
|
+
onLineAmountReset={
|
|
5433
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountReset : undefined
|
|
5434
|
+
}
|
|
4904
5435
|
/>
|
|
4905
5436
|
</>
|
|
4906
5437
|
)}
|