@ticketboothapp/booking 1.2.119 → 1.2.121

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.119",
3
+ "version": "1.2.121",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -4208,18 +4208,12 @@ export function AdminChangeBookingFlow({
4208
4208
  type: 'TAX',
4209
4209
  });
4210
4210
  }
4211
- let subtotal = roundMoney(displayChangeFlowSubtotal + editableSummaryPreSubtotalDelta);
4212
4211
  const tax = roundMoney(displayChangeFlowTax);
4213
4212
  const expectedTotal = roundMoney(displayChangeFlowProposedTotalWithEditableLines);
4214
- const promoAdjustmentTotal = roundMoney(
4215
- lineItems.reduce((sum, line) => (
4216
- isPromoDiscountGiftCardOrVoucherLine(line) ? sum + (Number(line.amount) || 0) : sum
4217
- ), 0)
4218
- );
4219
- const recomputed = roundMoney(subtotal + tax + promoAdjustmentTotal);
4220
- if (!unchangedReceiptAnchor && Math.abs(recomputed - expectedTotal) >= 0.005) {
4221
- subtotal = roundMoney(expectedTotal - tax - promoAdjustmentTotal);
4222
- }
4213
+ // Receipt subtotal means "subtotal before tax" after discount/payment-adjustment lines.
4214
+ // The visible line list already includes promo/voucher/gift-card rows, so deriving from
4215
+ // total minus tax keeps the footer aligned with the receipt totals we send to the API.
4216
+ const subtotal = roundMoney(expectedTotal - tax);
4223
4217
  return {
4224
4218
  subtotal,
4225
4219
  tax,
@@ -4231,14 +4225,27 @@ export function AdminChangeBookingFlow({
4231
4225
  taxIncludedReconciledCheckoutPriceSummaryLines,
4232
4226
  quantities,
4233
4227
  displayChangeFlowTax,
4234
- displayChangeFlowSubtotal,
4235
- editableSummaryPreSubtotalDelta,
4236
4228
  displayChangeFlowProposedTotalWithEditableLines,
4237
- unchangedReceiptAnchor,
4238
4229
  currency,
4239
4230
  t,
4240
4231
  ]);
4241
4232
 
4233
+ const providerApplyAuthoritativeReceipt = useMemo(() => {
4234
+ if (!useAdminFeAuthoritativeQuote) return undefined;
4235
+ return {
4236
+ subtotalBeforeTax: adminFeAuthoritativeReceipt.subtotal,
4237
+ taxAmount: adminFeAuthoritativeReceipt.tax,
4238
+ totalAmount: adminFeAuthoritativeReceipt.total,
4239
+ currency: adminFeAuthoritativeReceipt.currency ?? currency,
4240
+ lineItems: adminFeAuthoritativeReceipt.lineItems.map((line) => ({
4241
+ type: String(line.type || 'FEE'),
4242
+ label: String(line.label || 'Adjustment'),
4243
+ amount: Number(line.amount) || 0,
4244
+ ...(line.quantity != null ? { quantity: Number(line.quantity) || 0 } : {}),
4245
+ })),
4246
+ };
4247
+ }, [adminFeAuthoritativeReceipt, currency, useAdminFeAuthoritativeQuote]);
4248
+
4242
4249
  const changeFlowClientEstimateDueBase = (() => {
4243
4250
  if (!originalReceipt) return totalPrice;
4244
4251
  if (unchangedReceiptAnchor) return 0;
@@ -5401,6 +5408,7 @@ export function AdminChangeBookingFlow({
5401
5408
  : {}),
5402
5409
  }
5403
5410
  : undefined,
5411
+ authoritativeReceipt: providerApplyAuthoritativeReceipt,
5404
5412
  capacitySeatCredit: {
5405
5413
  enabled: true,
5406
5414
  previousPassengerCount: changeFlowInitialTicketCount,
@@ -5796,6 +5804,7 @@ export function AdminChangeBookingFlow({
5796
5804
  : {}),
5797
5805
  }
5798
5806
  : undefined,
5807
+ authoritativeReceipt: providerApplyAuthoritativeReceipt,
5799
5808
  capacitySeatCredit: {
5800
5809
  enabled: true,
5801
5810
  previousPassengerCount: changeFlowInitialTicketCount,
@@ -352,7 +352,7 @@ export function PrivateShuttleBookingFlow({
352
352
  itineraryDisplay?: ItineraryDisplayStep[] | null;
353
353
  clientSecret: string;
354
354
  ticketLinesForModal: CheckoutModalLineItem[];
355
- feeLineItems: { name: string; totalAmount: number }[];
355
+ feeLineItems: { name: string; totalAmount: number; description?: string }[];
356
356
  cancellationPolicyFee: number;
357
357
  cancellationPolicyLabel?: string;
358
358
  subtotal: number;
@@ -1095,6 +1095,17 @@ export function PrivateShuttleBookingFlow({
1095
1095
  const cancellationPolicyFee = selectedCancellationPolicy
1096
1096
  ? (selectedCancellationPolicy.feeByCurrency[currency] ?? 0)
1097
1097
  : 0;
1098
+ const perBookingFeeLineItems = useMemo(() => {
1099
+ if (isTaxIncludedInPrice) return [];
1100
+ const amountsByName = pricingConfig?.perBookingFeesByCurrency?.[currency];
1101
+ if (!amountsByName) return [];
1102
+ const feeMetadata = pricingConfig?.perBookingFees ?? {};
1103
+ return Object.entries(amountsByName).map(([name, amount]) => ({
1104
+ name,
1105
+ totalAmount: amount,
1106
+ description: feeMetadata[name]?.description,
1107
+ }));
1108
+ }, [currency, isTaxIncludedInPrice, pricingConfig]);
1098
1109
 
1099
1110
  const addOnTotal = useMemo(() => {
1100
1111
  let sum = 0;
@@ -1114,7 +1125,8 @@ export function PrivateShuttleBookingFlow({
1114
1125
  }, [addOnSelections, addOns]);
1115
1126
 
1116
1127
  const additionalHoursAmount = (isAdmin ? additionalHoursCount : 0) * ADDITIONAL_HOUR_PRICE;
1117
- const subtotal = basePrice + addOnTotal + additionalHoursAmount;
1128
+ const perBookingFeeTotal = perBookingFeeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0);
1129
+ const subtotal = basePrice + addOnTotal + additionalHoursAmount + perBookingFeeTotal;
1118
1130
  const effectivePromoDiscountAmount = promoDiscountAmount > 0 ? promoDiscountAmount : 0;
1119
1131
  const taxAmount = isTaxIncludedInPrice ? 0 : subtotal * taxRate;
1120
1132
  const effectiveTaxAmount =
@@ -1792,6 +1804,11 @@ export function PrivateShuttleBookingFlow({
1792
1804
  },
1793
1805
  ]
1794
1806
  : []),
1807
+ ...perBookingFeeLineItems.map((fee) => ({
1808
+ label: fee.name,
1809
+ amount: fee.totalAmount,
1810
+ type: 'FEE' as const,
1811
+ })),
1795
1812
  ...(cancellationPolicyFee > 0 && selectedCancellationPolicy
1796
1813
  ? [
1797
1814
  {
@@ -1929,23 +1946,26 @@ export function PrivateShuttleBookingFlow({
1929
1946
  breakdown: null,
1930
1947
  },
1931
1948
  ],
1932
- feeLineItems: addOnSelections
1933
- .map((sel) => {
1934
- const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
1935
- if (!addOn) return null;
1936
- const base = addOn.price ?? 0;
1937
- const hasVariant =
1938
- (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
1939
- sel.variantId;
1940
- const adj = hasVariant
1941
- ? (addOn.variants?.find((v) => v.id === sel.variantId)?.priceAdjustment ?? 0)
1942
- : 0;
1943
- return {
1944
- name: addOn.name,
1945
- totalAmount: (base + adj) * (sel.quantity ?? 1),
1946
- };
1947
- })
1948
- .filter(Boolean) as { name: string; totalAmount: number }[],
1949
+ feeLineItems: [
1950
+ ...addOnSelections
1951
+ .map((sel) => {
1952
+ const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
1953
+ if (!addOn) return null;
1954
+ const base = addOn.price ?? 0;
1955
+ const hasVariant =
1956
+ (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
1957
+ sel.variantId;
1958
+ const adj = hasVariant
1959
+ ? (addOn.variants?.find((v) => v.id === sel.variantId)?.priceAdjustment ?? 0)
1960
+ : 0;
1961
+ return {
1962
+ name: addOn.name,
1963
+ totalAmount: (base + adj) * (sel.quantity ?? 1),
1964
+ };
1965
+ })
1966
+ .filter(Boolean) as { name: string; totalAmount: number }[],
1967
+ ...perBookingFeeLineItems,
1968
+ ],
1949
1969
  cancellationPolicyFee,
1950
1970
  cancellationPolicyLabel: selectedCancellationPolicy?.label,
1951
1971
  subtotal,
@@ -2036,7 +2056,7 @@ export function PrivateShuttleBookingFlow({
2036
2056
  breakdown: null,
2037
2057
  },
2038
2058
  ];
2039
- const feeLineItemsForModal = addOnSelections
2059
+ const addOnFeeLineItemsForModal = addOnSelections
2040
2060
  .map((sel) => {
2041
2061
  const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
2042
2062
  if (!addOn) return null;
@@ -2053,6 +2073,7 @@ export function PrivateShuttleBookingFlow({
2053
2073
  };
2054
2074
  })
2055
2075
  .filter(Boolean) as { name: string; totalAmount: number }[];
2076
+ const feeLineItemsForModal = [...addOnFeeLineItemsForModal, ...perBookingFeeLineItems];
2056
2077
 
2057
2078
  setCheckoutClientSecret(paymentIntent.clientSecret ?? '');
2058
2079
  setCheckoutModalData({
@@ -2913,6 +2934,12 @@ export function PrivateShuttleBookingFlow({
2913
2934
  },
2914
2935
  ]
2915
2936
  : []),
2937
+ ...perBookingFeeLineItems.map((fee) => ({
2938
+ kind: 'line' as const,
2939
+ label: fee.name,
2940
+ amount: fee.totalAmount,
2941
+ type: 'fee' as const,
2942
+ })),
2916
2943
  ...(cancellationPolicyFee > 0
2917
2944
  ? [
2918
2945
  {
@@ -26,6 +26,17 @@ export type ProviderDashboardChangeBookingPayload = {
26
26
  /** Signed major-unit rows; may include admin custom receipt lines merged with provider inline adjustments. */
27
27
  additionalAdjustments?: Array<{ label: string; amount: number }>;
28
28
  } | null;
29
+ /**
30
+ * Admin/provider change flow: when FE-authoritative quoting is used, apply must persist
31
+ * the exact receipt that was quoted instead of rebuilding from catalog pricing.
32
+ */
33
+ authoritativeReceipt?: {
34
+ currency?: string | null;
35
+ lineItems: Array<{ type: string; label: string; amount: number; quantity?: number; reference?: string }>;
36
+ subtotalBeforeTax: number;
37
+ taxAmount: number;
38
+ totalAmount: number;
39
+ } | null;
29
40
  capacitySeatCredit?: {
30
41
  enabled: boolean;
31
42
  previousPassengerCount?: number | null;
@@ -450,15 +450,26 @@ export function computeOrderSummary(
450
450
  : 0;
451
451
 
452
452
  const fees = pricingConfig.fees ?? {};
453
- const byCurrency = hasFees ? pricingConfig.feesByCurrency?.[currency] : undefined;
454
- const feeLineItems: OrderSummaryFeeLine[] =
455
- !hasFees || totalQuantity === 0 || isTaxIncludedInPrice || byCurrency == null
453
+ const perBookingFees = pricingConfig.perBookingFees ?? {};
454
+ const perPersonByCurrency = hasFees ? pricingConfig.feesByCurrency?.[currency] : undefined;
455
+ const perBookingByCurrency = pricingConfig.perBookingFeesByCurrency?.[currency];
456
+ const perPersonFeeLineItems: OrderSummaryFeeLine[] =
457
+ !hasFees || totalQuantity === 0 || isTaxIncludedInPrice || perPersonByCurrency == null
456
458
  ? []
457
- : Object.entries(byCurrency).map(([name, amountPerPerson]) => ({
459
+ : Object.entries(perPersonByCurrency).map(([name, amountPerPerson]) => ({
458
460
  name,
459
461
  totalAmount: round2(totalQuantity * amountPerPerson),
460
462
  description: fees[name]?.description,
461
463
  }));
464
+ const perBookingFeeLineItems: OrderSummaryFeeLine[] =
465
+ totalQuantity === 0 || isTaxIncludedInPrice || perBookingByCurrency == null
466
+ ? []
467
+ : Object.entries(perBookingByCurrency).map(([name, amountPerBooking]) => ({
468
+ name,
469
+ totalAmount: round2(amountPerBooking),
470
+ description: perBookingFees[name]?.description,
471
+ }));
472
+ const feeLineItems = [...perPersonFeeLineItems, ...perBookingFeeLineItems];
462
473
 
463
474
  const feesTotal = feeLineItems.reduce((s, f) => s + f.totalAmount, 0);
464
475
  const subtotal = round2(basePrice + returnPriceAdjustment + cancellationPolicyFee + feesTotal);
@@ -850,6 +850,8 @@ export interface PricingConfig {
850
850
  currenciesWithTaxIncluded: string[];
851
851
  fees?: Record<string, { feePerPerson: number; description?: string }>;
852
852
  feesByCurrency?: Record<string, Record<string, number>>;
853
+ perBookingFees?: Record<string, { feePerBooking: number; description?: string }>;
854
+ perBookingFeesByCurrency?: Record<string, Record<string, number>>;
853
855
  exchangeRates?: Record<string, number>;
854
856
  cancellationPolicies?: CancellationPolicyOption[];
855
857
  }