@ticketboothapp/booking 1.2.166 → 1.2.168

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.
Files changed (65) hide show
  1. package/package.json +1 -1
  2. package/src/components/booking/AdminChangeBookingContent.tsx +15 -1
  3. package/src/components/booking/AdminChangeBookingFlow.tsx +274 -94
  4. package/src/components/booking/AdminChangeCheckoutPanel.tsx +49 -13
  5. package/src/components/booking/AdminChangePricingPanel.tsx +400 -134
  6. package/src/components/booking/AdminChangePromoEditor.tsx +97 -0
  7. package/src/components/booking/AdminChangeReceiptComparison.tsx +172 -147
  8. package/src/components/booking/ChangeBookingFlow.tsx +6 -3
  9. package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +7 -2
  10. package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +6 -0
  11. package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +80 -0
  12. package/src/components/booking/NewBookingFlow.tsx +14 -2
  13. package/src/components/booking/PickupLocationSelector.module.css +22 -0
  14. package/src/components/booking/PickupLocationSelector.tsx +3 -3
  15. package/src/components/booking/PriceBreakdown.tsx +66 -15
  16. package/src/components/booking/PriceSummary.tsx +16 -0
  17. package/src/components/booking/PrivateShuttleBookingFlow.tsx +52 -14
  18. package/src/components/booking/PrivateShuttleCheckoutSection.tsx +95 -54
  19. package/src/components/booking/admin-adjustment-components.ts +44 -0
  20. package/src/components/booking/admin-adjustment-tax-behavior.ts +50 -0
  21. package/src/components/booking/admin-change-flow-state-helpers.ts +10 -0
  22. package/src/components/booking/admin-change-provider-payload.ts +12 -1
  23. package/src/components/booking/admin-change-quote-request-key.ts +41 -1
  24. package/src/components/booking/admin-change-receipt-lines.ts +49 -0
  25. package/src/components/booking/admin-change-v2-quote-request.ts +109 -0
  26. package/src/components/booking/admin-refund-decision-context.ts +122 -0
  27. package/src/components/booking/admin-refund-disposition.ts +71 -2
  28. package/src/components/booking/availability-date-selection.ts +16 -0
  29. package/src/components/booking/booking-flow-types.ts +4 -0
  30. package/src/components/booking/booking-flow.css +5 -0
  31. package/src/components/booking/change-booking-error-message.ts +137 -0
  32. package/src/components/booking/change-booking-flow-helpers.ts +77 -2
  33. package/src/components/booking/change-booking-quote-guards.ts +4 -1
  34. package/src/components/booking/change-booking-quote-state.ts +44 -1
  35. package/src/components/booking/incompatible-add-on-selections.ts +19 -0
  36. package/src/components/booking/private-shuttle-availability.ts +14 -0
  37. package/src/components/booking/private-shuttle-cancellation-policy.ts +9 -0
  38. package/src/components/booking/private-shuttle-checkout-controller.ts +5 -0
  39. package/src/components/booking/private-shuttle-provider-change-runner.ts +21 -0
  40. package/src/components/booking/private-shuttle-reservation-runner.ts +5 -3
  41. package/src/components/booking/provider-dashboard-change-booking.ts +18 -2
  42. package/src/components/booking/use-standard-booking-availability.ts +5 -2
  43. package/src/components/booking/useAdminChangeCheckoutController.ts +56 -1
  44. package/src/components/booking/useAdminChangeProductReset.ts +6 -0
  45. package/src/components/booking/useAdminChangeProtectedPricing.ts +4 -0
  46. package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +8 -6
  47. package/src/components/booking/useAdminChangeQuotePreview.ts +146 -61
  48. package/src/components/booking/useAdminCustomReceiptLines.ts +171 -10
  49. package/src/components/booking/useAdminProviderPricingAdjustments.ts +50 -19
  50. package/src/components/booking/useBookingAvailabilityAddOns.ts +26 -4
  51. package/src/components/booking/useBookingPromoAndQuantityController.ts +16 -2
  52. package/src/components/booking/useChangeBookingAddOnFloorController.ts +15 -9
  53. package/src/components/booking/useChangeBookingAutoSelections.ts +60 -17
  54. package/src/components/booking/useChangeBookingInitialHydration.ts +10 -7
  55. package/src/components/booking/useChangeBookingQuotePreview.ts +3 -1
  56. package/src/components/booking/useChangeBookingSelectionDetails.ts +23 -17
  57. package/src/components/booking/useStandardBookingAutoSelections.ts +55 -7
  58. package/src/lib/booking/booking-cutoffs.ts +22 -0
  59. package/src/lib/booking/change-booking-server-preview.ts +92 -5
  60. package/src/lib/booking/change-flow-pricing.ts +12 -0
  61. package/src/lib/booking/i18n/messages/en.json +1 -0
  62. package/src/lib/booking/i18n/messages/fr.json +1 -0
  63. package/src/lib/booking-api.ts +76 -1
  64. package/test/change-booking-helpers.test.ts +1319 -7
  65. package/src/components/booking/useAdminChangePricingDebugPanel.tsx +0 -178
@@ -3,8 +3,11 @@
3
3
  import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
4
4
  import {
5
5
  type AdminAmendmentRefundDisposition,
6
+ type AdminPromoApplicationScope,
7
+ type AddOn,
6
8
  type Availability,
7
9
  type ReturnOption,
10
+ getAddOns,
8
11
  } from '../../lib/booking-api';
9
12
  import {
10
13
  AdminChangeBookingContent,
@@ -15,15 +18,18 @@ import { CURRENCIES, DEFAULT_CURRENCY, type Currency } from './CurrencySwitcher'
15
18
  import { useCompanyTimezone } from '../../contexts/CompanyContext';
16
19
  import { useBookingApp } from '../../contexts/BookingAppContext';
17
20
  import { useBookingHost } from '../../runtime';
21
+ import { filterAvailabilitiesAfterCurrentTime } from '../../lib/booking/booking-cutoffs';
18
22
  import type { ChangeBookingFlowProps } from './booking-flow-types';
19
23
  import {
20
24
  normalizeAddOnSelections,
25
+ resolveChangeBookingCancellationPolicyId,
21
26
  } from './change-booking-flow-helpers';
22
27
  import {
23
28
  buildChangeBookingMinimumQuantities,
24
29
  isAdminManualOverrideEligibleLine,
25
30
  resolveChangeFlowOriginalDate,
26
31
  resolveInitialChangeProductOptionId,
32
+ shouldLockExistingAddOnQuantities,
27
33
  } from './admin-change-flow-state-helpers';
28
34
  import { useAdminChangeProducts } from './useAdminChangeProducts';
29
35
  import {
@@ -40,6 +46,8 @@ import { useAdminChangePriceSummary } from './useAdminChangePriceSummary';
40
46
  import { useAdminChangeReceiptDerivation } from './useAdminChangeReceiptDerivation';
41
47
  import { useAdminChangeQuotePreview } from './useAdminChangeQuotePreview';
42
48
  import { useBookingAvailabilityAddOns } from './useBookingAvailabilityAddOns';
49
+ import { incompatibleAddOnSelections } from './incompatible-add-on-selections';
50
+ import { incompatibleAddOnChangeError } from './change-booking-error-message';
43
51
  import { useBookingPromoAndQuantityController } from './useBookingPromoAndQuantityController';
44
52
  import { useChangeBookingItineraryUiState } from './useChangeBookingItineraryUiState';
45
53
  import { useChangeBookingAutoSelections } from './useChangeBookingAutoSelections';
@@ -48,22 +56,22 @@ import { useChangeBookingAddOnFloorController } from './useChangeBookingAddOnFlo
48
56
  import { useChangeBookingReceiptFloorController } from './useChangeBookingReceiptFloorController';
49
57
  import { useAdminCustomReceiptLines } from './useAdminCustomReceiptLines';
50
58
  import { useAdminChangeQuoteDisplayState } from './useAdminChangeQuoteDisplayState';
51
- import { useAdminChangePricingDebugPanel } from './useAdminChangePricingDebugPanel';
52
59
  import { useAdminChangeOrderSummaryModel } from './useAdminChangeOrderSummaryModel';
53
60
  import { useActiveProductOptions } from './useActiveProductOptions';
54
61
  import { useSeedPromoCodeInput } from './useSeedPromoCodeInput';
55
62
  import { useAdminChangeProductReset } from './useAdminChangeProductReset';
56
63
  import { useBookingQuantityCapTrim } from './useBookingQuantityCapTrim';
57
64
  import { useBookingViewItemAnalytics } from './useBookingViewItemAnalytics';
65
+ import { AdminChangePromoEditor } from './AdminChangePromoEditor';
58
66
  import { buildAdminChangeQuoteRequestKey } from './admin-change-quote-request-key';
59
67
  import type { AdminReleaseRefundDisposition } from './admin-refund-disposition';
60
-
61
- type AdminRefundDecisionContext = {
62
- selectionKey: string;
63
- operations: NonNullable<AdminChangeLatestQuote['refundDecisionOperations']>;
64
- allowedByOperationId: NonNullable<AdminChangeLatestQuote['allowedRefundDispositionsByOperationId']>;
65
- removedValueByOperationId: NonNullable<AdminChangeLatestQuote['removedValueByOperationId']>;
66
- };
68
+ import {
69
+ defaultKeepOriginalReturnPriceDispositions,
70
+ defaultPendingRefundDispositions,
71
+ mergeAdminRefundDecisionContext,
72
+ selectRefundDispositionsForOperations,
73
+ type AdminRefundDecisionContext,
74
+ } from './admin-refund-decision-context';
67
75
 
68
76
  /**
69
77
  * ## Pricing contract (customer self-serve)
@@ -148,6 +156,24 @@ export function AdminChangeBookingFlow({
148
156
  isAdmin,
149
157
  companyId: env.COMPANY_ID,
150
158
  });
159
+ const [adminAvailabilityNowMs, setAdminAvailabilityNowMs] = useState(() => Date.now());
160
+
161
+ useEffect(() => {
162
+ const interval = window.setInterval(() => setAdminAvailabilityNowMs(Date.now()), 60 * 1000);
163
+ return () => window.clearInterval(interval);
164
+ }, []);
165
+
166
+ const adminAvailabilityNow = useMemo(
167
+ () => new Date(adminAvailabilityNowMs),
168
+ [adminAvailabilityNowMs],
169
+ );
170
+ const filterAdminCalendarAvailabilities = useCallback(
171
+ (rows: Availability[]) =>
172
+ product.productType === 'PRIVATE_SHUTTLE'
173
+ ? rows
174
+ : filterAvailabilitiesAfterCurrentTime(rows, adminAvailabilityNow),
175
+ [adminAvailabilityNow, product.productType],
176
+ );
151
177
  const [selectedAvailability, setSelectedAvailability] = useState<Availability | null>(null);
152
178
  const [selectedReturnOption, setSelectedReturnOption] = useState<ReturnOption | null>(null);
153
179
  const [quantities, setQuantities] = useState<Record<string, number>>({});
@@ -156,29 +182,83 @@ export function AdminChangeBookingFlow({
156
182
  const [lastName, setLastName] = useState('');
157
183
  const [promoCodeInput, setPromoCodeInput] = useState('');
158
184
  const [appliedPromoCode, setAppliedPromoCode] = useState<string | null>(null);
185
+ const [promoApplicationScope, setPromoApplicationScope] =
186
+ useState<AdminPromoApplicationScope>('CHANGE_ONLY');
159
187
  const [pickupLocationId, setPickupLocationId] = useState<string | null>(null);
160
188
  const [pickupLocationSkipped, setPickupLocationSkipped] = useState(false);
161
- /** Cancellation policy is fixed to the existing booking snapshot not user-editable in change flow. */
162
- const cancellationPolicyId = useMemo(
189
+ /** The policy remains non-editable, but a family conversion must adopt the target default. */
190
+ const [cancellationPolicyId, setCancellationPolicyId] = useState<string | null>(
163
191
  () => initialValues?.cancellationPolicyId?.trim() ?? null,
164
- [initialValues?.cancellationPolicyId],
165
192
  );
166
193
  /** Add-on selections (lunch, animals, etc.) - filtered by selected product option */
167
194
  const [addOnSelections, setAddOnSelections] = useState<Array<{ addOnId: string; variantId?: string; quantity?: number }>>(() =>
168
195
  normalizeAddOnSelections(initialValues?.addOnSelections ?? [])
169
196
  );
170
197
  /** Fetched add-ons for the selected product option */
171
- const { addOns, setAddOns } = useBookingAvailabilityAddOns({
198
+ const {
199
+ addOns,
200
+ setAddOns,
201
+ availabilityProductOptionId,
202
+ loadedProductOptionId,
203
+ } = useBookingAvailabilityAddOns({
172
204
  selectedAvailability,
173
205
  companyId: product.companyId,
174
206
  });
175
- const {
176
- adminCustomReceiptLines,
177
- handleAddAdminCustomReceiptLine,
178
- handleUpdateAdminCustomReceiptLine,
179
- handleRemoveAdminCustomReceiptLine,
180
- } = useAdminCustomReceiptLines(selectedChangeProductId);
181
-
207
+ const [knownAddOns, setKnownAddOns] = useState<AddOn[]>([]);
208
+ const mergeKnownAddOns = useCallback((rows: AddOn[]) => {
209
+ if (rows.length === 0) return;
210
+ setKnownAddOns((current) => {
211
+ const merged = new Map(current.map((addOn) => [addOn.addOnId, addOn]));
212
+ let changed = false;
213
+ for (const addOn of rows) {
214
+ if (merged.get(addOn.addOnId) !== addOn) {
215
+ merged.set(addOn.addOnId, addOn);
216
+ changed = true;
217
+ }
218
+ }
219
+ return changed ? Array.from(merged.values()) : current;
220
+ });
221
+ }, []);
222
+ useEffect(() => {
223
+ mergeKnownAddOns(addOns);
224
+ }, [addOns, mergeKnownAddOns]);
225
+ useEffect(() => {
226
+ let cancelled = false;
227
+ if (!product.companyId) return;
228
+ getAddOns(product.companyId, {
229
+ preCheckout: true,
230
+ dateTime: initialValues?.dateTime?.trim() || undefined,
231
+ })
232
+ .then((rows) => {
233
+ if (!cancelled) mergeKnownAddOns(rows);
234
+ })
235
+ .catch(() => {
236
+ // The selected tour's catalog still renders; the warning keeps a generic fallback.
237
+ });
238
+ return () => {
239
+ cancelled = true;
240
+ };
241
+ }, [product.companyId, initialValues?.dateTime, mergeKnownAddOns]);
242
+ const addOnCatalogLoaded =
243
+ availabilityProductOptionId != null &&
244
+ loadedProductOptionId === availabilityProductOptionId;
245
+ const incompatibleSelections = incompatibleAddOnSelections(
246
+ addOnSelections,
247
+ addOns,
248
+ addOnCatalogLoaded,
249
+ );
250
+ const incompatibleAddOnIds = Array.from(
251
+ new Set(incompatibleSelections.map((selection) => selection.addOnId)),
252
+ );
253
+ const hasIncompatibleAddOnSelections = incompatibleAddOnIds.length > 0;
254
+ const incompatibleAddOnError = hasIncompatibleAddOnSelections
255
+ ? incompatibleAddOnChangeError(
256
+ incompatibleAddOnIds.map((addOnId) => (
257
+ knownAddOns.find((addOn) => addOn.addOnId === addOnId)?.name ?? 'Selected add-on'
258
+ )),
259
+ product.name,
260
+ )
261
+ : '';
182
262
  useEffect(() => {
183
263
  if (!pickupLocationId) return;
184
264
  const pickupLocations = product.pickupLocations ?? [];
@@ -191,7 +271,7 @@ export function AdminChangeBookingFlow({
191
271
  useSeedPromoCodeInput(autoAppliedPromoCode, setPromoCodeInput);
192
272
  const [loading, setLoading] = useState(false);
193
273
  const [error, setError] = useState('');
194
- const [termsAccepted, setTermsAccepted] = useState(false);
274
+ const [termsAccepted, setTermsAccepted] = useState(true);
195
275
  const [termsAcceptedAt, setTermsAcceptedAt] = useState<string | null>(null);
196
276
  const [partnerAttributionConfirmed, setPartnerAttributionConfirmed] = useState(false);
197
277
  /** Admin only: skip sending confirmation at creation (provider dashboard). */
@@ -224,10 +304,11 @@ export function AdminChangeBookingFlow({
224
304
  * user picks a different return time — baseline is the first auto-selected return for this outbound.
225
305
  */
226
306
  const [implicitReturnBaselineId, setImplicitReturnBaselineId] = useState<string | null>(null);
227
- /** Any change flow (self-serve or provider): promo from booking is fixed — show read-only, never add new. */
228
- const lockedPromoCode = initialValues?.promoCode?.trim()
307
+ const historicalPromoCode = initialValues?.promoCode?.trim()
229
308
  ? initialValues.promoCode.trim().toUpperCase()
230
309
  : null;
310
+ /** Public/self-serve keeps its historical promo locked. Admin amendments require new explicit intent. */
311
+ const lockedPromoCode = isProviderDashboardChange ? null : historicalPromoCode;
231
312
  /** Public self-serve only: cannot reduce tickets below original counts. Provider-dashboard admins may reduce party size. */
232
313
  const changeBookingMinimumQuantities = useMemo(
233
314
  () =>
@@ -239,6 +320,21 @@ export function AdminChangeBookingFlow({
239
320
  [isCustomerSelfServeChange, initialValues?.bookingItems, isAdmin],
240
321
  );
241
322
  const [latestChangeQuote, setLatestChangeQuote] = useState<AdminChangeLatestQuote | null>(null);
323
+ const {
324
+ adminCustomReceiptLines,
325
+ appliedAdminCustomReceiptLines,
326
+ adminReverseAdjustmentIds,
327
+ hasPendingAdminCustomReceiptLineChanges,
328
+ pendingAdminCustomReceiptLineIds,
329
+ hasHydratedExistingAdjustments,
330
+ handleAddAdminCustomReceiptLine,
331
+ handleUpdateAdminCustomReceiptLine,
332
+ handleRemoveAdminCustomReceiptLine,
333
+ handleApplyAdminCustomReceiptLines: handleApplyAdminCustomReceiptLineDrafts,
334
+ } = useAdminCustomReceiptLines(
335
+ selectedChangeProductId,
336
+ latestChangeQuote?.reversibleAdjustments,
337
+ );
242
338
  const [refundDecisionContext, setRefundDecisionContext] = useState<AdminRefundDecisionContext | null>(null);
243
339
  const [refundDispositionsByOperationId, setRefundDispositionsByOperationId] = useState<
244
340
  Record<string, AdminAmendmentRefundDisposition>
@@ -256,6 +352,12 @@ export function AdminChangeBookingFlow({
256
352
  (latestChangeQuote.refundDecisionOperations?.length ?? 0) > 0) &&
257
353
  latestChangeQuote.serverDisplay != null;
258
354
  const changeQuoteRequestSeq = useRef(0);
355
+ const handleApplyAdminCustomReceiptLines = useCallback(() => {
356
+ setLatestChangeQuote(null);
357
+ setChangeQuoteFetchError(null);
358
+ setChangeQuoteLoading(true);
359
+ handleApplyAdminCustomReceiptLineDrafts();
360
+ }, [handleApplyAdminCustomReceiptLineDrafts]);
259
361
  const { activeOptions, activeOptionIdsKey, optionsMap } = useActiveProductOptions(product.options);
260
362
 
261
363
  const {
@@ -283,13 +385,37 @@ export function AdminChangeBookingFlow({
283
385
  appliedPromoCode,
284
386
  pricingProfileIdForAvailabilities,
285
387
  cancellationPolicyProfileIdForAvailabilities,
388
+ filterCalendarAvailabilities: filterAdminCalendarAvailabilities,
286
389
  resetKey: selectedChangeProductId,
287
390
  setQuantities,
288
391
  setError,
289
392
  });
290
393
 
394
+ useEffect(() => {
395
+ const policies = pricingConfig?.cancellationPolicies;
396
+ if (!policies?.length) return;
397
+ const isProductFamilyConversion =
398
+ isInitialPrivateShuttleBooking !== (product.productType === 'PRIVATE_SHUTTLE');
399
+ const resolvedPolicyId = resolveChangeBookingCancellationPolicyId(
400
+ cancellationPolicyId,
401
+ policies,
402
+ currency,
403
+ isProductFamilyConversion,
404
+ );
405
+ if (resolvedPolicyId !== cancellationPolicyId) {
406
+ setCancellationPolicyId(resolvedPolicyId);
407
+ }
408
+ }, [
409
+ cancellationPolicyId,
410
+ currency,
411
+ isInitialPrivateShuttleBooking,
412
+ pricingConfig?.cancellationPolicies,
413
+ product.productType,
414
+ ]);
415
+
291
416
  useAdminChangeProductReset({
292
417
  selectedChangeProductId,
418
+ setQuantities,
293
419
  setSelectedAvailability,
294
420
  setSelectedReturnOption,
295
421
  setAddOns,
@@ -353,6 +479,8 @@ export function AdminChangeBookingFlow({
353
479
  timesForSelectedDate,
354
480
  companyTimezone,
355
481
  changeFlowResolvedInitialProductOptionId,
482
+ mostPopularProductOptionId:
483
+ activeOptions.find((option) => option.mostPopular)?.optionId ?? null,
356
484
  precomputedPricesByOption,
357
485
  currency,
358
486
  originalReceiptSubtotal: originalReceipt?.subtotal,
@@ -373,7 +501,12 @@ export function AdminChangeBookingFlow({
373
501
  setError,
374
502
  });
375
503
 
504
+ const lockExistingAddOnQuantities = shouldLockExistingAddOnQuantities({
505
+ isCustomerSelfServeChange,
506
+ isAdmin,
507
+ });
376
508
  const {
509
+ initialAddOnQtyByKey,
377
510
  initialAddOnMinQtyByKey,
378
511
  initialAddOnMinTotalByAddOnId,
379
512
  updateAddOnSelections,
@@ -381,7 +514,7 @@ export function AdminChangeBookingFlow({
381
514
  initialValues,
382
515
  originalReceiptLineItems: originalReceipt?.lineItems,
383
516
  addOns,
384
- isCustomerSelfServeChange,
517
+ lockExistingAddOnQuantities,
385
518
  setAddOnSelections,
386
519
  });
387
520
 
@@ -507,7 +640,6 @@ export function AdminChangeBookingFlow({
507
640
  editableSummaryLineAmountInputs,
508
641
  editableSummaryLineLabelInputs,
509
642
  editableSummaryPreSubtotalDelta,
510
- editableSummaryPreSubtotalDebugRows,
511
643
  handleEditableLineAmountInputChange,
512
644
  handleEditableLineAmountInputBlur,
513
645
  handleEditableLineAmountReset,
@@ -535,7 +667,7 @@ export function AdminChangeBookingFlow({
535
667
  originalAddOnSelections: initialValues?.addOnSelections ?? null,
536
668
  originalProductOptionId: initialValues?.productOptionId ?? null,
537
669
  addOns,
538
- adminCustomReceiptLines,
670
+ adminCustomReceiptLines: appliedAdminCustomReceiptLines,
539
671
  quantities,
540
672
  flowUi,
541
673
  isProviderDashboardChange,
@@ -573,8 +705,6 @@ export function AdminChangeBookingFlow({
573
705
  unchangedReceiptAnchor,
574
706
  showChangeFlowManualPriceLines,
575
707
  showAdminCustomLineEditor,
576
- displayedChangeAmountsRaw,
577
- displayLayerUsesExternalPricing,
578
708
  displayChangeFlowTax,
579
709
  displayChangeFlowProposedTotal,
580
710
  displayChangeFlowProposedTotalWithEditableLines,
@@ -604,7 +734,7 @@ export function AdminChangeBookingFlow({
604
734
  pickupLocationId,
605
735
  quantities,
606
736
  addOnSelections,
607
- initialAddOnMinQtyByKey,
737
+ initialAddOnQtyByKey,
608
738
  changeFlowResolvedInitialProductOptionId,
609
739
  selectedParentProductChanged,
610
740
  isProviderDashboardChange,
@@ -613,7 +743,8 @@ export function AdminChangeBookingFlow({
613
743
  providerQuotedLines,
614
744
  editableSummaryPreSubtotalDelta,
615
745
  adminCustomAdjustmentTotal,
616
- adminCustomReceiptLines,
746
+ adminCustomReceiptLines: appliedAdminCustomReceiptLines,
747
+ adminReverseAdjustmentIds,
617
748
  isManualOverrideEligibleLine,
618
749
  isAdmin,
619
750
  useAdminFeAuthoritativeQuote,
@@ -651,37 +782,6 @@ export function AdminChangeBookingFlow({
651
782
  editableSummaryPreSubtotalDelta,
652
783
  });
653
784
 
654
- const changeFlowAdminPricingDebugPanel = useAdminChangePricingDebugPanel({
655
- isAdmin,
656
- hasSelectedAvailability: selectedAvailability != null,
657
- totalQuantity,
658
- useAdminFeAuthoritativeQuote,
659
- currency,
660
- locale,
661
- originalReceipt,
662
- isCustomerSelfServeChange,
663
- latestChangeQuote,
664
- changeQuoteFetchError,
665
- displayLayerUsesExternalPricing,
666
- selfServePricingConfirmed,
667
- changeQuoteLoading,
668
- effectiveSubtotal,
669
- adminCustomAdjustmentTotal,
670
- editableSummaryPreSubtotalDebugRows,
671
- effectiveSubtotalForCheckout,
672
- effectiveTax,
673
- effectivePromoDiscountAmount,
674
- totalPrice,
675
- changeFlowNewBookingTotal,
676
- displayedChangeAmountsRaw,
677
- editableSummaryPreSubtotalDelta,
678
- displayChangeFlowProposedTotal,
679
- displayChangeFlowProposedTotalWithEditableLines,
680
- adminFeAuthoritativeReceipt,
681
- changeFlowClientEstimateDue,
682
- changeFlowAmountDue,
683
- });
684
-
685
785
  const refundDecisionSelectionKey = useMemo(() => buildAdminChangeQuoteRequestKey({
686
786
  bookingReference: initialValues?.bookingReference,
687
787
  lastName,
@@ -692,8 +792,11 @@ export function AdminChangeBookingFlow({
692
792
  quantities,
693
793
  addOnSelections,
694
794
  cancellationPolicyId,
695
- adminCustomReceiptLines,
795
+ promoCode: appliedPromoCode,
796
+ promoApplicationScope,
797
+ adminCustomReceiptLines: appliedAdminCustomReceiptLines,
696
798
  structuredAdjustments: adminStructuredAdjustments,
799
+ reverseAdjustmentIds: adminReverseAdjustmentIds,
697
800
  useAdminFeAuthoritativeQuote,
698
801
  }), [
699
802
  initialValues?.bookingReference,
@@ -705,8 +808,11 @@ export function AdminChangeBookingFlow({
705
808
  quantities,
706
809
  addOnSelections,
707
810
  cancellationPolicyId,
708
- adminCustomReceiptLines,
811
+ appliedPromoCode,
812
+ promoApplicationScope,
813
+ appliedAdminCustomReceiptLines,
709
814
  adminStructuredAdjustments,
815
+ adminReverseAdjustmentIds,
710
816
  useAdminFeAuthoritativeQuote,
711
817
  ]);
712
818
  const responseRefundDecisionOperations = latestChangeQuote?.refundDecisionOperations ?? [];
@@ -714,41 +820,51 @@ export function AdminChangeBookingFlow({
714
820
  latestChangeQuote?.returnPriceTreatmentOperations ?? [];
715
821
  useEffect(() => {
716
822
  if (responseRefundDecisionOperations.length === 0) return;
717
- setRefundDecisionContext({
823
+ setRefundDecisionContext((current) => mergeAdminRefundDecisionContext({
824
+ current,
718
825
  selectionKey: refundDecisionSelectionKey,
719
826
  operations: responseRefundDecisionOperations,
720
827
  allowedByOperationId: latestChangeQuote?.allowedRefundDispositionsByOperationId ?? {},
721
828
  removedValueByOperationId: latestChangeQuote?.removedValueByOperationId ?? {},
722
- });
829
+ }));
723
830
  }, [
724
831
  refundDecisionSelectionKey,
725
832
  responseRefundDecisionOperations,
726
833
  latestChangeQuote?.allowedRefundDispositionsByOperationId,
727
834
  latestChangeQuote?.removedValueByOperationId,
728
835
  ]);
729
- const activeRefundDecisionContext = responseRefundDecisionOperations.length > 0
730
- ? {
731
- selectionKey: refundDecisionSelectionKey,
732
- operations: responseRefundDecisionOperations,
733
- allowedByOperationId: latestChangeQuote?.allowedRefundDispositionsByOperationId ?? {},
734
- removedValueByOperationId: latestChangeQuote?.removedValueByOperationId ?? {},
735
- }
736
- : refundDecisionContext?.selectionKey === refundDecisionSelectionKey
737
- ? refundDecisionContext
738
- : null;
836
+ useEffect(() => {
837
+ if (!isProviderDashboardChange || responseRefundDecisionOperations.length === 0) return;
838
+ setRefundDispositionsByOperationId((current) =>
839
+ defaultPendingRefundDispositions(responseRefundDecisionOperations, current),
840
+ );
841
+ }, [isProviderDashboardChange, responseRefundDecisionOperations]);
842
+ const activeRefundDecisionContext = useMemo(() => mergeAdminRefundDecisionContext({
843
+ current: refundDecisionContext,
844
+ selectionKey: refundDecisionSelectionKey,
845
+ operations: responseRefundDecisionOperations,
846
+ allowedByOperationId: latestChangeQuote?.allowedRefundDispositionsByOperationId ?? {},
847
+ removedValueByOperationId: latestChangeQuote?.removedValueByOperationId ?? {},
848
+ }), [
849
+ refundDecisionContext,
850
+ refundDecisionSelectionKey,
851
+ responseRefundDecisionOperations,
852
+ latestChangeQuote?.allowedRefundDispositionsByOperationId,
853
+ latestChangeQuote?.removedValueByOperationId,
854
+ ]);
739
855
  const activeReturnPriceTreatmentOperations =
740
856
  responseReturnPriceTreatmentOperations.length > 0
741
857
  ? responseReturnPriceTreatmentOperations
742
858
  : [];
743
859
  const activeRefundDispositionsByOperationId = useMemo(() => {
744
- const relevantOperationIds = new Set([
745
- ...(activeRefundDecisionContext?.operations ?? []).map((operation) => operation.operationId),
746
- ...activeReturnPriceTreatmentOperations.map((operation) => operation.operationId),
747
- ]);
748
- return Object.fromEntries(
749
- Object.entries(refundDispositionsByOperationId).filter(([operationId]) =>
750
- relevantOperationIds.has(operationId),
751
- ),
860
+ const dispositionsWithReturnPriceDefaults = defaultKeepOriginalReturnPriceDispositions(
861
+ activeReturnPriceTreatmentOperations,
862
+ refundDispositionsByOperationId,
863
+ );
864
+ return selectRefundDispositionsForOperations(
865
+ activeRefundDecisionContext,
866
+ activeReturnPriceTreatmentOperations,
867
+ dispositionsWithReturnPriceDefaults,
752
868
  );
753
869
  }, [
754
870
  activeRefundDecisionContext?.operations,
@@ -787,8 +903,13 @@ export function AdminChangeBookingFlow({
787
903
  quantities,
788
904
  addOnSelections,
789
905
  cancellationPolicyId,
790
- adminCustomReceiptLines,
906
+ appliedPromoCode,
907
+ promoApplicationScope,
908
+ adminCustomReceiptLines: appliedAdminCustomReceiptLines,
791
909
  adminStructuredAdjustments,
910
+ adminReverseAdjustmentIds,
911
+ needsAdjustmentDiscovery:
912
+ isProviderDashboardChange && !hasHydratedExistingAdjustments,
792
913
  refundDispositionsByOperationId: activeRefundDispositionsByOperationId,
793
914
  useAdminFeAuthoritativeQuote,
794
915
  latestChangeQuote,
@@ -899,7 +1020,13 @@ export function AdminChangeBookingFlow({
899
1020
  setPickupLocationSkipped,
900
1021
  });
901
1022
 
902
- const { handleQuantityChange } = useBookingPromoAndQuantityController({
1023
+ const {
1024
+ handleQuantityChange,
1025
+ handleApplyPromo,
1026
+ handleRemovePromo,
1027
+ promoCodeError,
1028
+ promoCodeValidating,
1029
+ } = useBookingPromoAndQuantityController({
903
1030
  isAdmin,
904
1031
  effectivePartySizeCap,
905
1032
  currentTotalQuantity: orderSummary.totalQuantity,
@@ -918,6 +1045,33 @@ export function AdminChangeBookingFlow({
918
1045
  setError,
919
1046
  });
920
1047
 
1048
+ const adminPromoEditor = (
1049
+ <AdminChangePromoEditor
1050
+ previousPromoCode={historicalPromoCode}
1051
+ promoCodeInput={promoCodeInput}
1052
+ appliedPromoCode={appliedPromoCode}
1053
+ promoCodeError={promoCodeError}
1054
+ promoCodeValidating={promoCodeValidating}
1055
+ scope={promoApplicationScope}
1056
+ currency={currency}
1057
+ locale={locale}
1058
+ t={t}
1059
+ onInputChange={(value) => {
1060
+ setPromoCodeInput(value);
1061
+ if (appliedPromoCode && value.trim().toUpperCase() !== appliedPromoCode) {
1062
+ setAppliedPromoCode(null);
1063
+ }
1064
+ }}
1065
+ onApply={() => { void handleApplyPromo(); }}
1066
+ onRemove={() => {
1067
+ setPromoCodeInput('');
1068
+ setPromoApplicationScope('CHANGE_ONLY');
1069
+ handleRemovePromo();
1070
+ }}
1071
+ onScopeChange={setPromoApplicationScope}
1072
+ />
1073
+ );
1074
+
921
1075
  const {
922
1076
  showCheckoutModal,
923
1077
  checkoutClientSecret,
@@ -969,10 +1123,12 @@ export function AdminChangeBookingFlow({
969
1123
  onChangeBooking,
970
1124
  cancellationPolicyId,
971
1125
  appliedPromoCode,
1126
+ promoApplicationScope,
972
1127
  displayChangeFlowProposedTotalWithEditableLines,
973
1128
  providerPricingOverrides,
974
1129
  mergedProviderAdditionalAdjustments,
975
1130
  adminStructuredAdjustments,
1131
+ adminReverseAdjustmentIds,
976
1132
  refundDispositionsByOperationId: activeRefundDispositionsByOperationId,
977
1133
  providerApplyAuthoritativeReceipt,
978
1134
  onSuccess,
@@ -987,7 +1143,7 @@ export function AdminChangeBookingFlow({
987
1143
  effectiveCancellationPolicyLabel,
988
1144
  addOns,
989
1145
  feeLineItems,
990
- adminCustomReceiptLines,
1146
+ adminCustomReceiptLines: appliedAdminCustomReceiptLines,
991
1147
  isTaxIncludedInPrice,
992
1148
  originalReceiptPromoAdjustment,
993
1149
  effectivePromoLineType,
@@ -1063,9 +1219,9 @@ export function AdminChangeBookingFlow({
1063
1219
  isSimplifiedPricingView, onQuantityChange: handleQuantityChange,
1064
1220
  minimumQuantities: changeBookingMinimumQuantities, applyReceiptPaidFloors: changeFlowApplyReceiptPaidFloors,
1065
1221
  ticketUnitFloorByCategory: changeFlowTicketBookedUnitPriceByCategory,
1066
- ...{ addOns, addOnSelections },
1222
+ ...{ addOns, knownAddOns, addOnCatalogLoaded, addOnSelections },
1067
1223
  onAddOnSelectionsChange: updateAddOnSelections,
1068
- minimumTotalByAddOnId: isCustomerSelfServeChange ? initialAddOnMinTotalByAddOnId : undefined,
1224
+ minimumTotalByAddOnId: lockExistingAddOnQuantities ? initialAddOnMinTotalByAddOnId : undefined,
1069
1225
  priceSummaryLines: taxIncludedReconciledCheckoutPriceSummaryLines,
1070
1226
  replacePriceSummary: hasChangeSelection ? selfServeCheckoutPlaceholder : undefined,
1071
1227
  changeFlowAmountDue,
@@ -1077,13 +1233,23 @@ export function AdminChangeBookingFlow({
1077
1233
  // normalized server preview, render it; do not discard valid data because an
1078
1234
  // optional/versioned metadata string is missing or renamed.
1079
1235
  serverAmendmentLines: latestChangeQuote?.serverPreview?.priceSummaryLines ?? null,
1236
+ serverResultingReceiptLines:
1237
+ latestChangeQuote?.serverPreview?.resultingReceiptLines ?? null,
1080
1238
  serverAmountDue: latestChangeQuote?.serverPreview?.amountDue ?? null,
1081
1239
  serverUpdatedTotal: latestChangeQuote?.serverPreview?.totalNewBooking ?? null,
1240
+ serverNetPaid: latestChangeQuote?.amountPreviouslyPaid ?? null,
1241
+ serverCurrentPaymentCredit: latestChangeQuote?.currentPaymentCreditTotal ?? 0,
1242
+ serverProjectedPaymentCredit: latestChangeQuote?.projectedPaymentCreditTotal ?? 0,
1243
+ serverRefundCandidate: latestChangeQuote?.refundCandidate ?? 0,
1082
1244
  serverQuoteError:
1083
- changeQuoteFetchError ??
1084
- (isChangeQuoteBlocked
1085
- ? (latestChangeQuote?.reasonIfBlocked ?? 'This change cannot be priced right now.')
1086
- : null),
1245
+ hasIncompatibleAddOnSelections
1246
+ ? incompatibleAddOnError
1247
+ : (
1248
+ changeQuoteFetchError ??
1249
+ (isChangeQuoteBlocked
1250
+ ? (latestChangeQuote?.reasonIfBlocked ?? 'This change cannot be priced right now.')
1251
+ : null)
1252
+ ),
1087
1253
  refundDecisionOperations: activeRefundDecisionContext?.operations ?? [],
1088
1254
  returnPriceTreatmentOperations: activeReturnPriceTreatmentOperations,
1089
1255
  allowedRefundDispositionsByOperationId: {
@@ -1095,14 +1261,21 @@ export function AdminChangeBookingFlow({
1095
1261
  refundDispositionsByOperationId: activeRefundDispositionsByOperationId,
1096
1262
  onRefundDispositionChange: handleRefundDispositionChange,
1097
1263
  refundQuoteLoading: changeQuoteLoading,
1264
+ sourceProductName: initialProduct.name,
1265
+ targetProductName: product.name,
1098
1266
  ...{ priceSummaryLinesIncludeTaxRow, isTaxIncludedInPrice, showProviderPricingInlineEditor },
1099
1267
  taxRate: pricingConfig?.taxRate,
1100
1268
  ...{ providerPricingUi, providerQuotedLines },
1101
- debugPanel: changeFlowAdminPricingDebugPanel,
1269
+ promoEditor: adminPromoEditor,
1270
+ promoCode: appliedPromoCode,
1102
1271
  ...{ showChangeFlowManualPriceLines, showAdminCustomLineEditor, adminCustomReceiptLines },
1272
+ hasPendingAdminCustomReceiptLineChanges,
1273
+ pendingAdminCustomReceiptLineIds,
1274
+ adjustmentComponentOptions: latestChangeQuote?.selectableAdjustmentComponents ?? [],
1103
1275
  onAddAdminCustomReceiptLine: handleAddAdminCustomReceiptLine,
1104
1276
  onUpdateAdminCustomReceiptLine: handleUpdateAdminCustomReceiptLine,
1105
1277
  onRemoveAdminCustomReceiptLine: handleRemoveAdminCustomReceiptLine,
1278
+ onApplyAdminCustomReceiptLines: handleApplyAdminCustomReceiptLines,
1106
1279
  onFirstNameChange: setFirstName, onLastNameChange: setLastName, onEmailChange: setEmail,
1107
1280
  onClearError: () => setError(''),
1108
1281
  pickupLocations: product.pickupLocations, destinations: product.destinations, highlightedPickupLocationIds,
@@ -1112,11 +1285,18 @@ export function AdminChangeBookingFlow({
1112
1285
  ...{ skipConfirmationCommunications, disableAutoCommunications },
1113
1286
  onSkipConfirmationChange: setSkipConfirmationCommunications,
1114
1287
  onDisableCommunicationsChange: setDisableAutoCommunications,
1115
- checkoutError: checkoutFormError,
1288
+ checkoutError: incompatibleAddOnError || checkoutFormError,
1116
1289
  onCheckout: handleCheckout,
1117
1290
  submitLabel: changeCheckoutButtonLabel ?? deferredInvoiceSubmitLabel,
1118
1291
  ...{ hasEffectiveChangeSelection, isChangeQuoteBlocked },
1119
- submitDisabled: changeFlowSubmitDisabled,
1292
+ submitDisabled:
1293
+ changeFlowSubmitDisabled ||
1294
+ hasIncompatibleAddOnSelections ||
1295
+ hasPendingAdminCustomReceiptLineChanges ||
1296
+ adminCustomReceiptLines.some(
1297
+ (line) =>
1298
+ line.scope === 'SELECTED_COMPONENTS' && line.selectedComponentIds.length === 0,
1299
+ ),
1120
1300
  attributionSummary: flowUi?.partnerAttributionSummary, attributionConfirmLabel: flowUi?.partnerAttributionConfirmLabel,
1121
1301
  attributionConfirmed: partnerAttributionConfirmed,
1122
1302
  onAttributionConfirmedChange: setPartnerAttributionConfirmed,