@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.
@@ -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
- enrichLineComparisonsWithMergedRows,
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
- * Customer self-serve **change booking** only (no standard new-booking paths).
635
- * Duplicated from {@link NewBookingFlow} intentionally so each flow can evolve independently.
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 = true;
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
- /** Promo from booking is fixed — show read-only, never add new. */
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: Rule A (exact receipt unit when same calendar day
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
- () => changeFlowBookingParentProductIdForFloors === product.productId.trim(),
1667
- [changeFlowBookingParentProductIdForFloors, product.productId],
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 (false) return map;
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
- }, [true, initialAddOnBaselineSelections]);
1851
+ }, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
1800
1852
 
1801
1853
  const initialAddOnMinTotalByAddOnId = useMemo(() => {
1802
1854
  const map = new Map<string, number>();
1803
- if (false) return map;
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
- }, [true, initialAddOnBaselineSelections]);
1861
+ }, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
1810
1862
 
1811
1863
  const applyChangeFlowAddOnFloor = useCallback(
1812
1864
  (nextSelections: Array<{ addOnId: string; variantId?: string; quantity?: number }>) => {
1813
- if (false || initialAddOnMinQtyByKey.size === 0) return nextSelections;
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
- [true, initialAddOnMinQtyByKey, initialAddOnMinTotalByAddOnId]
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 && true;
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 (true && serverReturnMap && opt.returnAvailabilityId) {
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
- true,
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 && true;
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 (true && serverReturnMap && selectedReturnOption.returnAvailabilityId) {
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
- true,
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 (false || !m) return pricing;
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, true]);
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 (false || !selectedAvailability) return;
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
- false,
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 (false) return returnPriceAdjustment;
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
- false,
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 (true) {
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
- true,
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: undefined,
2901
- editable: false,
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: undefined,
2937
- editable: false,
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
- effectiveChangeFlowReturnUnitFloorPerPerson,
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
- const clientLinesForMerge = useBeClientLines ? clientMappedFromApi : checkoutPriceSummaryLines;
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
- let rows =
3313
+ const rows =
3179
3314
  api?.lineComparisons && api.lineComparisons.length > 0
3180
- ? enrichLineComparisonsWithMergedRows(api.lineComparisons, mergedForDrift)
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 (rows.length === 0 && !hasTotalDelta) {
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={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 = changeSelectionDetails.hasChangesFromInitial;
3592
+ const hasChangeSelection =
3593
+ isProviderDashboardChange
3594
+ ? changeSelectionDetails.hasOperationalChangesFromInitial
3595
+ : changeSelectionDetails.hasChangesFromInitial;
3430
3596
 
3431
3597
  const changeFlowNeedsServerPrice =
3432
- true &&
3598
+ isCustomerSelfServeChange &&
3433
3599
  hasChangeSelection &&
3434
3600
  !!initialValues?.bookingReference?.trim() &&
3435
3601
  !!lastName.trim();
3436
3602
 
3437
- const isChangeQuoteBlocked = true && latestChangeQuote?.canProceed === false;
3438
- const requiresReturnInChangeFlow = true && !!initialValues?.returnAvailabilityId?.trim();
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
- (true &&
3609
+ (isCustomerSelfServeChange &&
3444
3610
  changeFlowNeedsServerPrice &&
3445
3611
  (changeQuoteLoading || (!latestChangeQuote && !changeQuoteFetchError)));
3446
3612
 
3447
- const providerTotalsPreview = null;
3448
- const hasEffectiveChangeSelection = hasChangeSelection;
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
- true && latestChangeQuote?.serverDisplay
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 (true && latestChangeQuote != null && !changeQuoteFetchError) {
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
- (true && isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
3518
- (true ? changeQuoteFetchError ?? '' : '');
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 (false) {
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
- true,
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
- false,
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 (false) return rows.some((a) => (a.vacancies ?? 0) > 0);
4201
+ if (isAdmin) return rows.some((a) => (a.vacancies ?? 0) > 0);
3967
4202
  if (
3968
- true &&
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 ?? (false && dates[0] ? dates[0] : undefined);
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
- false,
4001
- true,
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 = false ? Number.MAX_SAFE_INTEGER : effectivePartySizeCap;
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 && !false && orderSummary.totalQuantity >= maxAvailable) {
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
- // Validate email (required)
4181
- if (!email) {
4182
- setError(t('booking.enterEmail') || 'Please enter your email address');
4183
- return;
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
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
4187
- setError(t('booking.invalidEmail') || 'Please enter a valid email address');
4188
- return;
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
- // Validate first name (required)
4192
- if (!firstName?.trim()) {
4193
- setError(t('booking.enterFirstName') || 'Please enter your first name');
4194
- return;
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
- // Validate last name (required for manage booking lookup)
4198
- if (!lastName?.trim()) {
4199
- setError(t('booking.enterLastName') || 'Please enter your last name');
4200
- return;
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: false,
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 = changeFlowBalanceVsOriginal({
4365
- newTotal: changeFlowNewBookingTotal,
4366
- originalReceiptTotal: originalReceipt?.total ?? 0,
4367
- audience: 'customer',
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 = await createChangeBookingPaymentIntent(
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
- true && changeBookingReferenceForPaidFlow
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
- true && originalReceipt
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
- true && !false && changeFlowInitialTicketCount > 0
5096
+ isCustomerSelfServeChange && !isAdmin && changeFlowInitialTicketCount > 0
4659
5097
  ? changeFlowInitialTicketCount
4660
5098
  : undefined
4661
5099
  }
4662
5100
  getEffectiveVacancies={
4663
- true && !false && changeFlowInitialTicketCount > 0
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={false}
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={false}
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={false}
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={false}
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={undefined}
4840
- extraBeforeSubtotal={undefined}
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={false}
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={false}
5403
+ isAdmin={isAdmin}
4882
5404
  showCommunicationAdminSection={false}
4883
- skipConfirmationCommunications={false}
4884
- disableAutoCommunications={false}
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 || !hasEffectiveChangeSelection || isChangeQuoteBlocked
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={undefined}
4902
- onLineAmountInputBlur={undefined}
4903
- onLineAmountReset={undefined}
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
  )}