@ticketboothapp/booking 1.2.123 → 1.2.124

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 (112) hide show
  1. package/CHANGE_BOOKING_BE_HANDOFF.md +15 -4
  2. package/package.json +1 -1
  3. package/src/components/booking/AdminChangeBookingContent.tsx +276 -0
  4. package/src/components/booking/AdminChangeBookingFlow.tsx +651 -6202
  5. package/src/components/booking/AdminChangeCheckoutDialogs.tsx +105 -0
  6. package/src/components/booking/AdminChangeCheckoutPanel.tsx +307 -0
  7. package/src/components/booking/AdminChangePricingPanel.tsx +244 -0
  8. package/src/components/booking/AdminChangeProductSwitcher.tsx +50 -0
  9. package/src/components/booking/AdminChangeReceiptComparison.tsx +167 -0
  10. package/src/components/booking/ChangeBookingCalendarPanel.tsx +126 -0
  11. package/src/components/booking/ChangeBookingCheckoutPanel.tsx +204 -0
  12. package/src/components/booking/ChangeBookingFlow.tsx +576 -4648
  13. package/src/components/booking/ChangeBookingItineraryPanel.tsx +115 -0
  14. package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +57 -0
  15. package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +157 -0
  16. package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +102 -0
  17. package/src/components/booking/CheckoutModal.tsx +68 -49
  18. package/src/components/booking/NewBookingFlow.tsx +485 -3505
  19. package/src/components/booking/PriceBreakdown.tsx +17 -3
  20. package/src/components/booking/PriceSummary.tsx +21 -3
  21. package/src/components/booking/PrivateShuttleAddOnsSection.tsx +102 -0
  22. package/src/components/booking/PrivateShuttleBookingFlow.tsx +407 -2788
  23. package/src/components/booking/PrivateShuttleCheckoutDialogs.tsx +106 -0
  24. package/src/components/booking/PrivateShuttleCheckoutSection.tsx +333 -0
  25. package/src/components/booking/PrivateShuttleItinerarySection.tsx +95 -0
  26. package/src/components/booking/PrivateShuttleMediaIntro.tsx +62 -0
  27. package/src/components/booking/PrivateShuttleOptionSection.tsx +81 -0
  28. package/src/components/booking/PrivateShuttlePassengerSection.tsx +148 -0
  29. package/src/components/booking/PrivateShuttlePickupSection.tsx +91 -0
  30. package/src/components/booking/PrivateShuttlePreferencesSection.tsx +75 -0
  31. package/src/components/booking/PrivateShuttleStartTimeSection.tsx +88 -0
  32. package/src/components/booking/StandardBookingCheckoutDialogs.tsx +106 -0
  33. package/src/components/booking/StandardBookingCheckoutSection.tsx +185 -0
  34. package/src/components/booking/StandardBookingItineraryPanel.tsx +125 -0
  35. package/src/components/booking/StandardBookingMediaIntro.tsx +69 -0
  36. package/src/components/booking/StandardBookingSelectionControlsPanel.tsx +236 -0
  37. package/src/components/booking/admin-change-customer-quote-runner.ts +119 -0
  38. package/src/components/booking/admin-change-flow-state-helpers.ts +124 -0
  39. package/src/components/booking/admin-change-payment-choice-runner.ts +172 -0
  40. package/src/components/booking/admin-change-provider-payload.ts +101 -0
  41. package/src/components/booking/change-booking-checkout-builders.ts +185 -0
  42. package/src/components/booking/change-booking-checkout-quote-runner.ts +207 -0
  43. package/src/components/booking/change-booking-flow-helpers.ts +706 -0
  44. package/src/components/booking/change-booking-payment-modal-builders.ts +151 -0
  45. package/src/components/booking/change-booking-quote-guards.ts +223 -0
  46. package/src/components/booking/change-booking-quote-state.ts +64 -0
  47. package/src/components/booking/pricing-v2-checkout-summary.ts +144 -0
  48. package/src/components/booking/private-shuttle-availability.ts +111 -0
  49. package/src/components/booking/private-shuttle-checkout-builders.ts +264 -0
  50. package/src/components/booking/private-shuttle-checkout-controller.ts +744 -0
  51. package/src/components/booking/private-shuttle-payment-choice-runner.ts +271 -0
  52. package/src/components/booking/private-shuttle-provider-change-runner.ts +73 -0
  53. package/src/components/booking/private-shuttle-reservation-runner.ts +260 -0
  54. package/src/components/booking/standard-booking-availability.ts +305 -0
  55. package/src/components/booking/standard-booking-checkout-builders.ts +289 -0
  56. package/src/components/booking/standard-booking-checkout-controller.ts +659 -0
  57. package/src/components/booking/standard-booking-payment-choice-runner.ts +274 -0
  58. package/src/components/booking/standard-booking-reservation-runner.ts +229 -0
  59. package/src/components/booking/use-private-shuttle-availability.ts +602 -0
  60. package/src/components/booking/use-standard-booking-availability.ts +806 -0
  61. package/src/components/booking/useActiveProductOptions.ts +26 -0
  62. package/src/components/booking/useAdminChangeCheckoutController.ts +806 -0
  63. package/src/components/booking/useAdminChangeOrderSummaryModel.ts +246 -0
  64. package/src/components/booking/useAdminChangePriceSummary.ts +416 -0
  65. package/src/components/booking/useAdminChangePricingDebugPanel.tsx +178 -0
  66. package/src/components/booking/useAdminChangeProductReset.ts +60 -0
  67. package/src/components/booking/useAdminChangeProducts.ts +102 -0
  68. package/src/components/booking/useAdminChangePromoDiscount.ts +235 -0
  69. package/src/components/booking/useAdminChangeProtectedPricing.ts +280 -0
  70. package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +437 -0
  71. package/src/components/booking/useAdminChangeQuotePreview.ts +506 -0
  72. package/src/components/booking/useAdminChangeReceiptDerivation.ts +203 -0
  73. package/src/components/booking/useAdminCustomReceiptLines.ts +57 -0
  74. package/src/components/booking/useAdminProviderPricingAdjustments.ts +146 -0
  75. package/src/components/booking/useBookingAvailabilityAddOns.ts +54 -0
  76. package/src/components/booking/useBookingPromoAndQuantityController.ts +163 -0
  77. package/src/components/booking/useBookingQuantityCapTrim.ts +56 -0
  78. package/src/components/booking/useBookingViewItemAnalytics.ts +30 -0
  79. package/src/components/booking/useChangeBookingAddOnFloorController.ts +109 -0
  80. package/src/components/booking/useChangeBookingAutoSelections.ts +148 -0
  81. package/src/components/booking/useChangeBookingAvailabilities.ts +692 -0
  82. package/src/components/booking/useChangeBookingCheckoutController.ts +461 -0
  83. package/src/components/booking/useChangeBookingInitialHydration.ts +204 -0
  84. package/src/components/booking/useChangeBookingItineraryUiState.ts +133 -0
  85. package/src/components/booking/useChangeBookingPickupItinerary.ts +472 -0
  86. package/src/components/booking/useChangeBookingPriceSummary.ts +199 -0
  87. package/src/components/booking/useChangeBookingPromoDiscount.ts +218 -0
  88. package/src/components/booking/useChangeBookingProtectedPricing.ts +256 -0
  89. package/src/components/booking/useChangeBookingQuotePreview.ts +442 -0
  90. package/src/components/booking/useChangeBookingReceiptDerivation.ts +128 -0
  91. package/src/components/booking/useChangeBookingReceiptFloorController.ts +449 -0
  92. package/src/components/booking/useChangeBookingSelection.ts +362 -0
  93. package/src/components/booking/useChangeBookingSelectionDetails.ts +200 -0
  94. package/src/components/booking/useChangeBookingSummaryLines.ts +169 -0
  95. package/src/components/booking/usePrivateShuttlePriceSummary.ts +513 -0
  96. package/src/components/booking/usePrivateShuttlePromoController.ts +143 -0
  97. package/src/components/booking/usePrivateShuttlePromoDiscount.ts +124 -0
  98. package/src/components/booking/useSeedPromoCodeInput.ts +13 -0
  99. package/src/components/booking/useStandardBookingAutoSelections.ts +224 -0
  100. package/src/components/booking/useStandardBookingPickupItinerary.ts +473 -0
  101. package/src/components/booking/useStandardBookingPriceSummary.ts +438 -0
  102. package/src/components/booking/useStandardBookingPromoController.ts +258 -0
  103. package/src/components/booking/useStandardBookingPromoDiscount.ts +203 -0
  104. package/src/data/products-config.json +1 -1
  105. package/src/lib/booking/change-booking-server-preview.ts +134 -20
  106. package/src/lib/booking/change-flow-pricing.ts +31 -11
  107. package/src/lib/booking/checkout-breakdown.ts +66 -1
  108. package/src/lib/booking/pricing-config.ts +10 -0
  109. package/src/lib/booking/source-metadata.ts +58 -0
  110. package/src/lib/booking-api.ts +154 -3
  111. package/src/lib/pricing-v2-shadow.ts +272 -0
  112. package/test/change-booking-helpers.test.ts +206 -0
@@ -0,0 +1,437 @@
1
+ import { useMemo } from 'react';
2
+ import type { Availability, PricingConfig, ReturnOption } from '../../lib/booking-api';
3
+ import {
4
+ resolveChangeFlowDisplayedAmounts,
5
+ roundMoney,
6
+ } from '../../lib/booking/change-flow-pricing';
7
+ import {
8
+ computePricingDriftDelta,
9
+ mergeLineComparisonsWithFullDrift,
10
+ mergePriceSummaryLinesForDrift,
11
+ sumPriceSummaryLinesMajorUnits,
12
+ } from '../../lib/booking/change-booking-pricing-drift';
13
+ import { mapQuoteLineItemsToPriceSummaryLines } from '../../lib/booking/change-booking-server-preview';
14
+ import type { ChangeBookingFlowProps } from './booking-flow-types';
15
+ import type { Currency } from './CurrencySwitcher';
16
+ import type { PriceSummaryLine } from './PriceSummary';
17
+ import type {
18
+ ProviderDashboardChangePricingUi,
19
+ ProviderDashboardPricingLine,
20
+ } from './booking-flow-ui';
21
+ import type { PricingRate } from './change-booking-flow-helpers';
22
+ import { ChangeBookingPricingDriftPanel } from './ChangeBookingPricingDriftPanel';
23
+ import { buildChangeBookingQuoteStatusPlaceholder } from './ChangeBookingQuoteStatusPlaceholder';
24
+ import { useAdminProviderPricingAdjustments } from './useAdminProviderPricingAdjustments';
25
+ import type { AdminCustomReceiptLine } from './useAdminCustomReceiptLines';
26
+ import type { AdminChangeLatestQuote } from './useAdminChangeCheckoutController';
27
+ import { useChangeBookingSelectionDetails } from './useChangeBookingSelectionDetails';
28
+
29
+ type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
30
+ type TicketLineForDisplay = { category?: string | null; qty: number; itemTotal: number };
31
+
32
+ export interface UseAdminChangeQuoteDisplayStateParams {
33
+ suppressSelfServeCurrencyUi: boolean;
34
+ latestChangeQuote: AdminChangeLatestQuote | null;
35
+ selectedAvailability: Availability | null;
36
+ selectedReturnOption: ReturnOption | null;
37
+ totalQuantity: number;
38
+ checkoutPriceSummaryLines: PriceSummaryLine[];
39
+ changeFlowNewBookingTotal: number;
40
+ currency: Currency;
41
+ locale: 'en' | 'fr';
42
+ ticketLineItemsForChangeFlowDisplay: TicketLineForDisplay[];
43
+ pricingForTicketSelector: PricingRate[];
44
+ isTaxIncludedInPrice: boolean;
45
+ effectiveTax: number;
46
+ effectivePromoDiscountAmount: number;
47
+ appliedPromoCode: string | null;
48
+ originalReceipt: ChangeBookingFlowProps['originalReceipt'];
49
+ t: (key: string) => string;
50
+ selfServePricingConfirmed: boolean;
51
+ changeQuoteLoading: boolean;
52
+ changeQuoteFetchError: string | null;
53
+ initialValues: ChangeBookingFlowProps['initialValues'];
54
+ implicitReturnBaselineId: string | null;
55
+ pickupLocationId: string | null;
56
+ quantities: Record<string, number>;
57
+ addOnSelections: AddOnSelection[];
58
+ initialAddOnMinQtyByKey: Map<string, number>;
59
+ changeFlowResolvedInitialProductOptionId: string | null;
60
+ selectedParentProductChanged: boolean;
61
+ isProviderDashboardChange: boolean;
62
+ showProviderPricingInlineEditor: boolean;
63
+ providerPricingUi?: ProviderDashboardChangePricingUi;
64
+ providerQuotedLines: ProviderDashboardPricingLine[];
65
+ editableSummaryPreSubtotalDelta: number;
66
+ adminCustomAdjustmentTotal: number;
67
+ adminCustomReceiptLines: AdminCustomReceiptLine[];
68
+ isManualOverrideEligibleLine: (line: ProviderDashboardPricingLine) => boolean;
69
+ isAdmin: boolean;
70
+ useAdminFeAuthoritativeQuote: boolean;
71
+ isCustomerSelfServeChange: boolean;
72
+ effectiveSubtotalForCheckout: number;
73
+ pricingConfig: PricingConfig | null;
74
+ }
75
+
76
+ export function useAdminChangeQuoteDisplayState({
77
+ suppressSelfServeCurrencyUi,
78
+ latestChangeQuote,
79
+ selectedAvailability,
80
+ selectedReturnOption,
81
+ totalQuantity,
82
+ checkoutPriceSummaryLines,
83
+ changeFlowNewBookingTotal,
84
+ currency,
85
+ locale,
86
+ ticketLineItemsForChangeFlowDisplay,
87
+ pricingForTicketSelector,
88
+ isTaxIncludedInPrice,
89
+ effectiveTax,
90
+ effectivePromoDiscountAmount,
91
+ appliedPromoCode,
92
+ originalReceipt,
93
+ t,
94
+ selfServePricingConfirmed,
95
+ changeQuoteLoading,
96
+ changeQuoteFetchError,
97
+ initialValues,
98
+ implicitReturnBaselineId,
99
+ pickupLocationId,
100
+ quantities,
101
+ addOnSelections,
102
+ initialAddOnMinQtyByKey,
103
+ changeFlowResolvedInitialProductOptionId,
104
+ selectedParentProductChanged,
105
+ isProviderDashboardChange,
106
+ showProviderPricingInlineEditor,
107
+ providerPricingUi,
108
+ providerQuotedLines,
109
+ editableSummaryPreSubtotalDelta,
110
+ adminCustomAdjustmentTotal,
111
+ adminCustomReceiptLines,
112
+ isManualOverrideEligibleLine,
113
+ isAdmin,
114
+ useAdminFeAuthoritativeQuote,
115
+ isCustomerSelfServeChange,
116
+ effectiveSubtotalForCheckout,
117
+ pricingConfig,
118
+ }: UseAdminChangeQuoteDisplayStateParams) {
119
+ /** When quote blocks checkout, compare FE vs server lines for debugging (prefers `pricingDriftDetail` from API when sent). */
120
+ const quoteBlockedPricingDrift = useMemo(() => {
121
+ if (!suppressSelfServeCurrencyUi || latestChangeQuote?.canProceed !== false) return null;
122
+ if (!selectedAvailability || totalQuantity <= 0) return null;
123
+
124
+ const api = latestChangeQuote.pricingDriftDetail;
125
+ const currencyForFmt = (latestChangeQuote.currency ?? currency) as Currency;
126
+
127
+ const clientMappedFromApi = mapQuoteLineItemsToPriceSummaryLines(api?.clientLineItems);
128
+ const useBeClientLines = clientMappedFromApi.length > 0;
129
+ /**
130
+ * Checkout passes tax/discount via PriceSummary props, not always as `priceSummaryLines`. Include them here so
131
+ * drift rows and their sums align with `changeFlowNewBookingTotal` (subtotal + tax - promo).
132
+ */
133
+ const clientLinesForMerge: PriceSummaryLine[] = (() => {
134
+ if (useBeClientLines) return clientMappedFromApi;
135
+ const lines: PriceSummaryLine[] = [...checkoutPriceSummaryLines];
136
+ const hasTaxLine = lines.some(
137
+ (l) => l.kind === 'line' && String(l.type ?? '').toUpperCase() === 'TAX',
138
+ );
139
+ if (!hasTaxLine && !isTaxIncludedInPrice && Math.abs(effectiveTax) >= 0.005) {
140
+ lines.push({
141
+ kind: 'line',
142
+ label: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
143
+ amount: effectiveTax,
144
+ type: 'TAX',
145
+ });
146
+ }
147
+ const hasPromoSummaryLine = lines.some(
148
+ (l) =>
149
+ l.kind === 'line' &&
150
+ (/PROMO|DISCOUNT|VOUCHER|GIFT/i.test(String(l.type ?? '')) ||
151
+ (l.amount < -0.005 && /promo|discount/i.test(l.label))),
152
+ );
153
+ if (!hasPromoSummaryLine && Math.abs(effectivePromoDiscountAmount) >= 0.005) {
154
+ const trimmedPromoCode = appliedPromoCode?.trim() ?? '';
155
+ const promoLabel =
156
+ trimmedPromoCode.length > 0
157
+ ? `Promo: ${trimmedPromoCode}`
158
+ : originalReceipt?.promoLabel?.trim() || 'Discount';
159
+ lines.push({
160
+ kind: 'line',
161
+ label: promoLabel,
162
+ amount: -effectivePromoDiscountAmount,
163
+ type: 'PROMO_CODE',
164
+ });
165
+ }
166
+ return lines;
167
+ })();
168
+
169
+ const serverMappedFromApi = mapQuoteLineItemsToPriceSummaryLines(api?.serverLineItems);
170
+ const useBeServerLines = serverMappedFromApi.length > 0;
171
+ const serverLinesForMerge = useBeServerLines
172
+ ? serverMappedFromApi
173
+ : (latestChangeQuote.serverPreview?.priceSummaryLines ?? []);
174
+
175
+ const clientTotalForDrift =
176
+ api?.clientTotalMajorUnits != null && Number.isFinite(api.clientTotalMajorUnits)
177
+ ? api.clientTotalMajorUnits
178
+ : useBeClientLines
179
+ ? sumPriceSummaryLinesMajorUnits(clientMappedFromApi)
180
+ : changeFlowNewBookingTotal;
181
+
182
+ let totalDelta: number | null = null;
183
+ if (api?.deltaMajorUnits != null && Number.isFinite(api.deltaMajorUnits)) {
184
+ totalDelta = roundMoney(api.deltaMajorUnits);
185
+ }
186
+
187
+ /**
188
+ * Server "price check" total: explicit API fields -> sum of BE server lines -> derive from delta -> receipt preview totals.
189
+ */
190
+ let serverTotalFromQuote: number | undefined =
191
+ api?.serverTotalMajorUnits != null && Number.isFinite(api.serverTotalMajorUnits)
192
+ ? api.serverTotalMajorUnits
193
+ : undefined;
194
+ if (serverTotalFromQuote == null && useBeServerLines) {
195
+ serverTotalFromQuote = sumPriceSummaryLinesMajorUnits(serverMappedFromApi);
196
+ }
197
+ if (serverTotalFromQuote == null && totalDelta != null) {
198
+ serverTotalFromQuote = roundMoney(clientTotalForDrift - totalDelta);
199
+ }
200
+ if (serverTotalFromQuote == null) {
201
+ serverTotalFromQuote =
202
+ latestChangeQuote.serverDisplay?.total ??
203
+ latestChangeQuote.serverPreview?.totalNewBooking ??
204
+ undefined;
205
+ }
206
+ if (totalDelta == null && serverTotalFromQuote != null && Number.isFinite(serverTotalFromQuote)) {
207
+ totalDelta = roundMoney(clientTotalForDrift - serverTotalFromQuote);
208
+ }
209
+
210
+ const mergedForDrift = mergePriceSummaryLinesForDrift(clientLinesForMerge, serverLinesForMerge);
211
+ const rows =
212
+ api?.lineComparisons && api.lineComparisons.length > 0
213
+ ? mergeLineComparisonsWithFullDrift(api.lineComparisons, mergedForDrift)
214
+ : mergedForDrift;
215
+
216
+ const receiptAnchoring = api?.receiptAnchoring;
217
+ let driftRows = rows;
218
+ if (receiptAnchoring) {
219
+ const adj = roundMoney(
220
+ receiptAnchoring.reconciledTotalMajorUnits -
221
+ receiptAnchoring.requestedCatalogLineSumMajorUnits,
222
+ );
223
+ if (Math.abs(adj) >= 0.005) {
224
+ driftRows = [
225
+ ...rows,
226
+ {
227
+ key: 'meta:receipt-anchoring',
228
+ label: 'Receipt anchoring adjustment (BE only - not included in app total)',
229
+ clientAmount: null,
230
+ serverAmount: adj,
231
+ delta: computePricingDriftDelta(null, adj),
232
+ },
233
+ ];
234
+ }
235
+ }
236
+
237
+ const hasTotalDelta = totalDelta != null && Math.abs(totalDelta) >= 0.005;
238
+
239
+ if (driftRows.length === 0 && !hasTotalDelta) {
240
+ return null;
241
+ }
242
+
243
+ const usesBeLinePayload = useBeClientLines || useBeServerLines;
244
+
245
+ const ticketCartDetail = ticketLineItemsForChangeFlowDisplay
246
+ .filter((l) => (l.qty ?? 0) > 0)
247
+ .map((line) => {
248
+ const cat = line.category?.trim().toUpperCase() ?? '';
249
+ const rate = pricingForTicketSelector.find((r) => r.category.toUpperCase() === cat);
250
+ const rawList = rate?.baseInDisplayCurrency ?? rate?.priceCAD;
251
+ const listUnit =
252
+ rawList != null && Number.isFinite(Number(rawList))
253
+ ? Number(rawList)
254
+ : line.qty > 0
255
+ ? line.itemTotal / line.qty
256
+ : 0;
257
+ return {
258
+ category: cat,
259
+ qty: line.qty,
260
+ listUnitMajor: roundMoney(listUnit),
261
+ effectiveUnitMajor: roundMoney(line.qty > 0 ? line.itemTotal / line.qty : 0),
262
+ lineTotalMajor: line.itemTotal,
263
+ };
264
+ });
265
+
266
+ return (
267
+ <ChangeBookingPricingDriftPanel
268
+ rows={driftRows}
269
+ clientTotal={clientTotalForDrift}
270
+ serverTotal={serverTotalFromQuote}
271
+ totalDelta={totalDelta}
272
+ currency={currencyForFmt}
273
+ locale={locale}
274
+ ticketCartDetail={ticketCartDetail.length > 0 ? ticketCartDetail : undefined}
275
+ serverTicketPricingTrace={latestChangeQuote.ticketPricingTrace ?? undefined}
276
+ receiptAnchoring={receiptAnchoring}
277
+ footnote={
278
+ usesBeLinePayload
279
+ ? '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.'
280
+ : undefined
281
+ }
282
+ />
283
+ );
284
+ }, [
285
+ suppressSelfServeCurrencyUi,
286
+ latestChangeQuote,
287
+ selectedAvailability,
288
+ totalQuantity,
289
+ checkoutPriceSummaryLines,
290
+ changeFlowNewBookingTotal,
291
+ currency,
292
+ locale,
293
+ ticketLineItemsForChangeFlowDisplay,
294
+ pricingForTicketSelector,
295
+ isTaxIncludedInPrice,
296
+ effectiveTax,
297
+ effectivePromoDiscountAmount,
298
+ appliedPromoCode,
299
+ originalReceipt?.promoLabel,
300
+ t,
301
+ ]);
302
+
303
+ const selfServeCheckoutPlaceholder = buildChangeBookingQuoteStatusPlaceholder({
304
+ suppressSelfServeCurrencyUi,
305
+ hasSelectedAvailability: selectedAvailability != null,
306
+ totalQuantity,
307
+ selfServePricingConfirmed,
308
+ changeQuoteLoading,
309
+ changeQuoteFetchError,
310
+ quoteCanProceed: latestChangeQuote?.canProceed,
311
+ quoteBlockedReason: latestChangeQuote?.reasonIfBlocked,
312
+ blockedDetails: quoteBlockedPricingDrift,
313
+ t,
314
+ });
315
+
316
+ const changeSelectionDetails = useChangeBookingSelectionDetails({
317
+ initialValues,
318
+ selectedAvailability,
319
+ selectedReturnOption,
320
+ implicitReturnBaselineId,
321
+ pickupLocationId,
322
+ quantities,
323
+ addOnSelections,
324
+ initialAddOnMinQtyByKey,
325
+ changeFlowResolvedInitialProductOptionId,
326
+ productChanged: selectedParentProductChanged,
327
+ });
328
+ const hasChangeSelection =
329
+ isProviderDashboardChange
330
+ ? changeSelectionDetails.hasOperationalChangesFromInitial
331
+ : changeSelectionDetails.hasChangesFromInitial;
332
+
333
+ const {
334
+ providerTotalsPreview,
335
+ hasEffectiveChangeSelection,
336
+ providerPricingOverrides,
337
+ adminCustomLinesAsAdditionalAdjustments,
338
+ mergedProviderAdditionalAdjustments,
339
+ } = useAdminProviderPricingAdjustments({
340
+ isProviderDashboardChange,
341
+ showProviderPricingInlineEditor,
342
+ providerPricingUi,
343
+ providerQuotedLines,
344
+ hasChangeSelection,
345
+ editableSummaryPreSubtotalDelta,
346
+ adminCustomAdjustmentTotal,
347
+ adminCustomReceiptLines,
348
+ isManualOverrideEligibleLine,
349
+ });
350
+
351
+ const unchangedReceiptAnchor = useMemo(
352
+ () =>
353
+ originalReceipt && !hasEffectiveChangeSelection
354
+ ? {
355
+ subtotal: roundMoney(originalReceipt.subtotal),
356
+ tax: roundMoney(originalReceipt.tax),
357
+ total: roundMoney(originalReceipt.total),
358
+ }
359
+ : null,
360
+ [hasEffectiveChangeSelection, originalReceipt],
361
+ );
362
+
363
+ /**
364
+ * True until `quoteChangeBooking` returns a confirmed `serverDisplay` for the current selection.
365
+ * Named so manual price-line UI can align with "still settling final price" without touching ticket/receipt math.
366
+ */
367
+ const changeFlowFinalPricePending =
368
+ suppressSelfServeCurrencyUi &&
369
+ selectedAvailability != null &&
370
+ totalQuantity > 0 &&
371
+ !selfServePricingConfirmed;
372
+
373
+ /**
374
+ * Provider inline edits + admin custom lines: show while waiting on or after the authoritative quote.
375
+ * UI-only gate - does not alter ticket line items or receipt-floor rules.
376
+ */
377
+ const showChangeFlowManualPriceLines =
378
+ hasEffectiveChangeSelection && (changeFlowFinalPricePending || selfServePricingConfirmed);
379
+ const showAdminCustomLineEditor =
380
+ isAdmin &&
381
+ selectedAvailability != null &&
382
+ totalQuantity > 0;
383
+
384
+ const displayedChangeAmountsRaw = resolveChangeFlowDisplayedAmounts({
385
+ providerPreview: providerTotalsPreview,
386
+ serverQuotePreview:
387
+ !useAdminFeAuthoritativeQuote && isCustomerSelfServeChange && latestChangeQuote?.serverDisplay
388
+ ? latestChangeQuote.serverDisplay
389
+ : null,
390
+ fromCart: {
391
+ total: changeFlowNewBookingTotal,
392
+ subtotal: effectiveSubtotalForCheckout,
393
+ tax: effectiveTax,
394
+ },
395
+ });
396
+ /** When cart drives display, `effectiveSubtotalForCheckout` already includes admin adjustments - do not add twice. */
397
+ const displayLayerUsesExternalPricing =
398
+ Boolean(providerTotalsPreview) ||
399
+ (!useAdminFeAuthoritativeQuote && isCustomerSelfServeChange && Boolean(latestChangeQuote?.serverDisplay));
400
+ const adminTaxDeltaForExternalDisplay =
401
+ displayLayerUsesExternalPricing && !isTaxIncludedInPrice && Math.abs(adminCustomAdjustmentTotal) >= 0.0005
402
+ ? roundMoney(adminCustomAdjustmentTotal * (pricingConfig?.taxRate ?? 0))
403
+ : 0;
404
+ const displayChangeFlowTax = roundMoney(
405
+ unchangedReceiptAnchor
406
+ ? unchangedReceiptAnchor.tax
407
+ : displayedChangeAmountsRaw.tax + adminTaxDeltaForExternalDisplay
408
+ );
409
+ const displayChangeFlowProposedTotal = roundMoney(
410
+ unchangedReceiptAnchor
411
+ ? unchangedReceiptAnchor.total
412
+ : displayLayerUsesExternalPricing
413
+ ? displayedChangeAmountsRaw.total + adminCustomAdjustmentTotal + adminTaxDeltaForExternalDisplay
414
+ : displayedChangeAmountsRaw.total
415
+ );
416
+ const displayChangeFlowProposedTotalWithEditableLines = roundMoney(
417
+ displayChangeFlowProposedTotal + editableSummaryPreSubtotalDelta
418
+ );
419
+
420
+ return {
421
+ selfServeCheckoutPlaceholder,
422
+ changeSelectionDetails,
423
+ hasChangeSelection,
424
+ hasEffectiveChangeSelection,
425
+ providerPricingOverrides,
426
+ adminCustomLinesAsAdditionalAdjustments,
427
+ mergedProviderAdditionalAdjustments,
428
+ unchangedReceiptAnchor,
429
+ showChangeFlowManualPriceLines,
430
+ showAdminCustomLineEditor,
431
+ displayedChangeAmountsRaw,
432
+ displayLayerUsesExternalPricing,
433
+ displayChangeFlowTax,
434
+ displayChangeFlowProposedTotal,
435
+ displayChangeFlowProposedTotalWithEditableLines,
436
+ };
437
+ }