@ticketboothapp/booking 1.2.139 → 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
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,7 +179,10 @@ 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
187
|
const originalReceiptLinesWithComparableBreakdowns = useMemo(
|
|
167
188
|
() =>
|
|
@@ -184,9 +205,9 @@ export function AdminChangeReceiptComparison({
|
|
|
184
205
|
[originalReceiptLines],
|
|
185
206
|
);
|
|
186
207
|
const amountDueClass =
|
|
187
|
-
|
|
208
|
+
displayedAmountDue < -0.005
|
|
188
209
|
? 'text-emerald-700'
|
|
189
|
-
:
|
|
210
|
+
: displayedAmountDue > 0.005
|
|
190
211
|
? 'text-stone-900'
|
|
191
212
|
: 'text-stone-700';
|
|
192
213
|
|
|
@@ -219,10 +240,10 @@ export function AdminChangeReceiptComparison({
|
|
|
219
240
|
<p className="mt-0.5 text-xs text-stone-500">Only newly charged, credited, or repriced value</p>
|
|
220
241
|
</div>
|
|
221
242
|
<span className="shrink-0 text-xs font-medium text-stone-500">
|
|
222
|
-
{formatCurrencyAmount(
|
|
243
|
+
{formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
|
|
223
244
|
</span>
|
|
224
245
|
</div>
|
|
225
|
-
{changeLines.length === 0 && Math.abs(
|
|
246
|
+
{changeLines.length === 0 && Math.abs(displayedAmountDue) < 0.005 ? (
|
|
226
247
|
<div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
|
|
227
248
|
<p className="text-sm font-medium text-stone-700">No pricing changes</p>
|
|
228
249
|
<p className="mt-1 text-xs text-stone-500">The selected booking details match the existing purchase.</p>
|
|
@@ -230,10 +251,10 @@ export function AdminChangeReceiptComparison({
|
|
|
230
251
|
) : (
|
|
231
252
|
<PriceSummary
|
|
232
253
|
lines={changeLines}
|
|
233
|
-
total={
|
|
254
|
+
total={displayedAmountDue}
|
|
234
255
|
currency={currency}
|
|
235
256
|
locale={displayLocale}
|
|
236
|
-
subtotal={
|
|
257
|
+
subtotal={nonTaxChangeSubtotal}
|
|
237
258
|
taxAmount={!changeLinesIncludeTaxRow ? changeTax : 0}
|
|
238
259
|
taxRate={taxRate}
|
|
239
260
|
size="sm"
|
|
@@ -249,16 +270,16 @@ export function AdminChangeReceiptComparison({
|
|
|
249
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>
|
|
250
271
|
</div>
|
|
251
272
|
<span className="shrink-0 text-xs font-medium text-stone-500">
|
|
252
|
-
{formatCurrencyAmount(
|
|
273
|
+
{formatCurrencyAmount(displayedUpdatedTotal, currency, displayLocale)}
|
|
253
274
|
</span>
|
|
254
275
|
</div>
|
|
255
276
|
<PriceSummary
|
|
256
|
-
lines={
|
|
257
|
-
total={
|
|
277
|
+
lines={[...originalReceiptLines, ...changeLines].map((line) => ({ ...line, editable: false, lineKey: undefined }))}
|
|
278
|
+
total={displayedUpdatedTotal}
|
|
258
279
|
currency={currency}
|
|
259
280
|
locale={displayLocale}
|
|
260
|
-
subtotal={
|
|
261
|
-
taxAmount={!
|
|
281
|
+
subtotal={displayedUpdatedSubtotal}
|
|
282
|
+
taxAmount={!hasTaxLine([...originalReceiptLines, ...changeLines]) ? displayedUpdatedTax : 0}
|
|
262
283
|
taxRate={taxRate}
|
|
263
284
|
size="sm"
|
|
264
285
|
t={t}
|
|
@@ -276,7 +297,7 @@ export function AdminChangeReceiptComparison({
|
|
|
276
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">
|
|
277
298
|
<span className="text-sm font-semibold text-stone-900">{amountDueLabel}</span>
|
|
278
299
|
<span className={`text-lg font-semibold tabular-nums ${amountDueClass}`}>
|
|
279
|
-
{formatCurrencyAmount(
|
|
300
|
+
{formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
|
|
280
301
|
</span>
|
|
281
302
|
</div>
|
|
282
303
|
</div>
|
|
@@ -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> {
|