@ticketboothapp/booking 1.2.76 → 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 +296 -23
- package/src/components/booking/CheckoutForm.tsx +7 -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-api.ts +49 -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,
|
|
@@ -148,6 +150,26 @@ function omitZeroAmountPromoDiscountSummaryLines(lines: PriceSummaryLine[]): Pri
|
|
|
148
150
|
});
|
|
149
151
|
}
|
|
150
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
|
+
|
|
151
173
|
function formatTicketLineItemsForSummary(lines: Array<{ category: string; qty: number }>): string {
|
|
152
174
|
const labels: Record<string, string> = {
|
|
153
175
|
ADULT: 'adult',
|
|
@@ -733,6 +755,8 @@ export function AdminChangeBookingFlow({
|
|
|
733
755
|
const [adminCustomReceiptLines, setAdminCustomReceiptLines] = useState<
|
|
734
756
|
Array<{ id: string; label: string; amountInput: string; amountSign: 1 | -1 }>
|
|
735
757
|
>([]);
|
|
758
|
+
const [editableSummaryLineAmountInputs, setEditableSummaryLineAmountInputs] = useState<Record<string, string>>({});
|
|
759
|
+
const [editableSummaryLineLabelInputs, setEditableSummaryLineLabelInputs] = useState<Record<string, string>>({});
|
|
736
760
|
const adminCustomLineIdRef = useRef(0);
|
|
737
761
|
|
|
738
762
|
// Auto-apply promo code when parent page passes one (e.g. partner pages).
|
|
@@ -831,6 +855,10 @@ export function AdminChangeBookingFlow({
|
|
|
831
855
|
}
|
|
832
856
|
}, [initialValues?.dateTime, companyTimezone]);
|
|
833
857
|
const isProviderDashboardChange = Boolean(onChangeBooking);
|
|
858
|
+
const useAdminFeAuthoritativeQuote =
|
|
859
|
+
isAdmin &&
|
|
860
|
+
isProviderDashboardChange &&
|
|
861
|
+
Boolean(flowUi?.adminFeAuthoritativeQuoteEnabled);
|
|
834
862
|
/** Any change from an existing booking (public or provider). */
|
|
835
863
|
const isChangeBookingContext = Boolean(initialValues?.bookingReference?.trim());
|
|
836
864
|
/**
|
|
@@ -3120,6 +3148,140 @@ export function AdminChangeBookingFlow({
|
|
|
3120
3148
|
),
|
|
3121
3149
|
[checkoutPriceSummaryLinesForCheckout],
|
|
3122
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
|
+
]);
|
|
3123
3285
|
|
|
3124
3286
|
// Promo discount from backend (order-level only; rates are pre-promo)
|
|
3125
3287
|
const [promoDiscountAmount, setPromoDiscountAmount] = useState(0);
|
|
@@ -3741,8 +3903,46 @@ export function AdminChangeBookingFlow({
|
|
|
3741
3903
|
? displayedChangeAmountsRaw.total + adminCustomAdjustmentTotal + adminTaxDeltaForExternalDisplay
|
|
3742
3904
|
: displayedChangeAmountsRaw.total
|
|
3743
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
|
+
]);
|
|
3744
3944
|
|
|
3745
|
-
const
|
|
3945
|
+
const changeFlowClientEstimateDueBase = (() => {
|
|
3746
3946
|
if (!originalReceipt) return totalPrice;
|
|
3747
3947
|
// Self-serve quote: match BE receipt / intent: (newTotalCents − previousTotalCents) / 100.
|
|
3748
3948
|
if (isCustomerSelfServeChange && latestChangeQuote != null && !changeQuoteFetchError) {
|
|
@@ -3764,6 +3964,9 @@ export function AdminChangeBookingFlow({
|
|
|
3764
3964
|
audience: 'admin',
|
|
3765
3965
|
});
|
|
3766
3966
|
})();
|
|
3967
|
+
const changeFlowClientEstimateDue = normalizeNearZeroOwed(
|
|
3968
|
+
roundMoney(changeFlowClientEstimateDueBase + editableSummaryPreSubtotalDelta)
|
|
3969
|
+
);
|
|
3767
3970
|
|
|
3768
3971
|
const changeFlowAmountDueRaw = changeFlowClientEstimateDue;
|
|
3769
3972
|
const changeFlowAmountDue = normalizeNearZeroOwed(changeFlowAmountDueRaw);
|
|
@@ -3797,18 +4000,33 @@ export function AdminChangeBookingFlow({
|
|
|
3797
4000
|
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed text-stone-800">
|
|
3798
4001
|
{[
|
|
3799
4002
|
`Amount-due path: ${path}`,
|
|
4003
|
+
`adminFeAuthoritativeQuoteEnabled: ${String(useAdminFeAuthoritativeQuote)}`,
|
|
3800
4004
|
`displayLayerUsesExternalPricing: ${String(displayLayerUsesExternalPricing)} (server/provider totals omit FE admin lines until overlaid)`,
|
|
3801
4005
|
`selfServePricingConfirmed: ${String(selfServePricingConfirmed)} · changeQuoteLoading: ${String(changeQuoteLoading)}`,
|
|
3802
4006
|
`Cart subtotal (excl. admin custom lines): ${fmt(effectiveSubtotal)}`,
|
|
3803
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
|
+
),
|
|
3804
4013
|
`Checkout subtotal (incl. admin lines): ${fmt(effectiveSubtotalForCheckout)}`,
|
|
3805
4014
|
`effectiveTax · promo discount: ${fmt(effectiveTax)} · ${fmt(effectivePromoDiscountAmount)}`,
|
|
3806
4015
|
`totalPrice (subtotal + tax − promo): ${fmt(totalPrice)}`,
|
|
3807
4016
|
`changeFlowNewBookingTotal (after cent reconcile vs receipt): ${fmt(changeFlowNewBookingTotal)}`,
|
|
3808
4017
|
`Displayed layer subtotal / tax / total: ${fmt(displayedChangeAmountsRaw.subtotal)} / ${fmt(displayedChangeAmountsRaw.tax)} / ${fmt(displayedChangeAmountsRaw.total)}`,
|
|
3809
|
-
`
|
|
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)}`,
|
|
3810
4023
|
originalReceipt ? `originalReceipt.total: ${fmt(originalReceipt.total)}` : 'originalReceipt: —',
|
|
3811
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
|
+
}`,
|
|
3812
4030
|
`changeFlowClientEstimateDue (before near-zero): ${fmt(changeFlowClientEstimateDue)}`,
|
|
3813
4031
|
`changeFlowAmountDue (PriceSummary total row): ${fmt(changeFlowAmountDue)}`,
|
|
3814
4032
|
].join('\n')}
|
|
@@ -3819,6 +4037,7 @@ export function AdminChangeBookingFlow({
|
|
|
3819
4037
|
isAdmin,
|
|
3820
4038
|
selectedAvailability,
|
|
3821
4039
|
totalQuantity,
|
|
4040
|
+
useAdminFeAuthoritativeQuote,
|
|
3822
4041
|
currency,
|
|
3823
4042
|
locale,
|
|
3824
4043
|
originalReceipt,
|
|
@@ -3830,6 +4049,7 @@ export function AdminChangeBookingFlow({
|
|
|
3830
4049
|
changeQuoteLoading,
|
|
3831
4050
|
effectiveSubtotal,
|
|
3832
4051
|
adminCustomAdjustmentTotal,
|
|
4052
|
+
editableSummaryPreSubtotalDebugRows,
|
|
3833
4053
|
effectiveSubtotalForCheckout,
|
|
3834
4054
|
effectiveTax,
|
|
3835
4055
|
effectivePromoDiscountAmount,
|
|
@@ -3838,7 +4058,10 @@ export function AdminChangeBookingFlow({
|
|
|
3838
4058
|
displayedChangeAmountsRaw.subtotal,
|
|
3839
4059
|
displayedChangeAmountsRaw.tax,
|
|
3840
4060
|
displayedChangeAmountsRaw.total,
|
|
4061
|
+
editableSummaryPreSubtotalDelta,
|
|
3841
4062
|
displayChangeFlowProposedTotal,
|
|
4063
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
4064
|
+
adminFeAuthoritativeReceipt,
|
|
3842
4065
|
changeFlowClientEstimateDue,
|
|
3843
4066
|
changeFlowAmountDue,
|
|
3844
4067
|
]);
|
|
@@ -3966,7 +4189,7 @@ export function AdminChangeBookingFlow({
|
|
|
3966
4189
|
originalReceipt
|
|
3967
4190
|
? suppressSelfServeCurrencyUi && !selfServePricingConfirmed
|
|
3968
4191
|
? null
|
|
3969
|
-
:
|
|
4192
|
+
: displayChangeFlowProposedTotalWithEditableLines
|
|
3970
4193
|
: totalPrice,
|
|
3971
4194
|
selectionCurrency: currency,
|
|
3972
4195
|
};
|
|
@@ -3980,7 +4203,7 @@ export function AdminChangeBookingFlow({
|
|
|
3980
4203
|
totalPrice,
|
|
3981
4204
|
currency,
|
|
3982
4205
|
originalReceipt,
|
|
3983
|
-
|
|
4206
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
3984
4207
|
suppressSelfServeCurrencyUi,
|
|
3985
4208
|
selfServePricingConfirmed,
|
|
3986
4209
|
]);
|
|
@@ -4025,7 +4248,7 @@ export function AdminChangeBookingFlow({
|
|
|
4025
4248
|
? displayChangeFlowTax
|
|
4026
4249
|
: effectiveTax
|
|
4027
4250
|
: 0,
|
|
4028
|
-
total: originalReceipt ?
|
|
4251
|
+
total: originalReceipt ? displayChangeFlowProposedTotalWithEditableLines : totalPrice,
|
|
4029
4252
|
currency,
|
|
4030
4253
|
});
|
|
4031
4254
|
}, [
|
|
@@ -4034,7 +4257,7 @@ export function AdminChangeBookingFlow({
|
|
|
4034
4257
|
totalQuantity,
|
|
4035
4258
|
effectiveSubtotalForCheckout,
|
|
4036
4259
|
effectiveTax,
|
|
4037
|
-
|
|
4260
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
4038
4261
|
displayChangeFlowSubtotal,
|
|
4039
4262
|
displayChangeFlowTax,
|
|
4040
4263
|
currency,
|
|
@@ -4086,7 +4309,7 @@ export function AdminChangeBookingFlow({
|
|
|
4086
4309
|
.filter(([, count]) => count > 0)
|
|
4087
4310
|
.map(([category, count]) => ({ category, count }));
|
|
4088
4311
|
try {
|
|
4089
|
-
const
|
|
4312
|
+
const quoteRequestBase = {
|
|
4090
4313
|
bookingReference: bookingReferenceForQuote,
|
|
4091
4314
|
lastName: lastName.trim(),
|
|
4092
4315
|
newProductId: optionId,
|
|
@@ -4107,7 +4330,14 @@ export function AdminChangeBookingFlow({
|
|
|
4107
4330
|
previousAvailabilityId: initialValues.availabilityId ?? null,
|
|
4108
4331
|
previousReturnAvailabilityId: initialValues.returnAvailabilityId ?? null,
|
|
4109
4332
|
},
|
|
4110
|
-
}
|
|
4333
|
+
};
|
|
4334
|
+
const quote = useAdminFeAuthoritativeQuote
|
|
4335
|
+
? await quoteChangeBookingAdminFeReceipt({
|
|
4336
|
+
...quoteRequestBase,
|
|
4337
|
+
feReceipt: adminFeAuthoritativeReceipt,
|
|
4338
|
+
feAmountDueMajorUnits: roundMoney(changeFlowClientEstimateDue),
|
|
4339
|
+
})
|
|
4340
|
+
: await quoteChangeBooking(quoteRequestBase);
|
|
4111
4341
|
if (seq !== changeQuoteRequestSeq.current) return;
|
|
4112
4342
|
const slice = sliceChangeQuoteForUi(
|
|
4113
4343
|
quote,
|
|
@@ -4155,6 +4385,7 @@ export function AdminChangeBookingFlow({
|
|
|
4155
4385
|
addOnSelections,
|
|
4156
4386
|
changeFlowInitialTicketCount,
|
|
4157
4387
|
changeFlowNewBookingTotal,
|
|
4388
|
+
changeFlowClientEstimateDue,
|
|
4158
4389
|
effectiveSubtotalForCheckout,
|
|
4159
4390
|
effectiveTax,
|
|
4160
4391
|
totalPrice,
|
|
@@ -4163,6 +4394,8 @@ export function AdminChangeBookingFlow({
|
|
|
4163
4394
|
initialValues?.availabilityId,
|
|
4164
4395
|
initialValues?.returnAvailabilityId,
|
|
4165
4396
|
adminCustomLinesAsAdditionalAdjustments,
|
|
4397
|
+
adminFeAuthoritativeReceipt,
|
|
4398
|
+
useAdminFeAuthoritativeQuote,
|
|
4166
4399
|
]);
|
|
4167
4400
|
|
|
4168
4401
|
// Auto-select product option when date is selected: most popular if set, otherwise first available.
|
|
@@ -4683,7 +4916,7 @@ export function AdminChangeBookingFlow({
|
|
|
4683
4916
|
if (!changeBookingReference || !changeLastName) {
|
|
4684
4917
|
throw new Error('Missing booking reference or last name for change quote');
|
|
4685
4918
|
}
|
|
4686
|
-
const
|
|
4919
|
+
const quoteRequestBase = {
|
|
4687
4920
|
bookingReference: changeBookingReference,
|
|
4688
4921
|
lastName: changeLastName,
|
|
4689
4922
|
newProductId: availabilityProductOptionId,
|
|
@@ -4698,7 +4931,14 @@ export function AdminChangeBookingFlow({
|
|
|
4698
4931
|
: {}),
|
|
4699
4932
|
clientProposedTotal:
|
|
4700
4933
|
latestChangeQuote?.serverDisplay?.total ?? changeFlowNewBookingTotal,
|
|
4701
|
-
}
|
|
4934
|
+
};
|
|
4935
|
+
const quote = useAdminFeAuthoritativeQuote
|
|
4936
|
+
? await quoteChangeBookingAdminFeReceipt({
|
|
4937
|
+
...quoteRequestBase,
|
|
4938
|
+
feReceipt: adminFeAuthoritativeReceipt,
|
|
4939
|
+
feAmountDueMajorUnits: roundMoney(changeFlowClientEstimateDue),
|
|
4940
|
+
})
|
|
4941
|
+
: await quoteChangeBooking(quoteRequestBase);
|
|
4702
4942
|
const quoteSlice = sliceChangeQuoteForUi(
|
|
4703
4943
|
quote,
|
|
4704
4944
|
{
|
|
@@ -5052,7 +5292,7 @@ export function AdminChangeBookingFlow({
|
|
|
5052
5292
|
isCustomerSelfServeChange && originalReceipt
|
|
5053
5293
|
? {
|
|
5054
5294
|
previousTotal: originalReceipt.total,
|
|
5055
|
-
newTotal:
|
|
5295
|
+
newTotal: displayChangeFlowProposedTotalWithEditableLines,
|
|
5056
5296
|
differenceTotal: amountDueForCheckout,
|
|
5057
5297
|
}
|
|
5058
5298
|
: undefined,
|
|
@@ -5158,7 +5398,7 @@ export function AdminChangeBookingFlow({
|
|
|
5158
5398
|
addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
|
|
5159
5399
|
cancellationPolicyId: cancellationPolicyId ?? initialValues?.cancellationPolicyId ?? null,
|
|
5160
5400
|
promoCode: appliedPromoCode ?? null,
|
|
5161
|
-
newTotalAmount:
|
|
5401
|
+
newTotalAmount: displayChangeFlowProposedTotalWithEditableLines,
|
|
5162
5402
|
additionalHoursCount: null,
|
|
5163
5403
|
pricingAdjustment:
|
|
5164
5404
|
providerPricingOverrides.length > 0 || mergedProviderAdditionalAdjustments.length > 0
|
|
@@ -5531,7 +5771,7 @@ export function AdminChangeBookingFlow({
|
|
|
5531
5771
|
{selectedAvailability && (
|
|
5532
5772
|
<>
|
|
5533
5773
|
<CheckoutForm
|
|
5534
|
-
priceSummaryLines={
|
|
5774
|
+
priceSummaryLines={editableCheckoutPriceSummaryLines}
|
|
5535
5775
|
replacePriceSummary={selfServeCheckoutPlaceholder}
|
|
5536
5776
|
totalPrice={changeFlowAmountDue}
|
|
5537
5777
|
totalSummaryLabel={
|
|
@@ -5820,16 +6060,49 @@ export function AdminChangeBookingFlow({
|
|
|
5820
6060
|
attributionConfirmLabel={flowUi?.partnerAttributionConfirmLabel}
|
|
5821
6061
|
attributionConfirmed={partnerAttributionConfirmed}
|
|
5822
6062
|
onAttributionConfirmedChange={setPartnerAttributionConfirmed}
|
|
5823
|
-
lineAmountInputs={
|
|
5824
|
-
onLineAmountInputChange={
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
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
|
+
}}
|
|
5833
6106
|
/>
|
|
5834
6107
|
</>
|
|
5835
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,
|
|
@@ -168,6 +173,8 @@ export function CheckoutForm({
|
|
|
168
173
|
onLineAmountInputChange={onLineAmountInputChange}
|
|
169
174
|
onLineAmountInputBlur={onLineAmountInputBlur}
|
|
170
175
|
onLineAmountReset={onLineAmountReset}
|
|
176
|
+
lineLabelInputs={lineLabelInputs}
|
|
177
|
+
onLineLabelInputChange={onLineLabelInputChange}
|
|
171
178
|
extraBeforeSubtotal={extraBeforeSubtotal}
|
|
172
179
|
/>
|
|
173
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
|
/**
|
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;
|
|
@@ -1108,6 +1132,31 @@ export async function quoteChangeBooking(
|
|
|
1108
1132
|
data) as ChangeBookingQuoteResponse;
|
|
1109
1133
|
}
|
|
1110
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
|
+
|
|
1111
1160
|
export async function createChangeBookingPaymentIntent(
|
|
1112
1161
|
changeIntentId: string
|
|
1113
1162
|
): Promise<CreateChangePaymentIntentResponse> {
|