@ticketboothapp/booking 1.2.63 → 1.2.65
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 +158 -106
- package/src/components/booking/BookingFlow.tsx +2 -3
- package/src/components/booking/ChangeBookingFlow.tsx +158 -103
- package/src/components/booking/ChangeBookingPricingDriftPanel.tsx +130 -5
- package/src/components/booking/booking-flow-types.ts +3 -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
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
getAvailabilities,
|
|
8
8
|
cancelReservation,
|
|
9
9
|
cancelReservationBestEffort,
|
|
10
|
-
createPaymentIntent,
|
|
11
10
|
quoteChangeBooking,
|
|
12
11
|
confirmFreeChangeBooking,
|
|
13
12
|
createChangeBookingPaymentIntent,
|
|
@@ -62,10 +61,11 @@ import {
|
|
|
62
61
|
} from '../../lib/booking/change-flow-pricing';
|
|
63
62
|
import {
|
|
64
63
|
mergePriceSummaryLinesForDrift,
|
|
64
|
+
mergeLineComparisonsWithFullDrift,
|
|
65
65
|
normalizePricingDriftDetailFromQuote,
|
|
66
66
|
normalizeTicketPricingTraceFromQuote,
|
|
67
67
|
sumPriceSummaryLinesMajorUnits,
|
|
68
|
-
|
|
68
|
+
computePricingDriftDelta,
|
|
69
69
|
} from '../../lib/booking/change-booking-pricing-drift';
|
|
70
70
|
import { ChangeBookingPricingDriftPanel } from './ChangeBookingPricingDriftPanel';
|
|
71
71
|
import {
|
|
@@ -109,7 +109,6 @@ export type { ChangeBookingFlowProps } from './booking-flow-types';
|
|
|
109
109
|
* `serverPreview.priceSummaryLines` when the quote includes rows; otherwise falls back to FE-built lines after totals confirm.
|
|
110
110
|
*
|
|
111
111
|
* Until the first successful quote, `selfServeCheckoutPlaceholder` still avoids showing unchecked totals.
|
|
112
|
-
*
|
|
113
112
|
*/
|
|
114
113
|
function mergeQuoteSliceWithServerPreview(
|
|
115
114
|
slice: ReturnType<typeof sliceChangeQuoteForUi>,
|
|
@@ -631,7 +630,7 @@ function resolveInitialAvailabilityFromBooking(
|
|
|
631
630
|
}
|
|
632
631
|
|
|
633
632
|
/**
|
|
634
|
-
* Customer
|
|
633
|
+
* Customer **change booking** self-serve flow (`quoteChangeBooking`, Stripe).
|
|
635
634
|
* Duplicated from {@link NewBookingFlow} intentionally so each flow can evolve independently.
|
|
636
635
|
*/
|
|
637
636
|
export function ChangeBookingFlow({
|
|
@@ -679,6 +678,7 @@ export function ChangeBookingFlow({
|
|
|
679
678
|
onShowManage,
|
|
680
679
|
getSuccessUrl,
|
|
681
680
|
suppressCalendarDateScroll,
|
|
681
|
+
mode: bookingAppMode,
|
|
682
682
|
} = useBookingApp();
|
|
683
683
|
const availabilitiesCache = useAvailabilitiesCache();
|
|
684
684
|
const [availabilities, setAvailabilities] = useState<Availability[]>([]);
|
|
@@ -799,6 +799,9 @@ export function ChangeBookingFlow({
|
|
|
799
799
|
}, [initialValues?.dateTime, companyTimezone]);
|
|
800
800
|
/** Do not render catalog-/FE-derived dollar amounts in UI until `quoteChangeBooking` returns `serverDisplay`. */
|
|
801
801
|
const suppressSelfServeCurrencyUi = true;
|
|
802
|
+
/** Self-serve change flow only (no provider/admin pricing paths). */
|
|
803
|
+
const isCustomerSelfServeChange = true;
|
|
804
|
+
const isAdmin = false;
|
|
802
805
|
|
|
803
806
|
useEffect(() => {
|
|
804
807
|
setPartnerAttributionConfirmed(false);
|
|
@@ -1790,27 +1793,27 @@ export function ChangeBookingFlow({
|
|
|
1790
1793
|
|
|
1791
1794
|
const initialAddOnMinQtyByKey = useMemo(() => {
|
|
1792
1795
|
const map = new Map<string, number>();
|
|
1793
|
-
if (
|
|
1796
|
+
if (!isCustomerSelfServeChange) return map;
|
|
1794
1797
|
for (const sel of initialAddOnBaselineSelections) {
|
|
1795
1798
|
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
1796
1799
|
map.set(key, (map.get(key) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
1797
1800
|
}
|
|
1798
1801
|
return map;
|
|
1799
|
-
}, [
|
|
1802
|
+
}, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
|
|
1800
1803
|
|
|
1801
1804
|
const initialAddOnMinTotalByAddOnId = useMemo(() => {
|
|
1802
1805
|
const map = new Map<string, number>();
|
|
1803
|
-
if (
|
|
1806
|
+
if (!isCustomerSelfServeChange) return map;
|
|
1804
1807
|
for (const sel of initialAddOnBaselineSelections) {
|
|
1805
1808
|
const addOnId = sel.addOnId.trim();
|
|
1806
1809
|
map.set(addOnId, (map.get(addOnId) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
1807
1810
|
}
|
|
1808
1811
|
return map;
|
|
1809
|
-
}, [
|
|
1812
|
+
}, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
|
|
1810
1813
|
|
|
1811
1814
|
const applyChangeFlowAddOnFloor = useCallback(
|
|
1812
1815
|
(nextSelections: Array<{ addOnId: string; variantId?: string; quantity?: number }>) => {
|
|
1813
|
-
if (
|
|
1816
|
+
if (!isCustomerSelfServeChange || initialAddOnMinQtyByKey.size === 0) return nextSelections;
|
|
1814
1817
|
const qtyByKey = new Map<string, number>();
|
|
1815
1818
|
for (const sel of nextSelections) {
|
|
1816
1819
|
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
@@ -1837,7 +1840,7 @@ export function ChangeBookingFlow({
|
|
|
1837
1840
|
return { addOnId, variantId, quantity: qty };
|
|
1838
1841
|
});
|
|
1839
1842
|
},
|
|
1840
|
-
[
|
|
1843
|
+
[isCustomerSelfServeChange, initialAddOnMinQtyByKey, initialAddOnMinTotalByAddOnId]
|
|
1841
1844
|
);
|
|
1842
1845
|
|
|
1843
1846
|
const updateAddOnSelections = useCallback(
|
|
@@ -2451,14 +2454,14 @@ export function ChangeBookingFlow({
|
|
|
2451
2454
|
const options = selectedAvailability?.returnOptions ?? [];
|
|
2452
2455
|
const serverReturnMap = latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId;
|
|
2453
2456
|
const floor = effectiveChangeFlowReturnUnitFloorPerPerson;
|
|
2454
|
-
const applyReturnFloor = floor != null &&
|
|
2457
|
+
const applyReturnFloor = floor != null && isCustomerSelfServeChange;
|
|
2455
2458
|
return options.map((opt) => {
|
|
2456
2459
|
const vacancyCredit = changeFlowSeatCreditForReturnAvailabilityId(opt.returnAvailabilityId);
|
|
2457
2460
|
const adjustedVacancies = Math.max(0, opt.vacancies ?? 0) + vacancyCredit;
|
|
2458
2461
|
const rawPerPerson = opt.priceAdjustmentByCurrency?.[currency] ?? 0;
|
|
2459
2462
|
const flooredPerPerson = applyReturnFloor ? Math.max(rawPerPerson, floor) : rawPerPerson;
|
|
2460
2463
|
let perPerson = flooredPerPerson;
|
|
2461
|
-
if (
|
|
2464
|
+
if (isCustomerSelfServeChange && serverReturnMap && opt.returnAvailabilityId) {
|
|
2462
2465
|
const sid = opt.returnAvailabilityId.trim();
|
|
2463
2466
|
const sp = serverReturnMap[sid];
|
|
2464
2467
|
if (sp != null && Number.isFinite(sp)) {
|
|
@@ -2481,7 +2484,7 @@ export function ChangeBookingFlow({
|
|
|
2481
2484
|
}, [
|
|
2482
2485
|
selectedAvailability?.returnOptions,
|
|
2483
2486
|
effectiveChangeFlowReturnUnitFloorPerPerson,
|
|
2484
|
-
|
|
2487
|
+
isCustomerSelfServeChange,
|
|
2485
2488
|
currency,
|
|
2486
2489
|
changeFlowSeatCreditForReturnAvailabilityId,
|
|
2487
2490
|
latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId,
|
|
@@ -2490,11 +2493,11 @@ export function ChangeBookingFlow({
|
|
|
2490
2493
|
const selectedReturnOptionWithFloor = useMemo(() => {
|
|
2491
2494
|
if (!selectedReturnOption) return selectedReturnOption;
|
|
2492
2495
|
const floor = effectiveChangeFlowReturnUnitFloorPerPerson;
|
|
2493
|
-
const applyReturnFloor = floor != null &&
|
|
2496
|
+
const applyReturnFloor = floor != null && isCustomerSelfServeChange;
|
|
2494
2497
|
const rawPerPerson = selectedReturnOption.priceAdjustmentByCurrency?.[currency] ?? 0;
|
|
2495
2498
|
let perPerson = rawPerPerson;
|
|
2496
2499
|
const serverReturnMap = latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId;
|
|
2497
|
-
if (
|
|
2500
|
+
if (isCustomerSelfServeChange && serverReturnMap && selectedReturnOption.returnAvailabilityId) {
|
|
2498
2501
|
const sid = selectedReturnOption.returnAvailabilityId.trim();
|
|
2499
2502
|
const sp = serverReturnMap[sid];
|
|
2500
2503
|
if (sp != null && Number.isFinite(sp)) {
|
|
@@ -2515,7 +2518,7 @@ export function ChangeBookingFlow({
|
|
|
2515
2518
|
}, [
|
|
2516
2519
|
selectedReturnOption,
|
|
2517
2520
|
effectiveChangeFlowReturnUnitFloorPerPerson,
|
|
2518
|
-
|
|
2521
|
+
isCustomerSelfServeChange,
|
|
2519
2522
|
currency,
|
|
2520
2523
|
latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId,
|
|
2521
2524
|
]);
|
|
@@ -2556,7 +2559,7 @@ export function ChangeBookingFlow({
|
|
|
2556
2559
|
/** Quote `ticketUnitPriceByCategory` overrides catalog units for ticket rows (customer self-serve only). */
|
|
2557
2560
|
const pricingForTicketSelector = useMemo(() => {
|
|
2558
2561
|
const m = latestChangeQuote?.serverPreview?.ticketUnitPriceByCategory;
|
|
2559
|
-
if (
|
|
2562
|
+
if (!isCustomerSelfServeChange || !m) return pricing;
|
|
2560
2563
|
return pricing.map((r) => {
|
|
2561
2564
|
const u = m[r.category.toUpperCase()];
|
|
2562
2565
|
if (u == null || !Number.isFinite(u)) return r;
|
|
@@ -2567,7 +2570,7 @@ export function ChangeBookingFlow({
|
|
|
2567
2570
|
baseInDisplayCurrency: u,
|
|
2568
2571
|
};
|
|
2569
2572
|
});
|
|
2570
|
-
}, [pricing, latestChangeQuote?.serverPreview?.ticketUnitPriceByCategory,
|
|
2573
|
+
}, [pricing, latestChangeQuote?.serverPreview?.ticketUnitPriceByCategory, isCustomerSelfServeChange]);
|
|
2571
2574
|
|
|
2572
2575
|
// Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
|
|
2573
2576
|
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 +2675,7 @@ export function ChangeBookingFlow({
|
|
|
2672
2675
|
|
|
2673
2676
|
// When return selection (or refreshed availabilities) caps the party below current ticket counts, trim tickets (non-admin).
|
|
2674
2677
|
useEffect(() => {
|
|
2675
|
-
if (
|
|
2678
|
+
if (isAdmin || !selectedAvailability) return;
|
|
2676
2679
|
const cap = effectivePartySizeCap;
|
|
2677
2680
|
if (totalQuantity <= cap) return;
|
|
2678
2681
|
const over = totalQuantity - cap;
|
|
@@ -2700,7 +2703,7 @@ export function ChangeBookingFlow({
|
|
|
2700
2703
|
effectivePartySizeCap,
|
|
2701
2704
|
totalQuantity,
|
|
2702
2705
|
selectedAvailability,
|
|
2703
|
-
|
|
2706
|
+
isAdmin,
|
|
2704
2707
|
changeBookingMinimumQuantities,
|
|
2705
2708
|
]);
|
|
2706
2709
|
|
|
@@ -2774,7 +2777,6 @@ export function ChangeBookingFlow({
|
|
|
2774
2777
|
*/
|
|
2775
2778
|
const changeFlowProtectedReturnAdjustment = useMemo(() => {
|
|
2776
2779
|
if (totalQuantity <= 0) return returnPriceAdjustment;
|
|
2777
|
-
if (false) return returnPriceAdjustment;
|
|
2778
2780
|
if (effectiveChangeFlowReturnUnitFloorPerPerson == null) return returnPriceAdjustment;
|
|
2779
2781
|
const livePerPerson =
|
|
2780
2782
|
returnOptionCatalogPerPerson ?? (selectedReturnOption?.priceAdjustmentByCurrency?.[currency] ?? 0);
|
|
@@ -2788,7 +2790,6 @@ export function ChangeBookingFlow({
|
|
|
2788
2790
|
}, [
|
|
2789
2791
|
totalQuantity,
|
|
2790
2792
|
returnPriceAdjustment,
|
|
2791
|
-
false,
|
|
2792
2793
|
effectiveChangeFlowReturnUnitFloorPerPerson,
|
|
2793
2794
|
selectedReturnOption,
|
|
2794
2795
|
returnOptionCatalogPerPerson,
|
|
@@ -2799,7 +2800,7 @@ export function ChangeBookingFlow({
|
|
|
2799
2800
|
|
|
2800
2801
|
/** Return row amount for PriceSummary, Stripe breakdown, and CheckoutModal (catalog vs protected same-product-option). */
|
|
2801
2802
|
const checkoutReturnLineAmount = useMemo(() => {
|
|
2802
|
-
if (
|
|
2803
|
+
if (isCustomerSelfServeChange) {
|
|
2803
2804
|
return changeFlowProtectedReturnAdjustment;
|
|
2804
2805
|
}
|
|
2805
2806
|
if (changeFlowApplyReceiptPaidFloors) {
|
|
@@ -2807,7 +2808,7 @@ export function ChangeBookingFlow({
|
|
|
2807
2808
|
}
|
|
2808
2809
|
return returnPriceAdjustment;
|
|
2809
2810
|
}, [
|
|
2810
|
-
|
|
2811
|
+
isCustomerSelfServeChange,
|
|
2811
2812
|
changeFlowApplyReceiptPaidFloors,
|
|
2812
2813
|
changeFlowProtectedReturnAdjustment,
|
|
2813
2814
|
returnPriceAdjustment,
|
|
@@ -2879,13 +2880,12 @@ export function ChangeBookingFlow({
|
|
|
2879
2880
|
return [...feeLineItems, ...addOnLines];
|
|
2880
2881
|
}, [feeLineItems, addOnSelections, addOns]);
|
|
2881
2882
|
|
|
2883
|
+
|
|
2882
2884
|
const checkoutPriceSummaryLines = useMemo((): PriceSummaryLine[] => {
|
|
2883
2885
|
if (!selectedAvailability) return [];
|
|
2884
2886
|
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
2887
|
const showReturnLine =
|
|
2887
|
-
Boolean(selectedReturnOption) &&
|
|
2888
|
-
(Math.abs(returnLineAmount) > 0.0005 || effectiveChangeFlowReturnUnitFloorPerPerson != null);
|
|
2888
|
+
Boolean(selectedReturnOption) && Math.abs(returnLineAmount) > 0.0005;
|
|
2889
2889
|
return [
|
|
2890
2890
|
...ticketLineItemsForChangeFlowDisplay.map((line): PriceSummaryLine => {
|
|
2891
2891
|
const rate = pricing.find((r) => r.category === line.category);
|
|
@@ -2897,8 +2897,6 @@ export function ChangeBookingFlow({
|
|
|
2897
2897
|
);
|
|
2898
2898
|
return {
|
|
2899
2899
|
kind: 'ticket',
|
|
2900
|
-
lineKey: undefined,
|
|
2901
|
-
editable: false,
|
|
2902
2900
|
category: line.category,
|
|
2903
2901
|
qty: line.qty,
|
|
2904
2902
|
itemTotal: line.itemTotal,
|
|
@@ -2933,8 +2931,6 @@ export function ChangeBookingFlow({
|
|
|
2933
2931
|
fee.name.toLowerCase().includes('license'));
|
|
2934
2932
|
return {
|
|
2935
2933
|
kind: 'line' as const,
|
|
2936
|
-
lineKey: undefined,
|
|
2937
|
-
editable: false,
|
|
2938
2934
|
label: feeLineItems.some((f) => f.name === fee.name)
|
|
2939
2935
|
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
2940
2936
|
: fee.name,
|
|
@@ -2960,7 +2956,6 @@ export function ChangeBookingFlow({
|
|
|
2960
2956
|
effectiveCancellationPolicyLabel,
|
|
2961
2957
|
feeLineItemsWithAddOns,
|
|
2962
2958
|
feeLineItems,
|
|
2963
|
-
effectiveChangeFlowReturnUnitFloorPerPerson,
|
|
2964
2959
|
]);
|
|
2965
2960
|
|
|
2966
2961
|
const checkoutPriceSummaryLinesForCheckout = useMemo(() => {
|
|
@@ -3131,7 +3126,45 @@ export function ChangeBookingFlow({
|
|
|
3131
3126
|
|
|
3132
3127
|
const clientMappedFromApi = mapQuoteLineItemsToPriceSummaryLines(api?.clientLineItems);
|
|
3133
3128
|
const useBeClientLines = clientMappedFromApi.length > 0;
|
|
3134
|
-
|
|
3129
|
+
/**
|
|
3130
|
+
* Checkout passes tax/discount via PriceSummary props, not always as `priceSummaryLines`. Include them here so
|
|
3131
|
+
* drift rows and their sums align with `changeFlowNewBookingTotal` (subtotal + tax − promo).
|
|
3132
|
+
*/
|
|
3133
|
+
const clientLinesForMerge: PriceSummaryLine[] = (() => {
|
|
3134
|
+
if (useBeClientLines) return clientMappedFromApi;
|
|
3135
|
+
const lines: PriceSummaryLine[] = [...checkoutPriceSummaryLines];
|
|
3136
|
+
const hasTaxLine = lines.some(
|
|
3137
|
+
(l) => l.kind === 'line' && String(l.type ?? '').toUpperCase() === 'TAX',
|
|
3138
|
+
);
|
|
3139
|
+
if (!hasTaxLine && !isTaxIncludedInPrice && Math.abs(effectiveTax) >= 0.005) {
|
|
3140
|
+
lines.push({
|
|
3141
|
+
kind: 'line',
|
|
3142
|
+
label: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
|
|
3143
|
+
amount: effectiveTax,
|
|
3144
|
+
type: 'TAX',
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
const hasPromoSummaryLine = lines.some(
|
|
3148
|
+
(l) =>
|
|
3149
|
+
l.kind === 'line' &&
|
|
3150
|
+
(/PROMO|DISCOUNT|VOUCHER|GIFT/i.test(String(l.type ?? '')) ||
|
|
3151
|
+
(l.amount < -0.005 && /promo|discount/i.test(l.label))),
|
|
3152
|
+
);
|
|
3153
|
+
if (!hasPromoSummaryLine && Math.abs(effectivePromoDiscountAmount) >= 0.005) {
|
|
3154
|
+
const trimmedPromoCode = appliedPromoCode?.trim() ?? '';
|
|
3155
|
+
const promoLabel =
|
|
3156
|
+
trimmedPromoCode.length > 0
|
|
3157
|
+
? `Promo: ${trimmedPromoCode}`
|
|
3158
|
+
: originalReceipt?.promoLabel?.trim() || 'Discount';
|
|
3159
|
+
lines.push({
|
|
3160
|
+
kind: 'line',
|
|
3161
|
+
label: promoLabel,
|
|
3162
|
+
amount: -effectivePromoDiscountAmount,
|
|
3163
|
+
type: 'PROMO_CODE',
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
return lines;
|
|
3167
|
+
})();
|
|
3135
3168
|
|
|
3136
3169
|
const serverMappedFromApi = mapQuoteLineItemsToPriceSummaryLines(api?.serverLineItems);
|
|
3137
3170
|
const useBeServerLines = serverMappedFromApi.length > 0;
|
|
@@ -3175,14 +3208,35 @@ export function ChangeBookingFlow({
|
|
|
3175
3208
|
}
|
|
3176
3209
|
|
|
3177
3210
|
const mergedForDrift = mergePriceSummaryLinesForDrift(clientLinesForMerge, serverLinesForMerge);
|
|
3178
|
-
|
|
3211
|
+
const rows =
|
|
3179
3212
|
api?.lineComparisons && api.lineComparisons.length > 0
|
|
3180
|
-
?
|
|
3213
|
+
? mergeLineComparisonsWithFullDrift(api.lineComparisons, mergedForDrift)
|
|
3181
3214
|
: mergedForDrift;
|
|
3182
3215
|
|
|
3216
|
+
const receiptAnchoring = api?.receiptAnchoring;
|
|
3217
|
+
let driftRows = rows;
|
|
3218
|
+
if (receiptAnchoring) {
|
|
3219
|
+
const adj = roundMoney(
|
|
3220
|
+
receiptAnchoring.reconciledTotalMajorUnits -
|
|
3221
|
+
receiptAnchoring.requestedCatalogLineSumMajorUnits,
|
|
3222
|
+
);
|
|
3223
|
+
if (Math.abs(adj) >= 0.005) {
|
|
3224
|
+
driftRows = [
|
|
3225
|
+
...rows,
|
|
3226
|
+
{
|
|
3227
|
+
key: 'meta:receipt-anchoring',
|
|
3228
|
+
label: 'Receipt anchoring adjustment (BE only — not included in app total)',
|
|
3229
|
+
clientAmount: null,
|
|
3230
|
+
serverAmount: adj,
|
|
3231
|
+
delta: computePricingDriftDelta(null, adj),
|
|
3232
|
+
},
|
|
3233
|
+
];
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3183
3237
|
const hasTotalDelta = totalDelta != null && Math.abs(totalDelta) >= 0.005;
|
|
3184
3238
|
|
|
3185
|
-
if (
|
|
3239
|
+
if (driftRows.length === 0 && !hasTotalDelta) {
|
|
3186
3240
|
return null;
|
|
3187
3241
|
}
|
|
3188
3242
|
|
|
@@ -3211,7 +3265,7 @@ export function ChangeBookingFlow({
|
|
|
3211
3265
|
|
|
3212
3266
|
return (
|
|
3213
3267
|
<ChangeBookingPricingDriftPanel
|
|
3214
|
-
rows={
|
|
3268
|
+
rows={driftRows}
|
|
3215
3269
|
clientTotal={clientTotalForDrift}
|
|
3216
3270
|
serverTotal={serverTotalFromQuote}
|
|
3217
3271
|
totalDelta={totalDelta}
|
|
@@ -3219,6 +3273,7 @@ export function ChangeBookingFlow({
|
|
|
3219
3273
|
locale={locale}
|
|
3220
3274
|
ticketCartDetail={ticketCartDetail.length > 0 ? ticketCartDetail : undefined}
|
|
3221
3275
|
serverTicketPricingTrace={latestChangeQuote.ticketPricingTrace ?? undefined}
|
|
3276
|
+
receiptAnchoring={receiptAnchoring}
|
|
3222
3277
|
footnote={
|
|
3223
3278
|
usesBeLinePayload
|
|
3224
3279
|
? '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 +3292,12 @@ export function ChangeBookingFlow({
|
|
|
3237
3292
|
locale,
|
|
3238
3293
|
ticketLineItemsForChangeFlowDisplay,
|
|
3239
3294
|
pricingForTicketSelector,
|
|
3295
|
+
isTaxIncludedInPrice,
|
|
3296
|
+
effectiveTax,
|
|
3297
|
+
effectivePromoDiscountAmount,
|
|
3298
|
+
appliedPromoCode,
|
|
3299
|
+
originalReceipt?.promoLabel,
|
|
3300
|
+
t,
|
|
3240
3301
|
]);
|
|
3241
3302
|
|
|
3242
3303
|
/** Replaces PriceSummary with non-numeric status until quote returns authoritative totals (no FE dollar amounts). */
|
|
@@ -3398,8 +3459,7 @@ export function ChangeBookingFlow({
|
|
|
3398
3459
|
countsChanged ||
|
|
3399
3460
|
addOnsChanged ||
|
|
3400
3461
|
returnChanged,
|
|
3401
|
-
// Authoritative for "real user change" gating
|
|
3402
|
-
// ignore option-id noise and only consider user-visible booking deltas.
|
|
3462
|
+
// Authoritative for "real user change" gating: ignore option-id noise; only user-visible deltas.
|
|
3403
3463
|
hasOperationalChangesFromInitial:
|
|
3404
3464
|
dateChanged ||
|
|
3405
3465
|
pickupChanged ||
|
|
@@ -3429,28 +3489,25 @@ export function ChangeBookingFlow({
|
|
|
3429
3489
|
const hasChangeSelection = changeSelectionDetails.hasChangesFromInitial;
|
|
3430
3490
|
|
|
3431
3491
|
const changeFlowNeedsServerPrice =
|
|
3432
|
-
true &&
|
|
3433
3492
|
hasChangeSelection &&
|
|
3434
3493
|
!!initialValues?.bookingReference?.trim() &&
|
|
3435
3494
|
!!lastName.trim();
|
|
3436
3495
|
|
|
3437
|
-
const isChangeQuoteBlocked =
|
|
3438
|
-
const requiresReturnInChangeFlow =
|
|
3496
|
+
const isChangeQuoteBlocked = latestChangeQuote?.canProceed === false;
|
|
3497
|
+
const requiresReturnInChangeFlow = !!initialValues?.returnAvailabilityId?.trim();
|
|
3439
3498
|
const missingRequiredReturnSelection = requiresReturnInChangeFlow && !selectedReturnOption;
|
|
3440
3499
|
|
|
3441
3500
|
const changeFlowSubmitDisabled =
|
|
3442
3501
|
missingRequiredReturnSelection ||
|
|
3443
|
-
(
|
|
3444
|
-
changeFlowNeedsServerPrice &&
|
|
3502
|
+
(changeFlowNeedsServerPrice &&
|
|
3445
3503
|
(changeQuoteLoading || (!latestChangeQuote && !changeQuoteFetchError)));
|
|
3446
3504
|
|
|
3447
|
-
const providerTotalsPreview = null;
|
|
3448
3505
|
const hasEffectiveChangeSelection = hasChangeSelection;
|
|
3449
3506
|
|
|
3450
3507
|
const displayedChangeAmounts = resolveChangeFlowDisplayedAmounts({
|
|
3451
|
-
providerPreview:
|
|
3508
|
+
providerPreview: null,
|
|
3452
3509
|
serverQuotePreview:
|
|
3453
|
-
|
|
3510
|
+
latestChangeQuote?.serverDisplay
|
|
3454
3511
|
? latestChangeQuote.serverDisplay
|
|
3455
3512
|
: null,
|
|
3456
3513
|
fromCart: {
|
|
@@ -3466,7 +3523,7 @@ export function ChangeBookingFlow({
|
|
|
3466
3523
|
const changeFlowClientEstimateDue = (() => {
|
|
3467
3524
|
if (!originalReceipt) return totalPrice;
|
|
3468
3525
|
// Customer self-serve: amount due comes from POST .../change/quote (`amountDueCents` / priceDiff), not FE delta math.
|
|
3469
|
-
if (
|
|
3526
|
+
if (latestChangeQuote != null && !changeQuoteFetchError) {
|
|
3470
3527
|
return normalizeNearZeroOwed(latestChangeQuote.priceDiff);
|
|
3471
3528
|
}
|
|
3472
3529
|
return changeFlowBalanceVsOriginal({
|
|
@@ -3514,8 +3571,8 @@ export function ChangeBookingFlow({
|
|
|
3514
3571
|
const checkoutFormError =
|
|
3515
3572
|
(error || '') ||
|
|
3516
3573
|
(missingRequiredReturnSelection ? 'Removing return option in self-serve is not available. Please contact support.' : '') ||
|
|
3517
|
-
(
|
|
3518
|
-
(
|
|
3574
|
+
(isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
|
|
3575
|
+
(changeQuoteFetchError ?? '');
|
|
3519
3576
|
|
|
3520
3577
|
const changeFlowSelectionPreview = useMemo((): ChangeFlowSelectionPreview | null => {
|
|
3521
3578
|
if (!selectedAvailability || totalQuantity <= 0) return null;
|
|
@@ -3621,7 +3678,7 @@ export function ChangeBookingFlow({
|
|
|
3621
3678
|
|
|
3622
3679
|
/** Debounced server quote so CTA + “amount owed” match PaymentIntent; avoids free confirm when FE estimate ≠ BE. */
|
|
3623
3680
|
useEffect(() => {
|
|
3624
|
-
if (
|
|
3681
|
+
if (!isCustomerSelfServeChange) {
|
|
3625
3682
|
setChangeQuoteLoading(false);
|
|
3626
3683
|
setChangeQuoteFetchError(null);
|
|
3627
3684
|
setLatestChangeQuote(null);
|
|
@@ -3712,7 +3769,7 @@ export function ChangeBookingFlow({
|
|
|
3712
3769
|
window.clearTimeout(timer);
|
|
3713
3770
|
};
|
|
3714
3771
|
}, [
|
|
3715
|
-
|
|
3772
|
+
isCustomerSelfServeChange,
|
|
3716
3773
|
hasChangeSelection,
|
|
3717
3774
|
selectedAvailability,
|
|
3718
3775
|
selectedAvailability?.dateTime,
|
|
@@ -3900,7 +3957,7 @@ export function ChangeBookingFlow({
|
|
|
3900
3957
|
selectedAvailability,
|
|
3901
3958
|
selectedReturnOption,
|
|
3902
3959
|
companyTimezone,
|
|
3903
|
-
|
|
3960
|
+
isAdmin,
|
|
3904
3961
|
initialValues?.returnAvailabilityId,
|
|
3905
3962
|
initialValues?.returnDateTime,
|
|
3906
3963
|
]);
|
|
@@ -3963,9 +4020,9 @@ export function ChangeBookingFlow({
|
|
|
3963
4020
|
const firstWithInventory = dates.find((d) => {
|
|
3964
4021
|
const rows = availabilitiesByDate[d] ?? [];
|
|
3965
4022
|
if (rows.length === 0) return false;
|
|
3966
|
-
if (
|
|
4023
|
+
if (isAdmin) return rows.some((a) => (a.vacancies ?? 0) > 0);
|
|
3967
4024
|
if (
|
|
3968
|
-
|
|
4025
|
+
isCustomerSelfServeChange &&
|
|
3969
4026
|
changeFlowInitialTicketCount > 0
|
|
3970
4027
|
) {
|
|
3971
4028
|
return rows.some(
|
|
@@ -3974,7 +4031,7 @@ export function ChangeBookingFlow({
|
|
|
3974
4031
|
}
|
|
3975
4032
|
return rows.some((a) => (a.vacancies ?? 0) > 0);
|
|
3976
4033
|
});
|
|
3977
|
-
const first = firstWithInventory ?? (
|
|
4034
|
+
const first = firstWithInventory ?? (isAdmin && dates[0] ? dates[0] : undefined);
|
|
3978
4035
|
if (!first) return;
|
|
3979
4036
|
|
|
3980
4037
|
hasAutoSelectedPartnerDateRef.current = true;
|
|
@@ -3997,8 +4054,8 @@ export function ChangeBookingFlow({
|
|
|
3997
4054
|
selectedDate,
|
|
3998
4055
|
dates,
|
|
3999
4056
|
availabilitiesByDate,
|
|
4000
|
-
|
|
4001
|
-
|
|
4057
|
+
isAdmin,
|
|
4058
|
+
isCustomerSelfServeChange,
|
|
4002
4059
|
changeFlowInitialTicketCount,
|
|
4003
4060
|
getCalendarEffectiveOutboundVacancies,
|
|
4004
4061
|
useWindowScroll,
|
|
@@ -4036,7 +4093,7 @@ export function ChangeBookingFlow({
|
|
|
4036
4093
|
};
|
|
4037
4094
|
|
|
4038
4095
|
const handleQuantityChange = (category: string, delta: number) => {
|
|
4039
|
-
const maxAvailable =
|
|
4096
|
+
const maxAvailable = isAdmin ? Number.MAX_SAFE_INTEGER : effectivePartySizeCap;
|
|
4040
4097
|
const currentQty = quantities[category] || 0;
|
|
4041
4098
|
const minQ =
|
|
4042
4099
|
changeBookingMinimumQuantities != null
|
|
@@ -4044,7 +4101,7 @@ export function ChangeBookingFlow({
|
|
|
4044
4101
|
: 0;
|
|
4045
4102
|
const newQty = Math.max(minQ, currentQty + delta);
|
|
4046
4103
|
// Admin can overbook; non-admin cannot exceed vacancies
|
|
4047
|
-
if (delta > 0 && !
|
|
4104
|
+
if (delta > 0 && !isAdmin && orderSummary.totalQuantity >= maxAvailable) {
|
|
4048
4105
|
return;
|
|
4049
4106
|
}
|
|
4050
4107
|
setQuantities(prev => ({
|
|
@@ -4177,28 +4234,28 @@ export function ChangeBookingFlow({
|
|
|
4177
4234
|
return;
|
|
4178
4235
|
}
|
|
4179
4236
|
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4237
|
+
// Validate email (required)
|
|
4238
|
+
if (!email) {
|
|
4239
|
+
setError(t('booking.enterEmail') || 'Please enter your email address');
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4185
4242
|
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4243
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
4244
|
+
setError(t('booking.invalidEmail') || 'Please enter a valid email address');
|
|
4245
|
+
return;
|
|
4246
|
+
}
|
|
4190
4247
|
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4248
|
+
// Validate first name (required)
|
|
4249
|
+
if (!firstName?.trim()) {
|
|
4250
|
+
setError(t('booking.enterFirstName') || 'Please enter your first name');
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4196
4253
|
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4254
|
+
// Validate last name (required for manage booking lookup)
|
|
4255
|
+
if (!lastName?.trim()) {
|
|
4256
|
+
setError(t('booking.enterLastName') || 'Please enter your last name');
|
|
4257
|
+
return;
|
|
4258
|
+
}
|
|
4202
4259
|
|
|
4203
4260
|
// Allow checkout if pickup location is selected OR if user chose "I don't know"
|
|
4204
4261
|
if (product.pickupLocations && product.pickupLocations.length > 0 && !pickupLocationId && !pickupLocationSkipped) {
|
|
@@ -4225,24 +4282,23 @@ export function ChangeBookingFlow({
|
|
|
4225
4282
|
return;
|
|
4226
4283
|
}
|
|
4227
4284
|
|
|
4228
|
-
|
|
4229
4285
|
const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
|
|
4230
4286
|
clientChannelSource: inferClientBookingSourceFromProductIds(
|
|
4231
4287
|
product.productId,
|
|
4232
4288
|
availabilityProductOptionId,
|
|
4233
4289
|
),
|
|
4234
4290
|
forcePartnerPortalChannel: partnerPortalBooking,
|
|
4235
|
-
forceDashboardSource:
|
|
4291
|
+
forceDashboardSource: bookingAppMode === 'provider-dashboard',
|
|
4236
4292
|
});
|
|
4237
4293
|
|
|
4238
4294
|
// Get the hotel name if a pickup location was selected
|
|
4239
4295
|
const selectedPickupLocation = pickupLocationId
|
|
4240
4296
|
? product.pickupLocations?.find(loc => loc.id === pickupLocationId)
|
|
4241
4297
|
: null;
|
|
4298
|
+
let quotedPriceDiff: number | null = null;
|
|
4242
4299
|
let changeIntentIdForCheckout: string | undefined;
|
|
4243
4300
|
let changeBookingReferenceForPaidFlow: string | undefined;
|
|
4244
4301
|
|
|
4245
|
-
{
|
|
4246
4302
|
const changeBookingReference = initialValues?.bookingReference?.trim();
|
|
4247
4303
|
const changeLastName = lastName.trim();
|
|
4248
4304
|
if (!changeBookingReference || !changeLastName) {
|
|
@@ -4270,6 +4326,7 @@ export function ChangeBookingFlow({
|
|
|
4270
4326
|
},
|
|
4271
4327
|
currency
|
|
4272
4328
|
);
|
|
4329
|
+
quotedPriceDiff = quoteSlice.priceDiff;
|
|
4273
4330
|
changeBookingReferenceForPaidFlow = changeBookingReference;
|
|
4274
4331
|
changeIntentIdForCheckout = quoteSlice.changeIntentId ?? undefined;
|
|
4275
4332
|
setLatestChangeQuote(
|
|
@@ -4329,13 +4386,12 @@ export function ChangeBookingFlow({
|
|
|
4329
4386
|
if (!changeIntentIdForCheckout) {
|
|
4330
4387
|
throw new Error('Missing change intent for payment.');
|
|
4331
4388
|
}
|
|
4332
|
-
}
|
|
4333
4389
|
|
|
4334
4390
|
pendingReservationRef.current = null;
|
|
4335
4391
|
|
|
4336
4392
|
// Note: Do NOT call onSuccess here for paid bookings — we're about to show the Stripe
|
|
4337
4393
|
// CheckoutModal. onSuccess (e.g. closing the parent dialog) should only run when we're
|
|
4338
|
-
// actually done (free booking redirect
|
|
4394
|
+
// actually done (free booking redirect). Calling it here
|
|
4339
4395
|
// would close the dialog before the payment modal opens.
|
|
4340
4396
|
|
|
4341
4397
|
// Update stored booking data (no holds reservation — change flow keys off booking reference elsewhere).
|
|
@@ -4362,10 +4418,10 @@ export function ChangeBookingFlow({
|
|
|
4362
4418
|
// Backend will charge totalAmount and store this as the receipt so /manage matches.
|
|
4363
4419
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
4364
4420
|
const amountDueForCheckout = changeFlowBalanceVsOriginal({
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4421
|
+
newTotal: changeFlowNewBookingTotal,
|
|
4422
|
+
originalReceiptTotal: originalReceipt?.total ?? 0,
|
|
4423
|
+
audience: 'customer',
|
|
4424
|
+
});
|
|
4369
4425
|
const lines = [
|
|
4370
4426
|
...ticketLineItemsForChangeFlowDisplay.map((line) => ({
|
|
4371
4427
|
label: line.category,
|
|
@@ -4452,7 +4508,6 @@ export function ChangeBookingFlow({
|
|
|
4452
4508
|
})()
|
|
4453
4509
|
);
|
|
4454
4510
|
|
|
4455
|
-
|
|
4456
4511
|
const ticketLinesForModal: CheckoutModalLineItem[] = ticketLineItemsForChangeFlowDisplay.map((line) => {
|
|
4457
4512
|
const rate = pricing.find((r) => r.category === line.category);
|
|
4458
4513
|
const breakdown = getPriceBreakdown(
|
|
@@ -4473,7 +4528,7 @@ export function ChangeBookingFlow({
|
|
|
4473
4528
|
// Paid change: always return to stable ref+lastName + explicit intent (not reservationRef).
|
|
4474
4529
|
// /manage-booking runs bounded refresh only when `from=change_payment` (see manage-booking page).
|
|
4475
4530
|
successUrlOverride:
|
|
4476
|
-
|
|
4531
|
+
changeBookingReferenceForPaidFlow
|
|
4477
4532
|
? (() => {
|
|
4478
4533
|
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
4479
4534
|
const ref = encodeURIComponent(
|
|
@@ -4501,7 +4556,7 @@ export function ChangeBookingFlow({
|
|
|
4501
4556
|
promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
|
|
4502
4557
|
discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : (originalReceipt?.promoLabel || undefined),
|
|
4503
4558
|
changeTotals:
|
|
4504
|
-
|
|
4559
|
+
originalReceipt
|
|
4505
4560
|
? {
|
|
4506
4561
|
previousTotal: originalReceipt.total,
|
|
4507
4562
|
newTotal: displayChangeFlowProposedTotal,
|
|
@@ -4570,8 +4625,6 @@ export function ChangeBookingFlow({
|
|
|
4570
4625
|
}
|
|
4571
4626
|
};
|
|
4572
4627
|
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
4628
|
if (activeOptions.length === 0) {
|
|
4576
4629
|
return (
|
|
4577
4630
|
<div className="flex items-center justify-center py-16">
|
|
@@ -4655,12 +4708,12 @@ export function ChangeBookingFlow({
|
|
|
4655
4708
|
syncVisibleWeekToSelectedDate={true}
|
|
4656
4709
|
selectableSoldOutDate={changeFlowOriginalDate}
|
|
4657
4710
|
partySizeRequiredForCalendarSelection={
|
|
4658
|
-
|
|
4711
|
+
isCustomerSelfServeChange && !isAdmin && changeFlowInitialTicketCount > 0
|
|
4659
4712
|
? changeFlowInitialTicketCount
|
|
4660
4713
|
: undefined
|
|
4661
4714
|
}
|
|
4662
4715
|
getEffectiveVacancies={
|
|
4663
|
-
|
|
4716
|
+
isCustomerSelfServeChange && !isAdmin && changeFlowInitialTicketCount > 0
|
|
4664
4717
|
? getCalendarEffectiveOutboundVacancies
|
|
4665
4718
|
: undefined
|
|
4666
4719
|
}
|
|
@@ -4684,7 +4737,7 @@ export function ChangeBookingFlow({
|
|
|
4684
4737
|
earliestDate={earliestAvailabilityDate}
|
|
4685
4738
|
onVisibleRangeChange={handleVisibleRangeChange}
|
|
4686
4739
|
currency={currency}
|
|
4687
|
-
showCapacity={
|
|
4740
|
+
showCapacity={isAdmin}
|
|
4688
4741
|
extraDiscountPercent={calendarDiscountPercent}
|
|
4689
4742
|
capDiscountBadgesToBookingDate={changeFlowOriginalDate}
|
|
4690
4743
|
/>
|
|
@@ -4740,7 +4793,7 @@ export function ChangeBookingFlow({
|
|
|
4740
4793
|
selectedTicketCount={totalQuantity}
|
|
4741
4794
|
optionsMap={optionsMap}
|
|
4742
4795
|
hasAnyMostPopular={hasAnyMostPopular}
|
|
4743
|
-
isAdmin={
|
|
4796
|
+
isAdmin={isAdmin}
|
|
4744
4797
|
pickupLocationSkipped={pickupLocationSkipped}
|
|
4745
4798
|
t={t}
|
|
4746
4799
|
onTimeSelect={handleTimeSelect}
|
|
@@ -4756,7 +4809,7 @@ export function ChangeBookingFlow({
|
|
|
4756
4809
|
companyTimezone={companyTimezone}
|
|
4757
4810
|
currency={currency}
|
|
4758
4811
|
locale={locale}
|
|
4759
|
-
isAdmin={
|
|
4812
|
+
isAdmin={isAdmin}
|
|
4760
4813
|
t={t}
|
|
4761
4814
|
onReturnSelect={(option) => {
|
|
4762
4815
|
const raw = selectedAvailability.returnOptions?.find(
|
|
@@ -4784,7 +4837,7 @@ export function ChangeBookingFlow({
|
|
|
4784
4837
|
resourceCount={selectedReturnOption ? null : (selectedAvailability.resourceCount ?? null)}
|
|
4785
4838
|
currency={currency}
|
|
4786
4839
|
locale={locale}
|
|
4787
|
-
isAdmin={
|
|
4840
|
+
isAdmin={isAdmin}
|
|
4788
4841
|
isSimplifiedPricingView={isSimplifiedPricingView}
|
|
4789
4842
|
t={t}
|
|
4790
4843
|
onQuantityChange={handleQuantityChange}
|
|
@@ -4806,7 +4859,7 @@ export function ChangeBookingFlow({
|
|
|
4806
4859
|
currency={currency}
|
|
4807
4860
|
locale={locale}
|
|
4808
4861
|
onSelectionsChange={updateAddOnSelections}
|
|
4809
|
-
minimumTotalByAddOnId={initialAddOnMinTotalByAddOnId}
|
|
4862
|
+
minimumTotalByAddOnId={isCustomerSelfServeChange ? initialAddOnMinTotalByAddOnId : undefined}
|
|
4810
4863
|
suppressPrices={false}
|
|
4811
4864
|
/>
|
|
4812
4865
|
)}
|
|
@@ -4836,7 +4889,7 @@ export function ChangeBookingFlow({
|
|
|
4836
4889
|
currency={currency}
|
|
4837
4890
|
locale={locale}
|
|
4838
4891
|
t={t}
|
|
4839
|
-
extraBetweenTaxAndTotal={
|
|
4892
|
+
extraBetweenTaxAndTotal={<></>}
|
|
4840
4893
|
extraBeforeSubtotal={undefined}
|
|
4841
4894
|
firstName={firstName}
|
|
4842
4895
|
lastName={lastName}
|
|
@@ -4844,7 +4897,7 @@ export function ChangeBookingFlow({
|
|
|
4844
4897
|
onFirstNameChange={(v) => { setFirstName(v); setError(''); }}
|
|
4845
4898
|
onLastNameChange={(v) => { setLastName(v); setError(''); }}
|
|
4846
4899
|
onEmailChange={(v) => { setEmail(v); setError(''); }}
|
|
4847
|
-
readOnlyContactFields
|
|
4900
|
+
readOnlyContactFields
|
|
4848
4901
|
pickupLocations={
|
|
4849
4902
|
selectedDate && product.pickupLocations && product.pickupLocations.length > 0
|
|
4850
4903
|
? product.pickupLocations
|
|
@@ -4890,7 +4943,9 @@ export function ChangeBookingFlow({
|
|
|
4890
4943
|
onCheckout={handleCheckout}
|
|
4891
4944
|
submitLabel={changeCheckoutButtonLabel ?? deferredInvoiceSubmitLabel}
|
|
4892
4945
|
hideSubmitButton={
|
|
4893
|
-
showCheckoutModal ||
|
|
4946
|
+
showCheckoutModal ||
|
|
4947
|
+
!hasEffectiveChangeSelection ||
|
|
4948
|
+
isChangeQuoteBlocked
|
|
4894
4949
|
}
|
|
4895
4950
|
submitDisabled={changeFlowSubmitDisabled}
|
|
4896
4951
|
attributionSummary={flowUi?.partnerAttributionSummary}
|