@ticketboothapp/booking 1.2.80 → 1.2.82
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
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
changeFlowTicketLineTotalWithReceiptFloor,
|
|
62
62
|
changeFlowFeeLineTotalWithReceiptFloor,
|
|
63
63
|
changeFlowReturnLineTotalWithReceiptFloor,
|
|
64
|
+
reconcileTaxIncludedChangeFlowTicketLines,
|
|
64
65
|
type ChangeFlowProtectedReceiptPricing,
|
|
65
66
|
roundMoney,
|
|
66
67
|
} from '../../lib/booking/change-flow-pricing';
|
|
@@ -885,7 +886,8 @@ export function AdminChangeBookingFlow({
|
|
|
885
886
|
/** Public self-serve only: cannot reduce tickets below original counts. Provider-dashboard admins may reduce party size. */
|
|
886
887
|
const changeBookingMinimumQuantities = useMemo(() => {
|
|
887
888
|
if (!isCustomerSelfServeChange || !initialValues?.bookingItems?.length) return undefined;
|
|
888
|
-
|
|
889
|
+
// Any admin-facing change flow should allow lowering quantities.
|
|
890
|
+
if (isAdmin) return undefined;
|
|
889
891
|
const m: Record<string, number> = {};
|
|
890
892
|
for (const item of initialValues.bookingItems) {
|
|
891
893
|
const key = item.category?.trim();
|
|
@@ -3710,7 +3712,6 @@ export function AdminChangeBookingFlow({
|
|
|
3710
3712
|
quantities,
|
|
3711
3713
|
addOnSelections,
|
|
3712
3714
|
adminCustomReceiptLines,
|
|
3713
|
-
editableCheckoutPriceSummaryLines,
|
|
3714
3715
|
editableSummaryLineAmountInputs,
|
|
3715
3716
|
editableSummaryLineLabelInputs,
|
|
3716
3717
|
useAdminFeAuthoritativeQuote,
|
|
@@ -3726,7 +3727,6 @@ export function AdminChangeBookingFlow({
|
|
|
3726
3727
|
quantities,
|
|
3727
3728
|
addOnSelections,
|
|
3728
3729
|
adminCustomReceiptLines,
|
|
3729
|
-
editableCheckoutPriceSummaryLines,
|
|
3730
3730
|
editableSummaryLineAmountInputs,
|
|
3731
3731
|
editableSummaryLineLabelInputs,
|
|
3732
3732
|
useAdminFeAuthoritativeQuote,
|
|
@@ -3828,11 +3828,30 @@ export function AdminChangeBookingFlow({
|
|
|
3828
3828
|
const displayChangeFlowProposedTotalWithEditableLines = roundMoney(
|
|
3829
3829
|
displayChangeFlowProposedTotal + editableSummaryPreSubtotalDelta
|
|
3830
3830
|
);
|
|
3831
|
+
/** Tax-included currencies: ticket row amounts match authoritative new-booking total (quote/display), not raw catalog/floor rollup. */
|
|
3832
|
+
const taxIncludedReconciledCheckoutPriceSummaryLines = useMemo(() => {
|
|
3833
|
+
if (!isTaxIncludedInPrice || !originalReceipt || showProviderPricingInlineEditor) {
|
|
3834
|
+
return editableCheckoutPriceSummaryLines;
|
|
3835
|
+
}
|
|
3836
|
+
return reconcileTaxIncludedChangeFlowTicketLines(
|
|
3837
|
+
editableCheckoutPriceSummaryLines,
|
|
3838
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
3839
|
+
effectivePromoDiscountAmount,
|
|
3840
|
+
);
|
|
3841
|
+
}, [
|
|
3842
|
+
isTaxIncludedInPrice,
|
|
3843
|
+
originalReceipt,
|
|
3844
|
+
showProviderPricingInlineEditor,
|
|
3845
|
+
editableCheckoutPriceSummaryLines,
|
|
3846
|
+
displayChangeFlowProposedTotalWithEditableLines,
|
|
3847
|
+
effectivePromoDiscountAmount,
|
|
3848
|
+
]);
|
|
3849
|
+
|
|
3831
3850
|
const adminFeAuthoritativeReceipt = useMemo((): AdminFeAuthoritativeReceipt => {
|
|
3832
|
-
const hasTaxLine =
|
|
3851
|
+
const hasTaxLine = taxIncludedReconciledCheckoutPriceSummaryLines.some(
|
|
3833
3852
|
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
3834
3853
|
);
|
|
3835
|
-
const lineItems = mapSummaryLinesToFeReceiptLineItems(
|
|
3854
|
+
const lineItems = mapSummaryLinesToFeReceiptLineItems(taxIncludedReconciledCheckoutPriceSummaryLines);
|
|
3836
3855
|
if (!hasTaxLine && Math.abs(displayChangeFlowTax) >= 0.0005) {
|
|
3837
3856
|
lineItems.push({
|
|
3838
3857
|
label: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
|
|
@@ -3855,7 +3874,7 @@ export function AdminChangeBookingFlow({
|
|
|
3855
3874
|
lineItems,
|
|
3856
3875
|
};
|
|
3857
3876
|
}, [
|
|
3858
|
-
|
|
3877
|
+
taxIncludedReconciledCheckoutPriceSummaryLines,
|
|
3859
3878
|
displayChangeFlowTax,
|
|
3860
3879
|
displayChangeFlowSubtotal,
|
|
3861
3880
|
editableSummaryPreSubtotalDelta,
|
|
@@ -5015,8 +5034,11 @@ export function AdminChangeBookingFlow({
|
|
|
5015
5034
|
}),
|
|
5016
5035
|
)
|
|
5017
5036
|
: totalPrice;
|
|
5037
|
+
const ticketRowsForCheckoutBreakdown = taxIncludedReconciledCheckoutPriceSummaryLines.filter(
|
|
5038
|
+
(l): l is Extract<PriceSummaryLine, { kind: 'ticket' }> => l.kind === 'ticket',
|
|
5039
|
+
);
|
|
5018
5040
|
const lines = [
|
|
5019
|
-
...
|
|
5041
|
+
...ticketRowsForCheckoutBreakdown.map((line) => ({
|
|
5020
5042
|
label: line.category,
|
|
5021
5043
|
amount: line.itemTotal,
|
|
5022
5044
|
type: 'TICKET' as const,
|
|
@@ -5159,7 +5181,7 @@ export function AdminChangeBookingFlow({
|
|
|
5159
5181
|
availabilityProductOptionId,
|
|
5160
5182
|
itineraryDisplay: itineraryDisplay ?? undefined,
|
|
5161
5183
|
clientSecret: paymentIntent.clientSecret ?? '',
|
|
5162
|
-
ticketLinesForModal:
|
|
5184
|
+
ticketLinesForModal: ticketRowsForCheckoutBreakdown.map((line) => {
|
|
5163
5185
|
const rate = pricing.find((r) => r.category === line.category);
|
|
5164
5186
|
const breakdown = getPriceBreakdown(
|
|
5165
5187
|
line.category,
|
|
@@ -5186,7 +5208,7 @@ export function AdminChangeBookingFlow({
|
|
|
5186
5208
|
return;
|
|
5187
5209
|
}
|
|
5188
5210
|
|
|
5189
|
-
const ticketLinesForModal: CheckoutModalLineItem[] =
|
|
5211
|
+
const ticketLinesForModal: CheckoutModalLineItem[] = ticketRowsForCheckoutBreakdown.map((line) => {
|
|
5190
5212
|
const rate = pricing.find((r) => r.category === line.category);
|
|
5191
5213
|
const breakdown = getPriceBreakdown(
|
|
5192
5214
|
line.category,
|
|
@@ -5725,7 +5747,7 @@ export function AdminChangeBookingFlow({
|
|
|
5725
5747
|
{selectedAvailability && (
|
|
5726
5748
|
<>
|
|
5727
5749
|
<CheckoutForm
|
|
5728
|
-
priceSummaryLines={
|
|
5750
|
+
priceSummaryLines={taxIncludedReconciledCheckoutPriceSummaryLines}
|
|
5729
5751
|
replacePriceSummary={hasChangeSelection ? selfServeCheckoutPlaceholder : undefined}
|
|
5730
5752
|
totalPrice={changeFlowAmountDue}
|
|
5731
5753
|
totalSummaryLabel={
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
changeFlowTicketLineTotalWithReceiptFloor,
|
|
57
57
|
changeFlowFeeLineTotalWithReceiptFloor,
|
|
58
58
|
changeFlowReturnLineTotalWithReceiptFloor,
|
|
59
|
+
reconcileTaxIncludedChangeFlowTicketLines,
|
|
59
60
|
type ChangeFlowProtectedReceiptPricing,
|
|
60
61
|
roundMoney,
|
|
61
62
|
} from '../../lib/booking/change-flow-pricing';
|
|
@@ -3535,6 +3536,34 @@ export function ChangeBookingFlow({
|
|
|
3535
3536
|
const displayChangeFlowSubtotal = displayedChangeAmounts.subtotal;
|
|
3536
3537
|
const displayChangeFlowTax = displayedChangeAmounts.tax;
|
|
3537
3538
|
|
|
3539
|
+
/** When quote is confirmed but BE sends no `priceSummaryLines`, we still show FE-built lines while total is quote-driven — reconcile ticket amounts for tax-included currencies. */
|
|
3540
|
+
const selfServeSummaryUsesServerLines =
|
|
3541
|
+
suppressSelfServeCurrencyUi &&
|
|
3542
|
+
selfServePricingConfirmed &&
|
|
3543
|
+
(latestChangeQuote?.serverPreview?.priceSummaryLines?.length ?? 0) > 0;
|
|
3544
|
+
|
|
3545
|
+
const taxIncludedSelfServeReconciledPriceSummaryLines = useMemo(() => {
|
|
3546
|
+
if (!isTaxIncludedInPrice || !originalReceipt || !isCustomerSelfServeChange) {
|
|
3547
|
+
return checkoutPriceSummaryLinesForCheckout;
|
|
3548
|
+
}
|
|
3549
|
+
if (selfServeSummaryUsesServerLines) {
|
|
3550
|
+
return checkoutPriceSummaryLinesForCheckout;
|
|
3551
|
+
}
|
|
3552
|
+
return reconcileTaxIncludedChangeFlowTicketLines(
|
|
3553
|
+
checkoutPriceSummaryLinesForCheckout,
|
|
3554
|
+
displayChangeFlowProposedTotal,
|
|
3555
|
+
effectivePromoDiscountAmount,
|
|
3556
|
+
);
|
|
3557
|
+
}, [
|
|
3558
|
+
isTaxIncludedInPrice,
|
|
3559
|
+
originalReceipt,
|
|
3560
|
+
isCustomerSelfServeChange,
|
|
3561
|
+
selfServeSummaryUsesServerLines,
|
|
3562
|
+
checkoutPriceSummaryLinesForCheckout,
|
|
3563
|
+
displayChangeFlowProposedTotal,
|
|
3564
|
+
effectivePromoDiscountAmount,
|
|
3565
|
+
]);
|
|
3566
|
+
|
|
3538
3567
|
const changeFlowClientEstimateDue = (() => {
|
|
3539
3568
|
if (!originalReceipt) return totalPrice;
|
|
3540
3569
|
// Customer self-serve: amount due comes from POST .../change/quote (`amountDueCents` / priceDiff), not FE delta math.
|
|
@@ -4437,8 +4466,11 @@ export function ChangeBookingFlow({
|
|
|
4437
4466
|
originalReceiptTotal: originalReceipt?.total ?? 0,
|
|
4438
4467
|
audience: 'customer',
|
|
4439
4468
|
});
|
|
4469
|
+
const ticketRowsForCheckoutBreakdown = taxIncludedSelfServeReconciledPriceSummaryLines.filter(
|
|
4470
|
+
(l): l is Extract<PriceSummaryLine, { kind: 'ticket' }> => l.kind === 'ticket',
|
|
4471
|
+
);
|
|
4440
4472
|
const lines = [
|
|
4441
|
-
...
|
|
4473
|
+
...ticketRowsForCheckoutBreakdown.map((line) => ({
|
|
4442
4474
|
label: line.category,
|
|
4443
4475
|
amount: line.itemTotal,
|
|
4444
4476
|
type: 'TICKET' as const,
|
|
@@ -4523,7 +4555,7 @@ export function ChangeBookingFlow({
|
|
|
4523
4555
|
})()
|
|
4524
4556
|
);
|
|
4525
4557
|
|
|
4526
|
-
const ticketLinesForModal: CheckoutModalLineItem[] =
|
|
4558
|
+
const ticketLinesForModal: CheckoutModalLineItem[] = ticketRowsForCheckoutBreakdown.map((line) => {
|
|
4527
4559
|
const rate = pricing.find((r) => r.category === line.category);
|
|
4528
4560
|
const breakdown = getPriceBreakdown(
|
|
4529
4561
|
line.category,
|
|
@@ -4883,7 +4915,7 @@ export function ChangeBookingFlow({
|
|
|
4883
4915
|
{selectedAvailability && (
|
|
4884
4916
|
<>
|
|
4885
4917
|
<CheckoutForm
|
|
4886
|
-
priceSummaryLines={
|
|
4918
|
+
priceSummaryLines={taxIncludedSelfServeReconciledPriceSummaryLines}
|
|
4887
4919
|
replacePriceSummary={selfServeCheckoutPlaceholder}
|
|
4888
4920
|
totalPrice={changeFlowAmountDue}
|
|
4889
4921
|
totalSummaryLabel={
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
* **Provider** inline pricing may show a **signed** delta from cart or manual overrides.
|
|
47
47
|
*/
|
|
48
48
|
import type { ChangeBookingQuoteResponse } from '../booking-api';
|
|
49
|
+
import type { PriceSummaryLine } from '../../components/booking/PriceSummary';
|
|
49
50
|
import { reconcileChangeBookingProposedTotal } from '../currency';
|
|
50
51
|
|
|
51
52
|
/** Money in major units, rounded to cents (half-up). */
|
|
@@ -301,6 +302,77 @@ export function resolveChangeFlowDisplayedAmounts(input: {
|
|
|
301
302
|
return { ...input.fromCart };
|
|
302
303
|
}
|
|
303
304
|
|
|
305
|
+
function isPromoSummaryLine(row: PriceSummaryLine): boolean {
|
|
306
|
+
if (row.kind !== 'line') return false;
|
|
307
|
+
const t = String(row.type ?? '').toUpperCase();
|
|
308
|
+
if (t === 'PROMO_CODE' || t === 'GIFT_CARD' || t === 'DISCOUNT' || t === 'VOUCHER') return true;
|
|
309
|
+
return row.amount < -0.005 && /promo|discount|voucher|gift/i.test(row.label);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Tax-included currencies (e.g. EUR): ticket rows from catalog + receipt floors can disagree with the
|
|
314
|
+
* authoritative **new booking total** (quote / display layer). Scale ticket `itemTotal` values so
|
|
315
|
+
* tickets + non-ticket lines + promo = `targetNewBookingTotal` without changing quantities or non-ticket rows.
|
|
316
|
+
*/
|
|
317
|
+
export function reconcileTaxIncludedChangeFlowTicketLines(
|
|
318
|
+
lines: PriceSummaryLine[],
|
|
319
|
+
targetNewBookingTotal: number,
|
|
320
|
+
promoDiscountAmountIfNotInLines: number,
|
|
321
|
+
): PriceSummaryLine[] {
|
|
322
|
+
if (!Number.isFinite(targetNewBookingTotal) || lines.length === 0) return lines;
|
|
323
|
+
|
|
324
|
+
const ticketIndices: number[] = [];
|
|
325
|
+
let currentTicketTotal = 0;
|
|
326
|
+
for (let i = 0; i < lines.length; i++) {
|
|
327
|
+
const row = lines[i];
|
|
328
|
+
if (row.kind === 'ticket') {
|
|
329
|
+
ticketIndices.push(i);
|
|
330
|
+
currentTicketTotal += row.itemTotal;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (ticketIndices.length === 0) return lines;
|
|
334
|
+
|
|
335
|
+
let nonTicketSum = 0;
|
|
336
|
+
let promoLineSum = 0;
|
|
337
|
+
for (const row of lines) {
|
|
338
|
+
if (row.kind === 'line') {
|
|
339
|
+
const t = String(row.type ?? '').toUpperCase();
|
|
340
|
+
if (t === 'TAX') continue;
|
|
341
|
+
if (isPromoSummaryLine(row)) {
|
|
342
|
+
promoLineSum += row.amount;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
nonTicketSum += row.amount;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const desiredTicketTotal =
|
|
350
|
+
Math.abs(promoLineSum) >= 0.005
|
|
351
|
+
? roundMoney(targetNewBookingTotal - nonTicketSum - promoLineSum)
|
|
352
|
+
: roundMoney(
|
|
353
|
+
targetNewBookingTotal - nonTicketSum + Math.max(0, promoDiscountAmountIfNotInLines),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (currentTicketTotal <= 0 || Math.abs(desiredTicketTotal - currentTicketTotal) < 0.02) {
|
|
357
|
+
return lines;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const scale = desiredTicketTotal / currentTicketTotal;
|
|
361
|
+
const next = lines.slice() as PriceSummaryLine[];
|
|
362
|
+
let running = 0;
|
|
363
|
+
for (let k = 0; k < ticketIndices.length; k++) {
|
|
364
|
+
const i = ticketIndices[k];
|
|
365
|
+
const row = next[i] as Extract<PriceSummaryLine, { kind: 'ticket' }>;
|
|
366
|
+
const isLast = k === ticketIndices.length - 1;
|
|
367
|
+
const newTotal = isLast
|
|
368
|
+
? roundMoney(desiredTicketTotal - running)
|
|
369
|
+
: roundMoney(row.itemTotal * scale);
|
|
370
|
+
running += newTotal;
|
|
371
|
+
next[i] = { ...row, itemTotal: newTotal };
|
|
372
|
+
}
|
|
373
|
+
return next;
|
|
374
|
+
}
|
|
375
|
+
|
|
304
376
|
/**
|
|
305
377
|
* Product: **Main “total owed” number** should not show -0.00 or noise when the balance is effectively zero.
|
|
306
378
|
*/
|