@ticketboothapp/booking 1.2.75 → 1.2.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_BOOKING_BE_HANDOFF.md +36 -0
- package/package.json +1 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +460 -53
- package/src/components/booking/CheckoutForm.tsx +8 -0
- package/src/components/booking/PriceBreakdown.tsx +30 -6
- package/src/components/booking/PriceSummary.tsx +31 -8
- package/src/components/booking/booking-flow-ui.ts +5 -0
- package/src/lib/booking/change-flow-pricing.ts +10 -5
- package/src/lib/booking-api.ts +54 -0
|
@@ -100,3 +100,39 @@ Pickers show **catalog** prices by default (same booking **currency** as the ori
|
|
|
100
100
|
- **Provider change API:** When `pricingAdjustment.additionalAdjustments` includes the same rows, persist them on the new receipt and charge the **same** total as `newTotalAmount` (within your rounding rules).
|
|
101
101
|
|
|
102
102
|
If the server ignores `manualLineAdjustments` or `additionalAdjustments`, the customer will see the FE total but pay a different amount.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Admin FE-authoritative quote path (new)
|
|
107
|
+
|
|
108
|
+
For provider dashboard / admin tooling, we need a dedicated quote route where the **frontend is authoritative** for
|
|
109
|
+
the full receipt preview (totals + line items), and BE does not re-price those values.
|
|
110
|
+
|
|
111
|
+
### Endpoint
|
|
112
|
+
|
|
113
|
+
- **`POST /1/admin/bookings/:bookingReference/change/quote?lastName=...`**
|
|
114
|
+
|
|
115
|
+
Keep the public self-serve endpoint unchanged.
|
|
116
|
+
|
|
117
|
+
### Request contract (admin)
|
|
118
|
+
|
|
119
|
+
Same selection fields as `ChangeBookingQuoteRequest`, plus:
|
|
120
|
+
|
|
121
|
+
- `feReceipt`:
|
|
122
|
+
- `subtotal` (major units)
|
|
123
|
+
- `tax` (major units)
|
|
124
|
+
- `total` (major units)
|
|
125
|
+
- `currency?`
|
|
126
|
+
- `lineItems: Array<{ label?: string; amount?: number; type?: string; quantity?: number }>`
|
|
127
|
+
- `feAmountDueMajorUnits?` (optional signed delta the FE displays; `newTotal - previousTotal`)
|
|
128
|
+
|
|
129
|
+
### Backend expectations
|
|
130
|
+
|
|
131
|
+
- Validate admin auth + booking/availability eligibility only.
|
|
132
|
+
- Persist `feReceipt` on the change intent as the quote source-of-truth.
|
|
133
|
+
- Return a `ChangeBookingQuoteResponse`-compatible payload using the stored FE receipt:
|
|
134
|
+
- `newReceipt` / `proposed` line items from `feReceipt.lineItems`
|
|
135
|
+
- `newReceipt.subtotal/tax/total` from `feReceipt`
|
|
136
|
+
- `amountDueCents` / `balanceDeltaMajorUnits` / `priceDiff` coherent with stored totals
|
|
137
|
+
- `changeIntentId`, `canProceed`, `reasonIfBlocked`
|
|
138
|
+
- Payment intent + apply/confirm for this change intent must use persisted FE totals/lines (no repricing).
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
cancelReservationBestEffort,
|
|
10
10
|
createPaymentIntent,
|
|
11
11
|
quoteChangeBooking,
|
|
12
|
+
quoteChangeBookingAdminFeReceipt,
|
|
12
13
|
confirmFreeChangeBooking,
|
|
13
14
|
createChangeBookingPaymentIntent,
|
|
14
15
|
confirmBookingWithoutPayment,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
isInsufficientCapacityReserveError,
|
|
26
27
|
describeStandardTourCapacityConflictMessage,
|
|
27
28
|
reportReserveCapacityConflictClientContext,
|
|
29
|
+
type AdminFeAuthoritativeReceipt,
|
|
28
30
|
} from '../../lib/booking-api';
|
|
29
31
|
import {
|
|
30
32
|
EARLIEST_AVAILABILITY_DATE,
|
|
@@ -126,6 +128,9 @@ function mergeQuoteSliceWithServerPreview(
|
|
|
126
128
|
serverPreview: buildChangeBookingServerPreview(quote, fallbackCart, currency),
|
|
127
129
|
pricingDriftDetail: normalizePricingDriftDetailFromQuote(quote),
|
|
128
130
|
ticketPricingTrace: normalizeTicketPricingTraceFromQuote(quote),
|
|
131
|
+
/** Same cent pair the BE uses for `amountDueCents` / intent & receipt "New Booking Difference" — use for signed refund display. */
|
|
132
|
+
quotePreviousTotalCents: quote.previousTotalCents,
|
|
133
|
+
quoteNewTotalCents: quote.newTotalCents,
|
|
129
134
|
};
|
|
130
135
|
}
|
|
131
136
|
|
|
@@ -145,6 +150,26 @@ function omitZeroAmountPromoDiscountSummaryLines(lines: PriceSummaryLine[]): Pri
|
|
|
145
150
|
});
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
function mapSummaryLinesToFeReceiptLineItems(lines: PriceSummaryLine[]): NonNullable<AdminFeAuthoritativeReceipt['lineItems']> {
|
|
154
|
+
return lines.map((line) => {
|
|
155
|
+
if (line.kind === 'ticket') {
|
|
156
|
+
return {
|
|
157
|
+
label: line.category,
|
|
158
|
+
amount: roundMoney(line.itemTotal),
|
|
159
|
+
type: 'TICKET',
|
|
160
|
+
quantity: line.qty,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const normalizedType = String(line.type ?? '').trim().toUpperCase();
|
|
164
|
+
return {
|
|
165
|
+
label: line.label,
|
|
166
|
+
amount: roundMoney(line.amount),
|
|
167
|
+
type: normalizedType || undefined,
|
|
168
|
+
quantity: line.quantity ?? undefined,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
function formatTicketLineItemsForSummary(lines: Array<{ category: string; qty: number }>): string {
|
|
149
174
|
const labels: Record<string, string> = {
|
|
150
175
|
ADULT: 'adult',
|
|
@@ -730,6 +755,8 @@ export function AdminChangeBookingFlow({
|
|
|
730
755
|
const [adminCustomReceiptLines, setAdminCustomReceiptLines] = useState<
|
|
731
756
|
Array<{ id: string; label: string; amountInput: string; amountSign: 1 | -1 }>
|
|
732
757
|
>([]);
|
|
758
|
+
const [editableSummaryLineAmountInputs, setEditableSummaryLineAmountInputs] = useState<Record<string, string>>({});
|
|
759
|
+
const [editableSummaryLineLabelInputs, setEditableSummaryLineLabelInputs] = useState<Record<string, string>>({});
|
|
733
760
|
const adminCustomLineIdRef = useRef(0);
|
|
734
761
|
|
|
735
762
|
// Auto-apply promo code when parent page passes one (e.g. partner pages).
|
|
@@ -828,6 +855,10 @@ export function AdminChangeBookingFlow({
|
|
|
828
855
|
}
|
|
829
856
|
}, [initialValues?.dateTime, companyTimezone]);
|
|
830
857
|
const isProviderDashboardChange = Boolean(onChangeBooking);
|
|
858
|
+
const useAdminFeAuthoritativeQuote =
|
|
859
|
+
isAdmin &&
|
|
860
|
+
isProviderDashboardChange &&
|
|
861
|
+
Boolean(flowUi?.adminFeAuthoritativeQuoteEnabled);
|
|
831
862
|
/** Any change from an existing booking (public or provider). */
|
|
832
863
|
const isChangeBookingContext = Boolean(initialValues?.bookingReference?.trim());
|
|
833
864
|
/**
|
|
@@ -852,9 +883,10 @@ export function AdminChangeBookingFlow({
|
|
|
852
883
|
const lockedPromoCode = initialValues?.promoCode?.trim()
|
|
853
884
|
? initialValues.promoCode.trim().toUpperCase()
|
|
854
885
|
: null;
|
|
855
|
-
/** Public self-serve only: cannot reduce tickets below original counts. */
|
|
886
|
+
/** Public self-serve only: cannot reduce tickets below original counts. Provider-dashboard admins may reduce party size. */
|
|
856
887
|
const changeBookingMinimumQuantities = useMemo(() => {
|
|
857
888
|
if (!isCustomerSelfServeChange || !initialValues?.bookingItems?.length) return undefined;
|
|
889
|
+
if (isAdmin && isProviderDashboardChange) return undefined;
|
|
858
890
|
const m: Record<string, number> = {};
|
|
859
891
|
for (const item of initialValues.bookingItems) {
|
|
860
892
|
const key = item.category?.trim();
|
|
@@ -862,7 +894,7 @@ export function AdminChangeBookingFlow({
|
|
|
862
894
|
m[key] = Math.max(0, Number(item.count) || 0);
|
|
863
895
|
}
|
|
864
896
|
return m;
|
|
865
|
-
}, [isCustomerSelfServeChange, initialValues?.bookingItems]);
|
|
897
|
+
}, [isCustomerSelfServeChange, initialValues?.bookingItems, isAdmin, isProviderDashboardChange]);
|
|
866
898
|
const [adminChoiceData, setAdminChoiceData] = useState<{
|
|
867
899
|
reservationReference: string;
|
|
868
900
|
reservationExpiration?: string;
|
|
@@ -895,6 +927,9 @@ export function AdminChangeBookingFlow({
|
|
|
895
927
|
quotedTotal?: number;
|
|
896
928
|
/** From `quoteChangeBooking` receipt fields — drives PriceSummary when self-serve. */
|
|
897
929
|
serverDisplay?: { total: number; subtotal: number; tax: number };
|
|
930
|
+
/** Server quote cents — authoritative vs `serverDisplay` scaling (aligns with receipt difference line). */
|
|
931
|
+
quotePreviousTotalCents?: number;
|
|
932
|
+
quoteNewTotalCents?: number;
|
|
898
933
|
/** Parsed from last quote — unified server-owned preview for lines + picker overrides. */
|
|
899
934
|
serverPreview: ReturnType<typeof buildChangeBookingServerPreview>;
|
|
900
935
|
pricingDriftDetail?: ChangeBookingQuotePricingDriftDetail;
|
|
@@ -3113,6 +3148,140 @@ export function AdminChangeBookingFlow({
|
|
|
3113
3148
|
),
|
|
3114
3149
|
[checkoutPriceSummaryLinesForCheckout],
|
|
3115
3150
|
);
|
|
3151
|
+
const getEditableSummaryLineKey = useCallback((line: PriceSummaryLine, index: number): string => {
|
|
3152
|
+
if (line.lineKey) return line.lineKey;
|
|
3153
|
+
return line.kind === 'ticket'
|
|
3154
|
+
? `change-ticket-${line.category}-${index}`
|
|
3155
|
+
: `change-line-${line.label}-${line.type ?? 'line'}-${index}`;
|
|
3156
|
+
}, []);
|
|
3157
|
+
const editableCheckoutPriceSummaryLines = useMemo((): PriceSummaryLine[] => {
|
|
3158
|
+
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
3159
|
+
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
3160
|
+
);
|
|
3161
|
+
return checkoutPriceSummaryLinesForCheckout.map((line, index) => {
|
|
3162
|
+
const isBeforeSubtotal = firstTaxLineIndex < 0 || index < firstTaxLineIndex;
|
|
3163
|
+
const lineKey = getEditableSummaryLineKey(line, index);
|
|
3164
|
+
if (line.kind === 'ticket') {
|
|
3165
|
+
const amountInput = editableSummaryLineAmountInputs[lineKey];
|
|
3166
|
+
const parsedAmount = amountInput == null || amountInput.trim() === '' ? line.itemTotal : Number(amountInput);
|
|
3167
|
+
return {
|
|
3168
|
+
...line,
|
|
3169
|
+
lineKey,
|
|
3170
|
+
editable: isBeforeSubtotal,
|
|
3171
|
+
category: editableSummaryLineLabelInputs[lineKey] ?? line.category,
|
|
3172
|
+
itemTotal: Number.isFinite(parsedAmount) ? roundMoney(parsedAmount) : line.itemTotal,
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
const amountInput = editableSummaryLineAmountInputs[lineKey];
|
|
3176
|
+
const parsedAmount = amountInput == null || amountInput.trim() === '' ? line.amount : Number(amountInput);
|
|
3177
|
+
return {
|
|
3178
|
+
...line,
|
|
3179
|
+
lineKey,
|
|
3180
|
+
editable: isBeforeSubtotal,
|
|
3181
|
+
label: editableSummaryLineLabelInputs[lineKey] ?? line.label,
|
|
3182
|
+
amount: Number.isFinite(parsedAmount) ? roundMoney(parsedAmount) : line.amount,
|
|
3183
|
+
};
|
|
3184
|
+
});
|
|
3185
|
+
}, [
|
|
3186
|
+
checkoutPriceSummaryLinesForCheckout,
|
|
3187
|
+
editableSummaryLineAmountInputs,
|
|
3188
|
+
editableSummaryLineLabelInputs,
|
|
3189
|
+
getEditableSummaryLineKey,
|
|
3190
|
+
]);
|
|
3191
|
+
useEffect(() => {
|
|
3192
|
+
setEditableSummaryLineAmountInputs((prev) => {
|
|
3193
|
+
const next: Record<string, string> = {};
|
|
3194
|
+
for (let i = 0; i < editableCheckoutPriceSummaryLines.length; i += 1) {
|
|
3195
|
+
const line = editableCheckoutPriceSummaryLines[i];
|
|
3196
|
+
const key = line.lineKey;
|
|
3197
|
+
if (!key) continue;
|
|
3198
|
+
if (prev[key] != null) {
|
|
3199
|
+
next[key] = prev[key];
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
next[key] = line.kind === 'ticket' ? String(line.itemTotal) : String(line.amount);
|
|
3203
|
+
}
|
|
3204
|
+
const prevKeys = Object.keys(prev);
|
|
3205
|
+
const nextKeys = Object.keys(next);
|
|
3206
|
+
if (
|
|
3207
|
+
prevKeys.length === nextKeys.length &&
|
|
3208
|
+
nextKeys.every((key) => prev[key] === next[key])
|
|
3209
|
+
) {
|
|
3210
|
+
return prev;
|
|
3211
|
+
}
|
|
3212
|
+
return next;
|
|
3213
|
+
});
|
|
3214
|
+
setEditableSummaryLineLabelInputs((prev) => {
|
|
3215
|
+
const next: Record<string, string> = {};
|
|
3216
|
+
for (let i = 0; i < editableCheckoutPriceSummaryLines.length; i += 1) {
|
|
3217
|
+
const line = editableCheckoutPriceSummaryLines[i];
|
|
3218
|
+
const key = line.lineKey;
|
|
3219
|
+
if (!key) continue;
|
|
3220
|
+
if (prev[key] != null) {
|
|
3221
|
+
next[key] = prev[key];
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
next[key] = line.kind === 'ticket' ? line.category : line.label;
|
|
3225
|
+
}
|
|
3226
|
+
const prevKeys = Object.keys(prev);
|
|
3227
|
+
const nextKeys = Object.keys(next);
|
|
3228
|
+
if (
|
|
3229
|
+
prevKeys.length === nextKeys.length &&
|
|
3230
|
+
nextKeys.every((key) => prev[key] === next[key])
|
|
3231
|
+
) {
|
|
3232
|
+
return prev;
|
|
3233
|
+
}
|
|
3234
|
+
return next;
|
|
3235
|
+
});
|
|
3236
|
+
}, [editableCheckoutPriceSummaryLines]);
|
|
3237
|
+
const editableSummaryPreSubtotalDelta = useMemo(() => {
|
|
3238
|
+
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
3239
|
+
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
3240
|
+
);
|
|
3241
|
+
let delta = 0;
|
|
3242
|
+
for (let i = 0; i < checkoutPriceSummaryLinesForCheckout.length; i += 1) {
|
|
3243
|
+
if (firstTaxLineIndex >= 0 && i >= firstTaxLineIndex) break;
|
|
3244
|
+
const line = checkoutPriceSummaryLinesForCheckout[i];
|
|
3245
|
+
const lineKey = getEditableSummaryLineKey(line, i);
|
|
3246
|
+
const baselineAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
3247
|
+
const raw = editableSummaryLineAmountInputs[lineKey];
|
|
3248
|
+
const parsed = raw == null || raw.trim() === '' ? baselineAmount : Number(raw);
|
|
3249
|
+
const editedAmount = Number.isFinite(parsed) ? roundMoney(parsed) : baselineAmount;
|
|
3250
|
+
delta += editedAmount - baselineAmount;
|
|
3251
|
+
}
|
|
3252
|
+
return roundMoney(delta);
|
|
3253
|
+
}, [
|
|
3254
|
+
checkoutPriceSummaryLinesForCheckout,
|
|
3255
|
+
editableSummaryLineAmountInputs,
|
|
3256
|
+
getEditableSummaryLineKey,
|
|
3257
|
+
]);
|
|
3258
|
+
const editableSummaryPreSubtotalDebugRows = useMemo(() => {
|
|
3259
|
+
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
3260
|
+
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
3261
|
+
);
|
|
3262
|
+
const rows: Array<{ label: string; baseline: number; edited: number; delta: number }> = [];
|
|
3263
|
+
for (let i = 0; i < checkoutPriceSummaryLinesForCheckout.length; i += 1) {
|
|
3264
|
+
if (firstTaxLineIndex >= 0 && i >= firstTaxLineIndex) break;
|
|
3265
|
+
const line = checkoutPriceSummaryLinesForCheckout[i];
|
|
3266
|
+
const lineKey = getEditableSummaryLineKey(line, i);
|
|
3267
|
+
const baselineAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
3268
|
+
const raw = editableSummaryLineAmountInputs[lineKey];
|
|
3269
|
+
const parsed = raw == null || raw.trim() === '' ? baselineAmount : Number(raw);
|
|
3270
|
+
const editedAmount = Number.isFinite(parsed) ? roundMoney(parsed) : baselineAmount;
|
|
3271
|
+
const delta = roundMoney(editedAmount - baselineAmount);
|
|
3272
|
+
rows.push({
|
|
3273
|
+
label: line.kind === 'ticket' ? line.category : line.label,
|
|
3274
|
+
baseline: roundMoney(baselineAmount),
|
|
3275
|
+
edited: editedAmount,
|
|
3276
|
+
delta,
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
return rows;
|
|
3280
|
+
}, [
|
|
3281
|
+
checkoutPriceSummaryLinesForCheckout,
|
|
3282
|
+
editableSummaryLineAmountInputs,
|
|
3283
|
+
getEditableSummaryLineKey,
|
|
3284
|
+
]);
|
|
3116
3285
|
|
|
3117
3286
|
// Promo discount from backend (order-level only; rates are pre-promo)
|
|
3118
3287
|
const [promoDiscountAmount, setPromoDiscountAmount] = useState(0);
|
|
@@ -3687,6 +3856,23 @@ export function AdminChangeBookingFlow({
|
|
|
3687
3856
|
providerHasAdditionalAdjustments ||
|
|
3688
3857
|
Math.abs(adminCustomAdjustmentTotal) >= 0.005;
|
|
3689
3858
|
|
|
3859
|
+
/**
|
|
3860
|
+
* True until `quoteChangeBooking` returns a confirmed `serverDisplay` for the current selection.
|
|
3861
|
+
* Named so manual price-line UI can align with “still settling final price” without touching ticket/receipt math.
|
|
3862
|
+
*/
|
|
3863
|
+
const changeFlowFinalPricePending =
|
|
3864
|
+
suppressSelfServeCurrencyUi &&
|
|
3865
|
+
selectedAvailability != null &&
|
|
3866
|
+
totalQuantity > 0 &&
|
|
3867
|
+
!selfServePricingConfirmed;
|
|
3868
|
+
|
|
3869
|
+
/**
|
|
3870
|
+
* Provider inline edits + admin custom lines: show while waiting on or after the authoritative quote.
|
|
3871
|
+
* UI-only gate — does not alter ticket line items or receipt-floor rules.
|
|
3872
|
+
*/
|
|
3873
|
+
const showChangeFlowManualPriceLines =
|
|
3874
|
+
changeFlowFinalPricePending || selfServePricingConfirmed;
|
|
3875
|
+
|
|
3690
3876
|
const displayedChangeAmountsRaw = resolveChangeFlowDisplayedAmounts({
|
|
3691
3877
|
providerPreview: providerTotalsPreview,
|
|
3692
3878
|
serverQuotePreview:
|
|
@@ -3713,25 +3899,173 @@ export function AdminChangeBookingFlow({
|
|
|
3713
3899
|
);
|
|
3714
3900
|
const displayChangeFlowTax = roundMoney(displayedChangeAmountsRaw.tax + adminTaxDeltaForExternalDisplay);
|
|
3715
3901
|
const displayChangeFlowProposedTotal = roundMoney(
|
|
3716
|
-
|
|
3902
|
+
displayLayerUsesExternalPricing
|
|
3903
|
+
? displayedChangeAmountsRaw.total + adminCustomAdjustmentTotal + adminTaxDeltaForExternalDisplay
|
|
3904
|
+
: displayedChangeAmountsRaw.total
|
|
3717
3905
|
);
|
|
3906
|
+
const displayChangeFlowProposedTotalWithEditableLines = roundMoney(
|
|
3907
|
+
displayChangeFlowProposedTotal + editableSummaryPreSubtotalDelta
|
|
3908
|
+
);
|
|
3909
|
+
const adminFeAuthoritativeReceipt = useMemo((): AdminFeAuthoritativeReceipt => {
|
|
3910
|
+
const hasTaxLine = editableCheckoutPriceSummaryLines.some(
|
|
3911
|
+
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
3912
|
+
);
|
|
3913
|
+
const lineItems = mapSummaryLinesToFeReceiptLineItems(editableCheckoutPriceSummaryLines);
|
|
3914
|
+
if (!hasTaxLine && Math.abs(displayChangeFlowTax) >= 0.0005) {
|
|
3915
|
+
lineItems.push({
|
|
3916
|
+
label: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
|
|
3917
|
+
amount: roundMoney(displayChangeFlowTax),
|
|
3918
|
+
type: 'TAX',
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
let subtotal = roundMoney(displayChangeFlowSubtotal + editableSummaryPreSubtotalDelta);
|
|
3922
|
+
const tax = roundMoney(displayChangeFlowTax);
|
|
3923
|
+
const expectedTotal = roundMoney(displayChangeFlowProposedTotalWithEditableLines);
|
|
3924
|
+
const recomputed = roundMoney(subtotal + tax);
|
|
3925
|
+
if (Math.abs(recomputed - expectedTotal) >= 0.005) {
|
|
3926
|
+
subtotal = roundMoney(expectedTotal - tax);
|
|
3927
|
+
}
|
|
3928
|
+
return {
|
|
3929
|
+
subtotal,
|
|
3930
|
+
tax,
|
|
3931
|
+
total: expectedTotal,
|
|
3932
|
+
currency,
|
|
3933
|
+
lineItems,
|
|
3934
|
+
};
|
|
3935
|
+
}, [
|
|
3936
|
+
editableCheckoutPriceSummaryLines,
|
|
3937
|
+
displayChangeFlowTax,
|
|
3938
|
+
displayChangeFlowSubtotal,
|
|
3939
|
+
editableSummaryPreSubtotalDelta,
|
|
3940
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
3941
|
+
currency,
|
|
3942
|
+
t,
|
|
3943
|
+
]);
|
|
3718
3944
|
|
|
3719
|
-
const
|
|
3945
|
+
const changeFlowClientEstimateDueBase = (() => {
|
|
3720
3946
|
if (!originalReceipt) return totalPrice;
|
|
3721
|
-
//
|
|
3947
|
+
// Self-serve quote: match BE receipt / intent: (newTotalCents − previousTotalCents) / 100.
|
|
3722
3948
|
if (isCustomerSelfServeChange && latestChangeQuote != null && !changeQuoteFetchError) {
|
|
3949
|
+
const pq = latestChangeQuote.quotePreviousTotalCents;
|
|
3950
|
+
const nq = latestChangeQuote.quoteNewTotalCents;
|
|
3951
|
+
if (pq != null && nq != null) {
|
|
3952
|
+
return normalizeNearZeroOwed(roundMoney((nq - pq) / 100));
|
|
3953
|
+
}
|
|
3954
|
+
if (latestChangeQuote.serverDisplay != null) {
|
|
3955
|
+
return normalizeNearZeroOwed(
|
|
3956
|
+
roundMoney(latestChangeQuote.serverDisplay.total - originalReceipt.total),
|
|
3957
|
+
);
|
|
3958
|
+
}
|
|
3723
3959
|
return normalizeNearZeroOwed(latestChangeQuote.priceDiff);
|
|
3724
3960
|
}
|
|
3725
3961
|
return changeFlowBalanceVsOriginal({
|
|
3726
3962
|
newTotal: displayChangeFlowProposedTotal,
|
|
3727
3963
|
originalReceiptTotal: originalReceipt.total,
|
|
3728
|
-
audience:
|
|
3964
|
+
audience: 'admin',
|
|
3729
3965
|
});
|
|
3730
3966
|
})();
|
|
3967
|
+
const changeFlowClientEstimateDue = normalizeNearZeroOwed(
|
|
3968
|
+
roundMoney(changeFlowClientEstimateDueBase + editableSummaryPreSubtotalDelta)
|
|
3969
|
+
);
|
|
3731
3970
|
|
|
3732
3971
|
const changeFlowAmountDueRaw = changeFlowClientEstimateDue;
|
|
3733
3972
|
const changeFlowAmountDue = normalizeNearZeroOwed(changeFlowAmountDueRaw);
|
|
3734
3973
|
|
|
3974
|
+
const changeFlowAdminPricingDebugPanel = useMemo(() => {
|
|
3975
|
+
if (!isAdmin || !selectedAvailability || totalQuantity <= 0) return null;
|
|
3976
|
+
const fmt = (n: number) => formatCurrencyAmount(roundMoney(n), currency, locale as 'en' | 'fr');
|
|
3977
|
+
const path = (() => {
|
|
3978
|
+
if (!originalReceipt) return 'totalPrice (no original receipt)';
|
|
3979
|
+
if (isCustomerSelfServeChange && latestChangeQuote != null && !changeQuoteFetchError) {
|
|
3980
|
+
if (
|
|
3981
|
+
latestChangeQuote.quotePreviousTotalCents != null &&
|
|
3982
|
+
latestChangeQuote.quoteNewTotalCents != null
|
|
3983
|
+
) {
|
|
3984
|
+
return 'POST /change/quote: (quoteNewTotalCents − quotePreviousTotalCents) / 100';
|
|
3985
|
+
}
|
|
3986
|
+
if (latestChangeQuote.serverDisplay != null) {
|
|
3987
|
+
return 'POST /change/quote: serverDisplay.total − originalReceipt.total';
|
|
3988
|
+
}
|
|
3989
|
+
return 'POST /change/quote: priceDiff (API balance delta)';
|
|
3990
|
+
}
|
|
3991
|
+
return 'FE: displayChangeFlowProposedTotal − originalReceipt.total (signed; used when quote not driving amount-due)';
|
|
3992
|
+
})();
|
|
3993
|
+
const quoteTotalPreview =
|
|
3994
|
+
latestChangeQuote?.serverDisplay != null ? fmt(latestChangeQuote.serverDisplay.total) : '—';
|
|
3995
|
+
return (
|
|
3996
|
+
<details className="mt-3 rounded-md border border-amber-200/80 bg-amber-50/50 p-2 text-left text-xs text-amber-950">
|
|
3997
|
+
<summary className="cursor-pointer select-none font-medium text-stone-800">
|
|
3998
|
+
Price calculation (admin debug)
|
|
3999
|
+
</summary>
|
|
4000
|
+
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed text-stone-800">
|
|
4001
|
+
{[
|
|
4002
|
+
`Amount-due path: ${path}`,
|
|
4003
|
+
`adminFeAuthoritativeQuoteEnabled: ${String(useAdminFeAuthoritativeQuote)}`,
|
|
4004
|
+
`displayLayerUsesExternalPricing: ${String(displayLayerUsesExternalPricing)} (server/provider totals omit FE admin lines until overlaid)`,
|
|
4005
|
+
`selfServePricingConfirmed: ${String(selfServePricingConfirmed)} · changeQuoteLoading: ${String(changeQuoteLoading)}`,
|
|
4006
|
+
`Cart subtotal (excl. admin custom lines): ${fmt(effectiveSubtotal)}`,
|
|
4007
|
+
`Admin custom lines (sum): ${fmt(adminCustomAdjustmentTotal)}`,
|
|
4008
|
+
`Pre-subtotal line overrides:`,
|
|
4009
|
+
...editableSummaryPreSubtotalDebugRows.map(
|
|
4010
|
+
(row, idx) =>
|
|
4011
|
+
` ${idx + 1}. ${row.label} | baseline ${fmt(row.baseline)} -> edited ${fmt(row.edited)} (delta ${fmt(row.delta)})`
|
|
4012
|
+
),
|
|
4013
|
+
`Checkout subtotal (incl. admin lines): ${fmt(effectiveSubtotalForCheckout)}`,
|
|
4014
|
+
`effectiveTax · promo discount: ${fmt(effectiveTax)} · ${fmt(effectivePromoDiscountAmount)}`,
|
|
4015
|
+
`totalPrice (subtotal + tax − promo): ${fmt(totalPrice)}`,
|
|
4016
|
+
`changeFlowNewBookingTotal (after cent reconcile vs receipt): ${fmt(changeFlowNewBookingTotal)}`,
|
|
4017
|
+
`Displayed layer subtotal / tax / total: ${fmt(displayedChangeAmountsRaw.subtotal)} / ${fmt(displayedChangeAmountsRaw.tax)} / ${fmt(displayedChangeAmountsRaw.total)}`,
|
|
4018
|
+
`Pre-subtotal editable lines delta: ${fmt(editableSummaryPreSubtotalDelta)}`,
|
|
4019
|
+
`displayChangeFlowProposedTotal (base): ${fmt(displayChangeFlowProposedTotal)}`,
|
|
4020
|
+
`displayChangeFlowProposedTotal (with editable lines): ${fmt(displayChangeFlowProposedTotalWithEditableLines)}`,
|
|
4021
|
+
`feReceipt sent subtotal/tax/total: ${fmt(adminFeAuthoritativeReceipt.subtotal)} / ${fmt(adminFeAuthoritativeReceipt.tax)} / ${fmt(adminFeAuthoritativeReceipt.total)}`,
|
|
4022
|
+
`feReceipt sent lineItems count: ${String(adminFeAuthoritativeReceipt.lineItems.length)}`,
|
|
4023
|
+
originalReceipt ? `originalReceipt.total: ${fmt(originalReceipt.total)}` : 'originalReceipt: —',
|
|
4024
|
+
`Quote serverDisplay.total (if any): ${quoteTotalPreview}`,
|
|
4025
|
+
`Quote serverDisplay subtotal/tax/total: ${
|
|
4026
|
+
latestChangeQuote?.serverDisplay
|
|
4027
|
+
? `${fmt(latestChangeQuote.serverDisplay.subtotal)} / ${fmt(latestChangeQuote.serverDisplay.tax)} / ${fmt(latestChangeQuote.serverDisplay.total)}`
|
|
4028
|
+
: '—'
|
|
4029
|
+
}`,
|
|
4030
|
+
`changeFlowClientEstimateDue (before near-zero): ${fmt(changeFlowClientEstimateDue)}`,
|
|
4031
|
+
`changeFlowAmountDue (PriceSummary total row): ${fmt(changeFlowAmountDue)}`,
|
|
4032
|
+
].join('\n')}
|
|
4033
|
+
</pre>
|
|
4034
|
+
</details>
|
|
4035
|
+
);
|
|
4036
|
+
}, [
|
|
4037
|
+
isAdmin,
|
|
4038
|
+
selectedAvailability,
|
|
4039
|
+
totalQuantity,
|
|
4040
|
+
useAdminFeAuthoritativeQuote,
|
|
4041
|
+
currency,
|
|
4042
|
+
locale,
|
|
4043
|
+
originalReceipt,
|
|
4044
|
+
isCustomerSelfServeChange,
|
|
4045
|
+
latestChangeQuote,
|
|
4046
|
+
changeQuoteFetchError,
|
|
4047
|
+
displayLayerUsesExternalPricing,
|
|
4048
|
+
selfServePricingConfirmed,
|
|
4049
|
+
changeQuoteLoading,
|
|
4050
|
+
effectiveSubtotal,
|
|
4051
|
+
adminCustomAdjustmentTotal,
|
|
4052
|
+
editableSummaryPreSubtotalDebugRows,
|
|
4053
|
+
effectiveSubtotalForCheckout,
|
|
4054
|
+
effectiveTax,
|
|
4055
|
+
effectivePromoDiscountAmount,
|
|
4056
|
+
totalPrice,
|
|
4057
|
+
changeFlowNewBookingTotal,
|
|
4058
|
+
displayedChangeAmountsRaw.subtotal,
|
|
4059
|
+
displayedChangeAmountsRaw.tax,
|
|
4060
|
+
displayedChangeAmountsRaw.total,
|
|
4061
|
+
editableSummaryPreSubtotalDelta,
|
|
4062
|
+
displayChangeFlowProposedTotal,
|
|
4063
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
4064
|
+
adminFeAuthoritativeReceipt,
|
|
4065
|
+
changeFlowClientEstimateDue,
|
|
4066
|
+
changeFlowAmountDue,
|
|
4067
|
+
]);
|
|
4068
|
+
|
|
3735
4069
|
const changeCheckoutButtonLabel = (() => {
|
|
3736
4070
|
if (!hasEffectiveChangeSelection) return undefined;
|
|
3737
4071
|
if (isProviderDashboardChange) {
|
|
@@ -3759,7 +4093,9 @@ export function AdminChangeBookingFlow({
|
|
|
3759
4093
|
const d = Math.round(changeFlowClientEstimateDue * 100) / 100;
|
|
3760
4094
|
return d > 0
|
|
3761
4095
|
? `Change booking (${formatCurrencyAmount(d, currency, locale as 'en' | 'fr')})`
|
|
3762
|
-
:
|
|
4096
|
+
: d < 0
|
|
4097
|
+
? `Change booking (${formatCurrencyAmount(d, currency, locale as 'en' | 'fr')})`
|
|
4098
|
+
: 'Change booking (no charge)';
|
|
3763
4099
|
}
|
|
3764
4100
|
const tr = t('booking.changeBooking');
|
|
3765
4101
|
return tr !== 'booking.changeBooking' ? tr : 'Change booking';
|
|
@@ -3767,7 +4103,9 @@ export function AdminChangeBookingFlow({
|
|
|
3767
4103
|
const est = Math.round(changeFlowClientEstimateDue * 100) / 100;
|
|
3768
4104
|
return est > 0
|
|
3769
4105
|
? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
|
|
3770
|
-
:
|
|
4106
|
+
: est < 0
|
|
4107
|
+
? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
|
|
4108
|
+
: 'Change booking (no charge)';
|
|
3771
4109
|
})();
|
|
3772
4110
|
/** Partner deferred-invoice path applies to {@link NewBookingFlow} only. */
|
|
3773
4111
|
const deferredInvoiceSubmitLabel = undefined;
|
|
@@ -3851,7 +4189,7 @@ export function AdminChangeBookingFlow({
|
|
|
3851
4189
|
originalReceipt
|
|
3852
4190
|
? suppressSelfServeCurrencyUi && !selfServePricingConfirmed
|
|
3853
4191
|
? null
|
|
3854
|
-
:
|
|
4192
|
+
: displayChangeFlowProposedTotalWithEditableLines
|
|
3855
4193
|
: totalPrice,
|
|
3856
4194
|
selectionCurrency: currency,
|
|
3857
4195
|
};
|
|
@@ -3865,7 +4203,7 @@ export function AdminChangeBookingFlow({
|
|
|
3865
4203
|
totalPrice,
|
|
3866
4204
|
currency,
|
|
3867
4205
|
originalReceipt,
|
|
3868
|
-
|
|
4206
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
3869
4207
|
suppressSelfServeCurrencyUi,
|
|
3870
4208
|
selfServePricingConfirmed,
|
|
3871
4209
|
]);
|
|
@@ -3910,7 +4248,7 @@ export function AdminChangeBookingFlow({
|
|
|
3910
4248
|
? displayChangeFlowTax
|
|
3911
4249
|
: effectiveTax
|
|
3912
4250
|
: 0,
|
|
3913
|
-
total: originalReceipt ?
|
|
4251
|
+
total: originalReceipt ? displayChangeFlowProposedTotalWithEditableLines : totalPrice,
|
|
3914
4252
|
currency,
|
|
3915
4253
|
});
|
|
3916
4254
|
}, [
|
|
@@ -3919,7 +4257,7 @@ export function AdminChangeBookingFlow({
|
|
|
3919
4257
|
totalQuantity,
|
|
3920
4258
|
effectiveSubtotalForCheckout,
|
|
3921
4259
|
effectiveTax,
|
|
3922
|
-
|
|
4260
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
3923
4261
|
displayChangeFlowSubtotal,
|
|
3924
4262
|
displayChangeFlowTax,
|
|
3925
4263
|
currency,
|
|
@@ -3971,7 +4309,7 @@ export function AdminChangeBookingFlow({
|
|
|
3971
4309
|
.filter(([, count]) => count > 0)
|
|
3972
4310
|
.map(([category, count]) => ({ category, count }));
|
|
3973
4311
|
try {
|
|
3974
|
-
const
|
|
4312
|
+
const quoteRequestBase = {
|
|
3975
4313
|
bookingReference: bookingReferenceForQuote,
|
|
3976
4314
|
lastName: lastName.trim(),
|
|
3977
4315
|
newProductId: optionId,
|
|
@@ -3992,7 +4330,14 @@ export function AdminChangeBookingFlow({
|
|
|
3992
4330
|
previousAvailabilityId: initialValues.availabilityId ?? null,
|
|
3993
4331
|
previousReturnAvailabilityId: initialValues.returnAvailabilityId ?? null,
|
|
3994
4332
|
},
|
|
3995
|
-
}
|
|
4333
|
+
};
|
|
4334
|
+
const quote = useAdminFeAuthoritativeQuote
|
|
4335
|
+
? await quoteChangeBookingAdminFeReceipt({
|
|
4336
|
+
...quoteRequestBase,
|
|
4337
|
+
feReceipt: adminFeAuthoritativeReceipt,
|
|
4338
|
+
feAmountDueMajorUnits: roundMoney(changeFlowClientEstimateDue),
|
|
4339
|
+
})
|
|
4340
|
+
: await quoteChangeBooking(quoteRequestBase);
|
|
3996
4341
|
if (seq !== changeQuoteRequestSeq.current) return;
|
|
3997
4342
|
const slice = sliceChangeQuoteForUi(
|
|
3998
4343
|
quote,
|
|
@@ -4040,6 +4385,7 @@ export function AdminChangeBookingFlow({
|
|
|
4040
4385
|
addOnSelections,
|
|
4041
4386
|
changeFlowInitialTicketCount,
|
|
4042
4387
|
changeFlowNewBookingTotal,
|
|
4388
|
+
changeFlowClientEstimateDue,
|
|
4043
4389
|
effectiveSubtotalForCheckout,
|
|
4044
4390
|
effectiveTax,
|
|
4045
4391
|
totalPrice,
|
|
@@ -4048,6 +4394,8 @@ export function AdminChangeBookingFlow({
|
|
|
4048
4394
|
initialValues?.availabilityId,
|
|
4049
4395
|
initialValues?.returnAvailabilityId,
|
|
4050
4396
|
adminCustomLinesAsAdditionalAdjustments,
|
|
4397
|
+
adminFeAuthoritativeReceipt,
|
|
4398
|
+
useAdminFeAuthoritativeQuote,
|
|
4051
4399
|
]);
|
|
4052
4400
|
|
|
4053
4401
|
// Auto-select product option when date is selected: most popular if set, otherwise first available.
|
|
@@ -4568,7 +4916,7 @@ export function AdminChangeBookingFlow({
|
|
|
4568
4916
|
if (!changeBookingReference || !changeLastName) {
|
|
4569
4917
|
throw new Error('Missing booking reference or last name for change quote');
|
|
4570
4918
|
}
|
|
4571
|
-
const
|
|
4919
|
+
const quoteRequestBase = {
|
|
4572
4920
|
bookingReference: changeBookingReference,
|
|
4573
4921
|
lastName: changeLastName,
|
|
4574
4922
|
newProductId: availabilityProductOptionId,
|
|
@@ -4583,7 +4931,14 @@ export function AdminChangeBookingFlow({
|
|
|
4583
4931
|
: {}),
|
|
4584
4932
|
clientProposedTotal:
|
|
4585
4933
|
latestChangeQuote?.serverDisplay?.total ?? changeFlowNewBookingTotal,
|
|
4586
|
-
}
|
|
4934
|
+
};
|
|
4935
|
+
const quote = useAdminFeAuthoritativeQuote
|
|
4936
|
+
? await quoteChangeBookingAdminFeReceipt({
|
|
4937
|
+
...quoteRequestBase,
|
|
4938
|
+
feReceipt: adminFeAuthoritativeReceipt,
|
|
4939
|
+
feAmountDueMajorUnits: roundMoney(changeFlowClientEstimateDue),
|
|
4940
|
+
})
|
|
4941
|
+
: await quoteChangeBooking(quoteRequestBase);
|
|
4587
4942
|
const quoteSlice = sliceChangeQuoteForUi(
|
|
4588
4943
|
quote,
|
|
4589
4944
|
{
|
|
@@ -4611,30 +4966,41 @@ export function AdminChangeBookingFlow({
|
|
|
4611
4966
|
quote.proposed?.total ??
|
|
4612
4967
|
quote.newReceipt?.total ??
|
|
4613
4968
|
changeFlowNewBookingTotal;
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
const
|
|
4620
|
-
|
|
4621
|
-
? Math.max(0,
|
|
4622
|
-
:
|
|
4623
|
-
|
|
4969
|
+
/** Signed proposed − previous (major units). Matches quote slices when cents present; refund owed < 0. */
|
|
4970
|
+
const signedBalanceMajor =
|
|
4971
|
+
quote.previousTotalCents != null && quote.newTotalCents != null
|
|
4972
|
+
? (quote.newTotalCents - quote.previousTotalCents) / 100
|
|
4973
|
+
: quote.balanceDeltaMajorUnits ?? null;
|
|
4974
|
+
const chargeDue =
|
|
4975
|
+
signedBalanceMajor != null
|
|
4976
|
+
? Math.max(0, signedBalanceMajor)
|
|
4977
|
+
: quote.amountDueCents != null
|
|
4978
|
+
? quote.amountDueCents / 100
|
|
4979
|
+
: Math.max(0, quote.priceDiff ?? 0);
|
|
4980
|
+
const feChangeDue =
|
|
4981
|
+
signedBalanceMajor ??
|
|
4982
|
+
changeFlowBalanceVsOriginal({
|
|
4983
|
+
newTotal: serverNewTotalForGuard,
|
|
4984
|
+
originalReceiptTotal: originalReceipt?.total ?? 0,
|
|
4985
|
+
audience: 'admin',
|
|
4986
|
+
});
|
|
4987
|
+
if (feChangeDue > 0.02 && chargeDue <= 0.009) {
|
|
4624
4988
|
throw new Error(
|
|
4625
4989
|
'This change requires payment, but the price could not be confirmed. Please refresh and try again.'
|
|
4626
4990
|
);
|
|
4627
4991
|
}
|
|
4628
|
-
// No
|
|
4629
|
-
if (
|
|
4992
|
+
// No additional charge (includes refund-owed / downgrade): confirm-free apply only when server agrees no payment due.
|
|
4993
|
+
if (chargeDue <= 0.009) {
|
|
4630
4994
|
if (feChangeDue > 0.02) {
|
|
4631
4995
|
throw new Error(
|
|
4632
4996
|
'This change requires payment, but the price could not be confirmed. Please refresh and try again.'
|
|
4633
4997
|
);
|
|
4634
4998
|
}
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
4999
|
+
const upgradeWithoutCharge =
|
|
5000
|
+
quote.newTotalCents != null &&
|
|
5001
|
+
quote.previousTotalCents != null &&
|
|
5002
|
+
quote.newTotalCents > quote.previousTotalCents + 1;
|
|
5003
|
+
if (upgradeWithoutCharge) {
|
|
4638
5004
|
throw new Error(
|
|
4639
5005
|
'This change requires payment, but the price could not be confirmed. Please refresh and try again.'
|
|
4640
5006
|
);
|
|
@@ -4686,11 +5052,14 @@ export function AdminChangeBookingFlow({
|
|
|
4686
5052
|
// Backend will charge totalAmount and store this as the receipt so /manage matches.
|
|
4687
5053
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
4688
5054
|
const amountDueForCheckout = isCustomerSelfServeChange
|
|
4689
|
-
?
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
5055
|
+
? Math.max(
|
|
5056
|
+
0,
|
|
5057
|
+
changeFlowBalanceVsOriginal({
|
|
5058
|
+
newTotal: changeFlowNewBookingTotal,
|
|
5059
|
+
originalReceiptTotal: originalReceipt?.total ?? 0,
|
|
5060
|
+
audience: 'admin',
|
|
5061
|
+
}),
|
|
5062
|
+
)
|
|
4694
5063
|
: totalPrice;
|
|
4695
5064
|
const lines = [
|
|
4696
5065
|
...ticketLineItemsForChangeFlowDisplay.map((line) => ({
|
|
@@ -4923,7 +5292,7 @@ export function AdminChangeBookingFlow({
|
|
|
4923
5292
|
isCustomerSelfServeChange && originalReceipt
|
|
4924
5293
|
? {
|
|
4925
5294
|
previousTotal: originalReceipt.total,
|
|
4926
|
-
newTotal:
|
|
5295
|
+
newTotal: displayChangeFlowProposedTotalWithEditableLines,
|
|
4927
5296
|
differenceTotal: amountDueForCheckout,
|
|
4928
5297
|
}
|
|
4929
5298
|
: undefined,
|
|
@@ -5029,7 +5398,7 @@ export function AdminChangeBookingFlow({
|
|
|
5029
5398
|
addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
|
|
5030
5399
|
cancellationPolicyId: cancellationPolicyId ?? initialValues?.cancellationPolicyId ?? null,
|
|
5031
5400
|
promoCode: appliedPromoCode ?? null,
|
|
5032
|
-
newTotalAmount:
|
|
5401
|
+
newTotalAmount: displayChangeFlowProposedTotalWithEditableLines,
|
|
5033
5402
|
additionalHoursCount: null,
|
|
5034
5403
|
pricingAdjustment:
|
|
5035
5404
|
providerPricingOverrides.length > 0 || mergedProviderAdditionalAdjustments.length > 0
|
|
@@ -5402,14 +5771,16 @@ export function AdminChangeBookingFlow({
|
|
|
5402
5771
|
{selectedAvailability && (
|
|
5403
5772
|
<>
|
|
5404
5773
|
<CheckoutForm
|
|
5405
|
-
priceSummaryLines={
|
|
5774
|
+
priceSummaryLines={editableCheckoutPriceSummaryLines}
|
|
5406
5775
|
replacePriceSummary={selfServeCheckoutPlaceholder}
|
|
5407
5776
|
totalPrice={changeFlowAmountDue}
|
|
5408
5777
|
totalSummaryLabel={
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5778
|
+
changeFlowAmountDue < -0.005
|
|
5779
|
+
? 'Refund owed (vs original booking)'
|
|
5780
|
+
: t('booking.totalOwedForBookingChange') &&
|
|
5781
|
+
t('booking.totalOwedForBookingChange') !== 'booking.totalOwedForBookingChange'
|
|
5782
|
+
? t('booking.totalOwedForBookingChange')
|
|
5783
|
+
: 'Total owed for booking difference'
|
|
5413
5784
|
}
|
|
5414
5785
|
subtotal={displayChangeFlowSubtotal}
|
|
5415
5786
|
taxAmount={
|
|
@@ -5438,9 +5809,11 @@ export function AdminChangeBookingFlow({
|
|
|
5438
5809
|
!providerPricingUi.error ? (
|
|
5439
5810
|
<div className="mt-2 text-xs text-stone-500">{providerPricingUi.helperText}</div>
|
|
5440
5811
|
) : null}
|
|
5812
|
+
{changeFlowAdminPricingDebugPanel}
|
|
5441
5813
|
</>
|
|
5442
5814
|
}
|
|
5443
5815
|
extraBeforeSubtotal={
|
|
5816
|
+
showChangeFlowManualPriceLines ? (
|
|
5444
5817
|
<>
|
|
5445
5818
|
{showProviderPricingInlineEditor && (providerPricingUi?.additionalAdjustments?.length ?? 0) > 0 ? (
|
|
5446
5819
|
<div className="space-y-1">
|
|
@@ -5623,6 +5996,7 @@ export function AdminChangeBookingFlow({
|
|
|
5623
5996
|
})}
|
|
5624
5997
|
</div>
|
|
5625
5998
|
</>
|
|
5999
|
+
) : null
|
|
5626
6000
|
}
|
|
5627
6001
|
firstName={firstName}
|
|
5628
6002
|
lastName={lastName}
|
|
@@ -5686,16 +6060,49 @@ export function AdminChangeBookingFlow({
|
|
|
5686
6060
|
attributionConfirmLabel={flowUi?.partnerAttributionConfirmLabel}
|
|
5687
6061
|
attributionConfirmed={partnerAttributionConfirmed}
|
|
5688
6062
|
onAttributionConfirmedChange={setPartnerAttributionConfirmed}
|
|
5689
|
-
lineAmountInputs={
|
|
5690
|
-
onLineAmountInputChange={
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
6063
|
+
lineAmountInputs={editableSummaryLineAmountInputs}
|
|
6064
|
+
onLineAmountInputChange={(lineKey, value) => {
|
|
6065
|
+
const sanitized = value.replace(/[^0-9.-]/g, '');
|
|
6066
|
+
setEditableSummaryLineAmountInputs((prev) => ({ ...prev, [lineKey]: sanitized }));
|
|
6067
|
+
if (showProviderPricingInlineEditor) {
|
|
6068
|
+
providerPricingUi?.onLineAmountInputChange?.(lineKey, sanitized);
|
|
6069
|
+
}
|
|
6070
|
+
}}
|
|
6071
|
+
onLineAmountInputBlur={(lineKey) => {
|
|
6072
|
+
setEditableSummaryLineAmountInputs((prev) => {
|
|
6073
|
+
const raw = prev[lineKey] ?? '';
|
|
6074
|
+
if (raw.trim() === '' || raw === '-' || raw === '.'
|
|
6075
|
+
|| raw === '-.') {
|
|
6076
|
+
return prev;
|
|
6077
|
+
}
|
|
6078
|
+
const parsed = Number(raw);
|
|
6079
|
+
if (!Number.isFinite(parsed)) return prev;
|
|
6080
|
+
const normalized = roundMoney(parsed).toFixed(2);
|
|
6081
|
+
if (normalized === raw) return prev;
|
|
6082
|
+
return { ...prev, [lineKey]: normalized };
|
|
6083
|
+
});
|
|
6084
|
+
if (showProviderPricingInlineEditor) {
|
|
6085
|
+
providerPricingUi?.onLineAmountInputBlur?.(lineKey);
|
|
6086
|
+
}
|
|
6087
|
+
}}
|
|
6088
|
+
onLineAmountReset={(lineKey) => {
|
|
6089
|
+
const original = checkoutPriceSummaryLinesForCheckout.find(
|
|
6090
|
+
(line, index) => getEditableSummaryLineKey(line, index) === lineKey
|
|
6091
|
+
);
|
|
6092
|
+
if (!original) return;
|
|
6093
|
+
const nextValue =
|
|
6094
|
+
original.kind === 'ticket'
|
|
6095
|
+
? String(roundMoney(original.itemTotal))
|
|
6096
|
+
: String(roundMoney(original.amount));
|
|
6097
|
+
setEditableSummaryLineAmountInputs((prev) => ({ ...prev, [lineKey]: nextValue }));
|
|
6098
|
+
if (showProviderPricingInlineEditor) {
|
|
6099
|
+
providerPricingUi?.onLineAmountReset?.(lineKey);
|
|
6100
|
+
}
|
|
6101
|
+
}}
|
|
6102
|
+
lineLabelInputs={editableSummaryLineLabelInputs}
|
|
6103
|
+
onLineLabelInputChange={(lineKey, value) => {
|
|
6104
|
+
setEditableSummaryLineLabelInputs((prev) => ({ ...prev, [lineKey]: value }));
|
|
6105
|
+
}}
|
|
5699
6106
|
/>
|
|
5700
6107
|
</>
|
|
5701
6108
|
)}
|
|
@@ -32,6 +32,9 @@ interface CheckoutFormProps {
|
|
|
32
32
|
onLineAmountInputChange?: (lineKey: string, value: string) => void;
|
|
33
33
|
onLineAmountInputBlur?: (lineKey: string) => void;
|
|
34
34
|
onLineAmountReset?: (lineKey: string) => void;
|
|
35
|
+
/** Optional map + handler for inline editable labels in PriceSummary. */
|
|
36
|
+
lineLabelInputs?: Record<string, string>;
|
|
37
|
+
onLineLabelInputChange?: (lineKey: string, value: string) => void;
|
|
35
38
|
extraBeforeSubtotal?: ReactNode;
|
|
36
39
|
// Promo (passed as extraBetweenTaxAndTotal content - we'll keep it in parent for now)
|
|
37
40
|
// Contact info
|
|
@@ -95,6 +98,8 @@ export function CheckoutForm({
|
|
|
95
98
|
onLineAmountInputChange,
|
|
96
99
|
onLineAmountInputBlur,
|
|
97
100
|
onLineAmountReset,
|
|
101
|
+
lineLabelInputs,
|
|
102
|
+
onLineLabelInputChange,
|
|
98
103
|
extraBeforeSubtotal,
|
|
99
104
|
firstName,
|
|
100
105
|
lastName,
|
|
@@ -146,6 +151,7 @@ export function CheckoutForm({
|
|
|
146
151
|
<div className={styles.summaryWrapper}>
|
|
147
152
|
{replacePriceSummary ? (
|
|
148
153
|
<>
|
|
154
|
+
{extraBeforeSubtotal ? <div className="mb-4 min-w-0">{extraBeforeSubtotal}</div> : null}
|
|
149
155
|
{replacePriceSummary}
|
|
150
156
|
<div className="mt-4">{extraBetweenTaxAndTotal}</div>
|
|
151
157
|
</>
|
|
@@ -167,6 +173,8 @@ export function CheckoutForm({
|
|
|
167
173
|
onLineAmountInputChange={onLineAmountInputChange}
|
|
168
174
|
onLineAmountInputBlur={onLineAmountInputBlur}
|
|
169
175
|
onLineAmountReset={onLineAmountReset}
|
|
176
|
+
lineLabelInputs={lineLabelInputs}
|
|
177
|
+
onLineLabelInputChange={onLineLabelInputChange}
|
|
170
178
|
extraBeforeSubtotal={extraBeforeSubtotal}
|
|
171
179
|
/>
|
|
172
180
|
)}
|
|
@@ -20,6 +20,8 @@ export interface PriceBreakdownProps {
|
|
|
20
20
|
onEditableChange?: (value: string) => void;
|
|
21
21
|
onEditableBlur?: () => void;
|
|
22
22
|
onEditableReset?: () => void;
|
|
23
|
+
editableLabelValue?: string;
|
|
24
|
+
onEditableLabelChange?: (value: string) => void;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -65,6 +67,8 @@ export function PriceBreakdown({
|
|
|
65
67
|
onEditableChange,
|
|
66
68
|
onEditableBlur,
|
|
67
69
|
onEditableReset,
|
|
70
|
+
editableLabelValue,
|
|
71
|
+
onEditableLabelChange,
|
|
68
72
|
}: PriceBreakdownProps) {
|
|
69
73
|
const [showTooltip, setShowTooltip] = useState(false);
|
|
70
74
|
const { t } = useTranslations();
|
|
@@ -81,9 +85,19 @@ export function PriceBreakdown({
|
|
|
81
85
|
if (!breakdown) {
|
|
82
86
|
return (
|
|
83
87
|
<div className="flex items-center justify-between">
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
{editable && onEditableLabelChange ? (
|
|
89
|
+
<input
|
|
90
|
+
type="text"
|
|
91
|
+
className="h-7 min-w-[10rem] max-w-[16rem] rounded border border-stone-300 bg-white px-2 text-sm text-stone-700"
|
|
92
|
+
value={editableLabelValue ?? category}
|
|
93
|
+
onChange={(e) => onEditableLabelChange(e.target.value)}
|
|
94
|
+
aria-label={`Edit ${category} label`}
|
|
95
|
+
/>
|
|
96
|
+
) : (
|
|
97
|
+
<span className="text-sm text-stone-600">
|
|
98
|
+
{category} {qty > 1 ? `× ${qty}` : ''}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
87
101
|
{editable && onEditableChange ? (
|
|
88
102
|
<div className="flex items-center gap-1">
|
|
89
103
|
{onEditableReset ? (
|
|
@@ -118,9 +132,19 @@ export function PriceBreakdown({
|
|
|
118
132
|
|
|
119
133
|
return (
|
|
120
134
|
<div className="flex items-center justify-between gap-3 min-w-0">
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
{editable && onEditableLabelChange ? (
|
|
136
|
+
<input
|
|
137
|
+
type="text"
|
|
138
|
+
className="h-7 min-w-[10rem] max-w-[16rem] rounded border border-stone-300 bg-white px-2 text-sm text-stone-700"
|
|
139
|
+
value={editableLabelValue ?? category}
|
|
140
|
+
onChange={(e) => onEditableLabelChange(e.target.value)}
|
|
141
|
+
aria-label={`Edit ${category} label`}
|
|
142
|
+
/>
|
|
143
|
+
) : (
|
|
144
|
+
<span className="text-sm text-stone-600 min-w-0 truncate">
|
|
145
|
+
{category} {qty > 1 ? `× ${qty}` : ''}
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
124
148
|
<div className="relative flex-shrink-0 whitespace-nowrap">
|
|
125
149
|
{editable && onEditableChange ? (
|
|
126
150
|
<div className="flex items-center gap-1">
|
|
@@ -73,8 +73,12 @@ export interface PriceSummaryProps {
|
|
|
73
73
|
extraAfterTotal?: ReactNode;
|
|
74
74
|
/** Optional map of editable line input values by line key. */
|
|
75
75
|
lineAmountInputs?: Record<string, string>;
|
|
76
|
+
/** Optional map of editable line label values by line key. */
|
|
77
|
+
lineLabelInputs?: Record<string, string>;
|
|
76
78
|
/** Optional change handler for inline editable line amounts. */
|
|
77
79
|
onLineAmountInputChange?: (lineKey: string, value: string) => void;
|
|
80
|
+
/** Optional change handler for inline editable line labels. */
|
|
81
|
+
onLineLabelInputChange?: (lineKey: string, value: string) => void;
|
|
78
82
|
/** Optional blur handler for enforcing display format (e.g. 2 decimals). */
|
|
79
83
|
onLineAmountInputBlur?: (lineKey: string) => void;
|
|
80
84
|
/** Optional reset handler for inline editable line amounts. */
|
|
@@ -134,7 +138,9 @@ export function PriceSummary({
|
|
|
134
138
|
totalLabel,
|
|
135
139
|
extraAfterTotal,
|
|
136
140
|
lineAmountInputs,
|
|
141
|
+
lineLabelInputs,
|
|
137
142
|
onLineAmountInputChange,
|
|
143
|
+
onLineLabelInputChange,
|
|
138
144
|
onLineAmountInputBlur,
|
|
139
145
|
onLineAmountReset,
|
|
140
146
|
}: PriceSummaryProps) {
|
|
@@ -161,6 +167,7 @@ export function PriceSummary({
|
|
|
161
167
|
return (
|
|
162
168
|
<div className={`space-y-2 min-w-0 ${className}`}>
|
|
163
169
|
{lines.map((row, index) => {
|
|
170
|
+
const isBeforeSubtotalBoundary = firstTaxLineIndex < 0 || index < firstTaxLineIndex;
|
|
164
171
|
if (row.kind === 'ticket') {
|
|
165
172
|
return (
|
|
166
173
|
<PriceBreakdown
|
|
@@ -171,23 +178,29 @@ export function PriceSummary({
|
|
|
171
178
|
breakdown={row.breakdown ?? null}
|
|
172
179
|
currency={currency}
|
|
173
180
|
locale={locale}
|
|
174
|
-
editable={row.editable}
|
|
181
|
+
editable={Boolean(row.editable && isBeforeSubtotalBoundary)}
|
|
175
182
|
editableValue={row.lineKey ? lineAmountInputs?.[row.lineKey] : undefined}
|
|
176
183
|
onEditableChange={
|
|
177
|
-
row.editable && row.lineKey && onLineAmountInputChange
|
|
184
|
+
row.editable && isBeforeSubtotalBoundary && row.lineKey && onLineAmountInputChange
|
|
178
185
|
? (value) => onLineAmountInputChange(row.lineKey!, value)
|
|
179
186
|
: undefined
|
|
180
187
|
}
|
|
181
188
|
onEditableBlur={
|
|
182
|
-
row.editable && row.lineKey && onLineAmountInputBlur
|
|
189
|
+
row.editable && isBeforeSubtotalBoundary && row.lineKey && onLineAmountInputBlur
|
|
183
190
|
? () => onLineAmountInputBlur(row.lineKey!)
|
|
184
191
|
: undefined
|
|
185
192
|
}
|
|
186
193
|
onEditableReset={
|
|
187
|
-
row.editable && row.lineKey && onLineAmountReset
|
|
194
|
+
row.editable && isBeforeSubtotalBoundary && row.lineKey && onLineAmountReset
|
|
188
195
|
? () => onLineAmountReset(row.lineKey!)
|
|
189
196
|
: undefined
|
|
190
197
|
}
|
|
198
|
+
editableLabelValue={row.lineKey ? lineLabelInputs?.[row.lineKey] : undefined}
|
|
199
|
+
onEditableLabelChange={
|
|
200
|
+
row.editable && isBeforeSubtotalBoundary && row.lineKey && onLineLabelInputChange
|
|
201
|
+
? (value) => onLineLabelInputChange(row.lineKey!, value)
|
|
202
|
+
: undefined
|
|
203
|
+
}
|
|
191
204
|
/>
|
|
192
205
|
);
|
|
193
206
|
}
|
|
@@ -209,10 +222,20 @@ export function PriceSummary({
|
|
|
209
222
|
)}
|
|
210
223
|
<div className={`flex justify-between gap-3 min-w-0 ${textSize}`}>
|
|
211
224
|
<span className="text-stone-600 min-w-0 flex items-center gap-1">
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
{editable && isBeforeSubtotalBoundary && lineKey && onLineLabelInputChange ? (
|
|
226
|
+
<input
|
|
227
|
+
type="text"
|
|
228
|
+
className="h-7 min-w-[10rem] max-w-[16rem] rounded border border-stone-300 bg-white px-2 text-sm text-stone-700"
|
|
229
|
+
value={lineLabelInputs?.[lineKey] ?? label}
|
|
230
|
+
onChange={(e) => onLineLabelInputChange(lineKey, e.target.value)}
|
|
231
|
+
aria-label={`Edit ${label} label`}
|
|
232
|
+
/>
|
|
233
|
+
) : (
|
|
234
|
+
<span className="min-w-0 truncate">
|
|
235
|
+
{label}
|
|
236
|
+
{type === 'TICKET' && quantity != null && quantity > 1 ? ` (x${quantity})` : ''}
|
|
237
|
+
</span>
|
|
238
|
+
)}
|
|
216
239
|
{tooltip && <InfoTooltip text={tooltip} />}
|
|
217
240
|
</span>
|
|
218
241
|
{editable && lineKey && onLineAmountInputChange ? (
|
|
@@ -65,6 +65,11 @@ export interface BookingFlowUiOptions {
|
|
|
65
65
|
itineraryStickyTopOffsetPx?: number;
|
|
66
66
|
/** Provider dashboard change flow: quote/override panel shown immediately before confirm CTA. */
|
|
67
67
|
providerDashboardChangePricingUi?: ProviderDashboardChangePricingUi;
|
|
68
|
+
/**
|
|
69
|
+
* Admin/provider-dashboard change flow: when true, use the admin FE-authoritative quote API.
|
|
70
|
+
* This is a rollout flag; public self-serve must stay on the existing public quote endpoint.
|
|
71
|
+
*/
|
|
72
|
+
adminFeAuthoritativeQuoteEnabled?: boolean;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
/**
|
|
@@ -178,16 +178,16 @@ export function resolveChangeFlowNewBookingTotal(input: {
|
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
180
|
* Product: **What the customer owes** for the difference is `max(0, newTotal − oldReceipt)`.
|
|
181
|
-
* **Provider dashboard** may show a signed delta (
|
|
181
|
+
* **Provider dashboard** and **admin change booking** may show a signed delta (refund owed as negative).
|
|
182
182
|
*/
|
|
183
183
|
export function changeFlowBalanceVsOriginal(input: {
|
|
184
184
|
newTotal: number;
|
|
185
185
|
originalReceiptTotal: number;
|
|
186
|
-
/** `customer` = self-serve
|
|
187
|
-
audience: 'customer' | 'provider';
|
|
186
|
+
/** `customer` = self-serve change only; `provider` | `admin` = signed newTotal − original receipt total. */
|
|
187
|
+
audience: 'customer' | 'provider' | 'admin';
|
|
188
188
|
}): number {
|
|
189
189
|
const delta = input.newTotal - input.originalReceiptTotal;
|
|
190
|
-
return input.audience === '
|
|
190
|
+
return input.audience === 'customer' ? Math.max(0, delta) : delta;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/**
|
|
@@ -255,8 +255,13 @@ export function sliceChangeQuoteForUi(
|
|
|
255
255
|
fallbackCart: { total: number; subtotal: number; tax: number },
|
|
256
256
|
currencyFallback: string
|
|
257
257
|
): ChangeQuoteUiSlice {
|
|
258
|
+
/** Staff JWT on quote: signed new − previous (refund owed negative). Otherwise same as legacy nonnegative amount-due. */
|
|
258
259
|
const priceDiff =
|
|
259
|
-
quote.
|
|
260
|
+
quote.balanceDeltaMajorUnits != null
|
|
261
|
+
? quote.balanceDeltaMajorUnits
|
|
262
|
+
: quote.amountDueCents != null
|
|
263
|
+
? quote.amountDueCents / 100
|
|
264
|
+
: quote.priceDiff ?? 0;
|
|
260
265
|
const serverDisplay = serverTotalsFromChangeQuoteResponse(quote, fallbackCart);
|
|
261
266
|
return {
|
|
262
267
|
priceDiff,
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -870,6 +870,30 @@ export interface ChangeBookingQuoteRequest {
|
|
|
870
870
|
} | null;
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
+
/** FE-authored receipt payload for admin quote path (major units, booking currency). */
|
|
874
|
+
export interface AdminFeAuthoritativeReceipt {
|
|
875
|
+
subtotal: number;
|
|
876
|
+
tax: number;
|
|
877
|
+
total: number;
|
|
878
|
+
currency?: string;
|
|
879
|
+
lineItems: Array<{
|
|
880
|
+
label?: string;
|
|
881
|
+
amount?: number;
|
|
882
|
+
type?: string;
|
|
883
|
+
quantity?: number;
|
|
884
|
+
}>;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Admin quote path where FE is authoritative for receipt lines/totals.
|
|
889
|
+
* Mirrors {@link ChangeBookingQuoteRequest} selection fields and adds a full receipt payload.
|
|
890
|
+
*/
|
|
891
|
+
export interface AdminChangeBookingQuoteRequest extends ChangeBookingQuoteRequest {
|
|
892
|
+
feReceipt: AdminFeAuthoritativeReceipt;
|
|
893
|
+
/** Optional explicit signed delta shown by FE (new total − original total), major units. */
|
|
894
|
+
feAmountDueMajorUnits?: number;
|
|
895
|
+
}
|
|
896
|
+
|
|
873
897
|
export interface ChangeBookingQuoteReceipt {
|
|
874
898
|
subtotal?: number;
|
|
875
899
|
tax?: number;
|
|
@@ -987,6 +1011,11 @@ export interface ChangeBookingQuoteResponse {
|
|
|
987
1011
|
originalReceipt?: ChangeBookingQuoteReceipt;
|
|
988
1012
|
newReceipt?: ChangeBookingQuoteReceipt;
|
|
989
1013
|
priceDiff: number;
|
|
1014
|
+
/**
|
|
1015
|
+
* Present when the quote was requested with a **staff** JWT: signed (new total − previous receipt total) in major units.
|
|
1016
|
+
* Negative = refund owed to the guest. Omitted for unauthenticated / customer-only quotes.
|
|
1017
|
+
*/
|
|
1018
|
+
balanceDeltaMajorUnits?: number;
|
|
990
1019
|
currency?: string;
|
|
991
1020
|
canProceed?: boolean;
|
|
992
1021
|
reasonIfBlocked?: string;
|
|
@@ -1103,6 +1132,31 @@ export async function quoteChangeBooking(
|
|
|
1103
1132
|
data) as ChangeBookingQuoteResponse;
|
|
1104
1133
|
}
|
|
1105
1134
|
|
|
1135
|
+
/** Admin-only quote endpoint: FE-authored receipt totals + line items are authoritative. */
|
|
1136
|
+
export async function quoteChangeBookingAdminFeReceipt(
|
|
1137
|
+
request: AdminChangeBookingQuoteRequest
|
|
1138
|
+
): Promise<ChangeBookingQuoteResponse> {
|
|
1139
|
+
const { bookingReference, lastName, ...payload } = request;
|
|
1140
|
+
const res = await fetch(
|
|
1141
|
+
`${API_BASE}/1/admin/bookings/${encodeURIComponent(bookingReference)}/change/quote?lastName=${encodeURIComponent(lastName)}`,
|
|
1142
|
+
{
|
|
1143
|
+
method: 'POST',
|
|
1144
|
+
headers: getAuthHeaders(),
|
|
1145
|
+
body: JSON.stringify(payload),
|
|
1146
|
+
}
|
|
1147
|
+
);
|
|
1148
|
+
if (!res.ok) {
|
|
1149
|
+
const err = await parseJsonSafely(res);
|
|
1150
|
+
const message = isApiErrorPayload(err)
|
|
1151
|
+
? err.errorMessage || err.error || 'Failed to quote admin booking change'
|
|
1152
|
+
: 'Failed to quote admin booking change';
|
|
1153
|
+
throw new Error(message);
|
|
1154
|
+
}
|
|
1155
|
+
const data = await parseJsonSafely(res);
|
|
1156
|
+
return ((data as { data?: ChangeBookingQuoteResponse } | null)?.data ??
|
|
1157
|
+
data) as ChangeBookingQuoteResponse;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1106
1160
|
export async function createChangeBookingPaymentIntent(
|
|
1107
1161
|
changeIntentId: string
|
|
1108
1162
|
): Promise<CreateChangePaymentIntentResponse> {
|