@ticketboothapp/booking 1.2.138 → 1.2.140
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/package.json +1 -1
- package/src/components/booking/AdminChangeReceiptComparison.tsx +51 -26
- package/src/components/booking/PriceBreakdown.tsx +0 -4
- package/src/components/booking/PriceSummary.tsx +0 -2
- package/src/components/booking/useAdminChangeQuotePreview.ts +4 -12
- package/src/lib/booking-api.ts +69 -0
package/package.json
CHANGED
|
@@ -84,12 +84,17 @@ function amendmentDeltaLines(
|
|
|
84
84
|
|
|
85
85
|
return proposedLines.flatMap((line): PriceSummaryLine[] => {
|
|
86
86
|
const original = remainingOriginal.get(lineIdentity(line));
|
|
87
|
-
const amount = roundMoney(lineAmount(line) - (original?.amount ?? 0));
|
|
88
87
|
const proposedQuantity = lineQuantity(line);
|
|
89
88
|
const quantity =
|
|
90
89
|
proposedQuantity == null && original?.quantity == null
|
|
91
90
|
? null
|
|
92
91
|
: (proposedQuantity ?? 0) - (original?.quantity ?? 0);
|
|
92
|
+
const proposedAmount = lineAmount(line);
|
|
93
|
+
const amount = roundMoney(
|
|
94
|
+
quantity != null && quantity > 0 && proposedQuantity != null && proposedQuantity > 0
|
|
95
|
+
? (proposedAmount / proposedQuantity) * quantity
|
|
96
|
+
: proposedAmount - (original?.amount ?? 0),
|
|
97
|
+
);
|
|
93
98
|
if (Math.abs(amount) < 0.005 && (quantity == null || quantity === 0)) return [];
|
|
94
99
|
|
|
95
100
|
if (line.kind === 'ticket') {
|
|
@@ -104,7 +109,9 @@ function amendmentDeltaLines(
|
|
|
104
109
|
const isTax = String(line.type ?? '').toUpperCase() === 'TAX';
|
|
105
110
|
return [{
|
|
106
111
|
...line,
|
|
107
|
-
label: isTax
|
|
112
|
+
label: isTax
|
|
113
|
+
? (amount >= 0 ? 'Additional tax' : 'Tax reduction')
|
|
114
|
+
: line.label.replace(/\s*\([^)]*(?:people|person|x\s*\d+)[^)]*\)\s*$/i, '').trim(),
|
|
108
115
|
amount,
|
|
109
116
|
quantity: quantity == null ? null : Math.abs(quantity),
|
|
110
117
|
editable: false,
|
|
@@ -117,8 +124,6 @@ export function AdminChangeReceiptComparison({
|
|
|
117
124
|
originalReceipt,
|
|
118
125
|
newReceipt,
|
|
119
126
|
newPriceSummaryLines,
|
|
120
|
-
newPriceSummaryLinesIncludeTaxRow,
|
|
121
|
-
amountDue,
|
|
122
127
|
amountDueLabel,
|
|
123
128
|
currency,
|
|
124
129
|
locale,
|
|
@@ -136,7 +141,20 @@ export function AdminChangeReceiptComparison({
|
|
|
136
141
|
() => amendmentDeltaLines(originalReceiptLines, newPriceSummaryLines),
|
|
137
142
|
[newPriceSummaryLines, originalReceiptLines],
|
|
138
143
|
);
|
|
139
|
-
const
|
|
144
|
+
const nonTaxChangeSubtotal = roundMoney(
|
|
145
|
+
calculatedChangeLines.reduce(
|
|
146
|
+
(sum, line) =>
|
|
147
|
+
line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
148
|
+
? sum
|
|
149
|
+
: sum + lineAmount(line),
|
|
150
|
+
0,
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
const receiptTaxDelta = roundMoney(
|
|
154
|
+
taxRate != null && nonTaxChangeSubtotal > 0
|
|
155
|
+
? nonTaxChangeSubtotal * taxRate
|
|
156
|
+
: newReceipt.tax - originalReceipt.tax,
|
|
157
|
+
);
|
|
140
158
|
const changeLines = useMemo(() => {
|
|
141
159
|
if (hasTaxLine(calculatedChangeLines) || Math.abs(receiptTaxDelta) < 0.005) {
|
|
142
160
|
return calculatedChangeLines;
|
|
@@ -161,28 +179,35 @@ export function AdminChangeReceiptComparison({
|
|
|
161
179
|
0,
|
|
162
180
|
),
|
|
163
181
|
);
|
|
164
|
-
const
|
|
182
|
+
const displayedAmountDue = roundMoney(changeLines.reduce((sum, line) => sum + lineAmount(line), 0));
|
|
183
|
+
const displayedUpdatedTotal = roundMoney(originalReceipt.total + displayedAmountDue);
|
|
184
|
+
const displayedUpdatedSubtotal = roundMoney(originalReceipt.subtotal + nonTaxChangeSubtotal);
|
|
185
|
+
const displayedUpdatedTax = roundMoney(originalReceipt.tax + changeTax);
|
|
165
186
|
const originalCurrency = originalReceipt.currency ?? currency;
|
|
166
|
-
const
|
|
187
|
+
const originalReceiptLinesWithComparableBreakdowns = useMemo(
|
|
167
188
|
() =>
|
|
168
189
|
originalReceiptLines.map((line) =>
|
|
169
190
|
line.kind === 'ticket' && line.qty > 0
|
|
170
191
|
? {
|
|
171
192
|
...line,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
193
|
+
breakdown: line.breakdown ?? {
|
|
194
|
+
lineItems: [],
|
|
195
|
+
subtotalAfterAdjustments: roundMoney(line.itemTotal / line.qty),
|
|
196
|
+
feeLines: [],
|
|
197
|
+
taxRate: 0,
|
|
198
|
+
taxAmount: 0,
|
|
199
|
+
isTaxIncluded: false,
|
|
200
|
+
finalPrice: roundMoney(line.itemTotal / line.qty),
|
|
201
|
+
},
|
|
177
202
|
}
|
|
178
203
|
: line,
|
|
179
204
|
),
|
|
180
|
-
[
|
|
205
|
+
[originalReceiptLines],
|
|
181
206
|
);
|
|
182
207
|
const amountDueClass =
|
|
183
|
-
|
|
208
|
+
displayedAmountDue < -0.005
|
|
184
209
|
? 'text-emerald-700'
|
|
185
|
-
:
|
|
210
|
+
: displayedAmountDue > 0.005
|
|
186
211
|
? 'text-stone-900'
|
|
187
212
|
: 'text-stone-700';
|
|
188
213
|
|
|
@@ -197,7 +222,7 @@ export function AdminChangeReceiptComparison({
|
|
|
197
222
|
</span>
|
|
198
223
|
</div>
|
|
199
224
|
<PriceSummary
|
|
200
|
-
lines={
|
|
225
|
+
lines={originalReceiptLinesWithComparableBreakdowns}
|
|
201
226
|
total={originalReceipt.total}
|
|
202
227
|
currency={originalCurrency}
|
|
203
228
|
locale={displayLocale}
|
|
@@ -215,10 +240,10 @@ export function AdminChangeReceiptComparison({
|
|
|
215
240
|
<p className="mt-0.5 text-xs text-stone-500">Only newly charged, credited, or repriced value</p>
|
|
216
241
|
</div>
|
|
217
242
|
<span className="shrink-0 text-xs font-medium text-stone-500">
|
|
218
|
-
{formatCurrencyAmount(
|
|
243
|
+
{formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
|
|
219
244
|
</span>
|
|
220
245
|
</div>
|
|
221
|
-
{changeLines.length === 0 && Math.abs(
|
|
246
|
+
{changeLines.length === 0 && Math.abs(displayedAmountDue) < 0.005 ? (
|
|
222
247
|
<div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
|
|
223
248
|
<p className="text-sm font-medium text-stone-700">No pricing changes</p>
|
|
224
249
|
<p className="mt-1 text-xs text-stone-500">The selected booking details match the existing purchase.</p>
|
|
@@ -226,10 +251,10 @@ export function AdminChangeReceiptComparison({
|
|
|
226
251
|
) : (
|
|
227
252
|
<PriceSummary
|
|
228
253
|
lines={changeLines}
|
|
229
|
-
total={
|
|
254
|
+
total={displayedAmountDue}
|
|
230
255
|
currency={currency}
|
|
231
256
|
locale={displayLocale}
|
|
232
|
-
subtotal={
|
|
257
|
+
subtotal={nonTaxChangeSubtotal}
|
|
233
258
|
taxAmount={!changeLinesIncludeTaxRow ? changeTax : 0}
|
|
234
259
|
taxRate={taxRate}
|
|
235
260
|
size="sm"
|
|
@@ -245,16 +270,16 @@ export function AdminChangeReceiptComparison({
|
|
|
245
270
|
<p className="mt-0.5 text-xs text-stone-500">Read-only pricing result; historical and added cohorts remain separate when supplied by the server</p>
|
|
246
271
|
</div>
|
|
247
272
|
<span className="shrink-0 text-xs font-medium text-stone-500">
|
|
248
|
-
{formatCurrencyAmount(
|
|
273
|
+
{formatCurrencyAmount(displayedUpdatedTotal, currency, displayLocale)}
|
|
249
274
|
</span>
|
|
250
275
|
</div>
|
|
251
276
|
<PriceSummary
|
|
252
|
-
lines={
|
|
253
|
-
total={
|
|
277
|
+
lines={[...originalReceiptLines, ...changeLines].map((line) => ({ ...line, editable: false, lineKey: undefined }))}
|
|
278
|
+
total={displayedUpdatedTotal}
|
|
254
279
|
currency={currency}
|
|
255
280
|
locale={displayLocale}
|
|
256
|
-
subtotal={
|
|
257
|
-
taxAmount={!
|
|
281
|
+
subtotal={displayedUpdatedSubtotal}
|
|
282
|
+
taxAmount={!hasTaxLine([...originalReceiptLines, ...changeLines]) ? displayedUpdatedTax : 0}
|
|
258
283
|
taxRate={taxRate}
|
|
259
284
|
size="sm"
|
|
260
285
|
t={t}
|
|
@@ -272,7 +297,7 @@ export function AdminChangeReceiptComparison({
|
|
|
272
297
|
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
|
|
273
298
|
<span className="text-sm font-semibold text-stone-900">{amountDueLabel}</span>
|
|
274
299
|
<span className={`text-lg font-semibold tabular-nums ${amountDueClass}`}>
|
|
275
|
-
{formatCurrencyAmount(
|
|
300
|
+
{formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
|
|
276
301
|
</span>
|
|
277
302
|
</div>
|
|
278
303
|
</div>
|
|
@@ -12,7 +12,6 @@ export interface PriceBreakdownProps {
|
|
|
12
12
|
category: string;
|
|
13
13
|
qty: number;
|
|
14
14
|
itemTotal: number;
|
|
15
|
-
unitAmountLabel?: string;
|
|
16
15
|
breakdown: PriceBreakdownType | null;
|
|
17
16
|
currency: Currency;
|
|
18
17
|
locale: Locale;
|
|
@@ -60,7 +59,6 @@ export function PriceBreakdown({
|
|
|
60
59
|
category,
|
|
61
60
|
qty,
|
|
62
61
|
itemTotal,
|
|
63
|
-
unitAmountLabel,
|
|
64
62
|
breakdown,
|
|
65
63
|
currency,
|
|
66
64
|
locale,
|
|
@@ -98,7 +96,6 @@ export function PriceBreakdown({
|
|
|
98
96
|
) : (
|
|
99
97
|
<span className="text-sm text-stone-600">
|
|
100
98
|
{category} {qty > 1 ? `× ${qty}` : ''}
|
|
101
|
-
{unitAmountLabel ? <span className="ml-1 text-xs text-stone-400">· {unitAmountLabel}</span> : null}
|
|
102
99
|
</span>
|
|
103
100
|
)}
|
|
104
101
|
{editable && onEditableChange ? (
|
|
@@ -160,7 +157,6 @@ export function PriceBreakdown({
|
|
|
160
157
|
}
|
|
161
158
|
>
|
|
162
159
|
{category} {qty > 1 ? `× ${qty}` : ''}
|
|
163
|
-
{unitAmountLabel ? <span className="ml-1 text-xs text-stone-400">· {unitAmountLabel}</span> : null}
|
|
164
160
|
</span>
|
|
165
161
|
)}
|
|
166
162
|
<div className="relative flex-shrink-0 whitespace-nowrap">
|
|
@@ -18,7 +18,6 @@ export type PriceSummaryLine =
|
|
|
18
18
|
category: string;
|
|
19
19
|
qty: number;
|
|
20
20
|
itemTotal: number;
|
|
21
|
-
unitAmountLabel?: string;
|
|
22
21
|
breakdown?: PriceBreakdownType | null;
|
|
23
22
|
}
|
|
24
23
|
| {
|
|
@@ -211,7 +210,6 @@ export function PriceSummary({
|
|
|
211
210
|
category={row.category}
|
|
212
211
|
qty={row.qty}
|
|
213
212
|
itemTotal={row.itemTotal}
|
|
214
|
-
unitAmountLabel={row.unitAmountLabel}
|
|
215
213
|
breakdown={row.breakdown ?? null}
|
|
216
214
|
currency={currency}
|
|
217
215
|
locale={locale}
|
|
@@ -8,14 +8,13 @@ import {
|
|
|
8
8
|
} from 'react';
|
|
9
9
|
import {
|
|
10
10
|
quoteChangeBooking,
|
|
11
|
-
|
|
11
|
+
quoteAdminChangeBookingV2,
|
|
12
12
|
type AdminFeAuthoritativeReceipt,
|
|
13
13
|
type Availability,
|
|
14
14
|
type ItineraryDisplayStep,
|
|
15
15
|
type Product,
|
|
16
16
|
type ReturnOption,
|
|
17
17
|
} from '../../lib/booking-api';
|
|
18
|
-
import { roundMoney } from '../../lib/booking/change-flow-pricing';
|
|
19
18
|
import { getItineraryStepLabel } from '../../lib/booking/itinerary-display';
|
|
20
19
|
import type { OrderSummaryTicketLine } from '../../lib/booking/pricing';
|
|
21
20
|
import { formatCurrencyAmount } from '../../lib/currency';
|
|
@@ -423,18 +422,11 @@ export function useAdminChangeQuotePreview({
|
|
|
423
422
|
previousReturnAvailabilityId: initialValues.returnAvailabilityId ?? null,
|
|
424
423
|
},
|
|
425
424
|
};
|
|
426
|
-
const deterministicClientProposedTotal = useAdminFeAuthoritativeQuote
|
|
427
|
-
? adminFeAuthoritativeReceipt.total
|
|
428
|
-
: changeFlowNewBookingTotal;
|
|
429
|
-
const deterministicAmountDue = originalReceipt
|
|
430
|
-
? roundMoney(adminFeAuthoritativeReceipt.total - originalReceipt.total)
|
|
431
|
-
: roundMoney(changeFlowClientEstimateDue);
|
|
432
425
|
const quote = useAdminFeAuthoritativeQuote
|
|
433
|
-
? await
|
|
426
|
+
? await quoteAdminChangeBookingV2({
|
|
434
427
|
...quoteRequestBase,
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
clientProposedTotal: deterministicClientProposedTotal,
|
|
428
|
+
// V2 is server-authoritative. Do not bind it to the outbound-only FE estimate.
|
|
429
|
+
clientProposedTotal: undefined,
|
|
438
430
|
})
|
|
439
431
|
: await quoteChangeBooking(quoteRequestBase);
|
|
440
432
|
if (seq !== changeQuoteRequestSeq.current) return;
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -1290,6 +1290,23 @@ export interface AdminChangeBookingQuoteRequest extends ChangeBookingQuoteReques
|
|
|
1290
1290
|
feAmountDueMajorUnits?: number;
|
|
1291
1291
|
}
|
|
1292
1292
|
|
|
1293
|
+
export interface AdminChangeBookingQuoteV2Data {
|
|
1294
|
+
oldReceipt: {
|
|
1295
|
+
currency?: string | null;
|
|
1296
|
+
grossSubtotal?: number | null;
|
|
1297
|
+
taxAmount?: number | null;
|
|
1298
|
+
payableTotal?: number | null;
|
|
1299
|
+
lines?: PricingV2QuoteLineSnapshot[] | null;
|
|
1300
|
+
};
|
|
1301
|
+
newQuote?: PricingV2QuoteSnapshot | null;
|
|
1302
|
+
balanceDelta?: number | null;
|
|
1303
|
+
amountToCharge?: number | null;
|
|
1304
|
+
refundCandidate?: number | null;
|
|
1305
|
+
canApply: boolean;
|
|
1306
|
+
reasonIfBlocked?: string | null;
|
|
1307
|
+
unsupportedFeatures?: string[];
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1293
1310
|
export interface ChangeBookingQuoteReceipt {
|
|
1294
1311
|
subtotal?: number;
|
|
1295
1312
|
tax?: number;
|
|
@@ -1593,6 +1610,58 @@ export async function quoteChangeBookingAdminFeReceipt(
|
|
|
1593
1610
|
data) as ChangeBookingQuoteResponse;
|
|
1594
1611
|
}
|
|
1595
1612
|
|
|
1613
|
+
/** Server-authoritative Pricing V2 quote used by the provider-dashboard amendment preview. */
|
|
1614
|
+
export async function quoteAdminChangeBookingV2(
|
|
1615
|
+
request: ChangeBookingQuoteRequest
|
|
1616
|
+
): Promise<ChangeBookingQuoteResponse> {
|
|
1617
|
+
const { bookingReference, lastName: _lastName, ...payload } = request;
|
|
1618
|
+
const res = await fetch(
|
|
1619
|
+
`${API_BASE}/1/admin/bookings/${encodeURIComponent(bookingReference)}/change/quote-v2`,
|
|
1620
|
+
{
|
|
1621
|
+
method: 'POST',
|
|
1622
|
+
headers: getAuthHeaders(),
|
|
1623
|
+
body: JSON.stringify(payload),
|
|
1624
|
+
}
|
|
1625
|
+
);
|
|
1626
|
+
if (!res.ok) {
|
|
1627
|
+
const err = await parseJsonSafely(res);
|
|
1628
|
+
const message = isApiErrorPayload(err)
|
|
1629
|
+
? err.errorMessage || err.error || 'Failed to quote admin booking change'
|
|
1630
|
+
: 'Failed to quote admin booking change';
|
|
1631
|
+
throw new Error(message);
|
|
1632
|
+
}
|
|
1633
|
+
const response = await parseJsonSafely(res);
|
|
1634
|
+
const outer = (response as { data?: unknown } | null)?.data;
|
|
1635
|
+
const data = ((outer as { data?: AdminChangeBookingQuoteV2Data } | null)?.data ??
|
|
1636
|
+
outer) as AdminChangeBookingQuoteV2Data;
|
|
1637
|
+
const oldReceipt = data.oldReceipt;
|
|
1638
|
+
const newQuote = data.newQuote ?? null;
|
|
1639
|
+
const currency = newQuote?.currency ?? oldReceipt.currency ?? undefined;
|
|
1640
|
+
return {
|
|
1641
|
+
pricingQuote: newQuote,
|
|
1642
|
+
quote: newQuote,
|
|
1643
|
+
balanceDelta: data.balanceDelta ?? newQuote?.balanceDelta ?? undefined,
|
|
1644
|
+
amountToCharge: data.amountToCharge ?? newQuote?.amountToCharge ?? undefined,
|
|
1645
|
+
refundCandidate: data.refundCandidate ?? newQuote?.refundCandidate ?? undefined,
|
|
1646
|
+
priceDiff: data.balanceDelta ?? newQuote?.balanceDelta ?? 0,
|
|
1647
|
+
currency: currency ?? undefined,
|
|
1648
|
+
canProceed: data.canApply,
|
|
1649
|
+
reasonIfBlocked: data.reasonIfBlocked ?? undefined,
|
|
1650
|
+
originalReceipt: {
|
|
1651
|
+
subtotal: oldReceipt.grossSubtotal ?? undefined,
|
|
1652
|
+
tax: oldReceipt.taxAmount ?? undefined,
|
|
1653
|
+
total: oldReceipt.payableTotal ?? 0,
|
|
1654
|
+
currency: oldReceipt.currency ?? undefined,
|
|
1655
|
+
lineItems: oldReceipt.lines?.map((line) => ({
|
|
1656
|
+
label: line.label ?? undefined,
|
|
1657
|
+
amount: line.amount ?? undefined,
|
|
1658
|
+
type: line.type ?? undefined,
|
|
1659
|
+
quantity: line.quantity ?? undefined,
|
|
1660
|
+
})),
|
|
1661
|
+
},
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1596
1665
|
export async function createChangeBookingPaymentIntent(
|
|
1597
1666
|
changeIntentId: string
|
|
1598
1667
|
): Promise<CreateChangePaymentIntentResponse> {
|