@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.138",
3
+ "version": "1.2.140",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -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 ? (amount >= 0 ? 'Additional tax' : 'Tax reduction') : line.label,
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 receiptTaxDelta = roundMoney(newReceipt.tax - originalReceipt.tax);
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 changeSubtotal = roundMoney(amountDue - changeTax);
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 originalReceiptLinesWithUnitAmounts = useMemo(
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
- unitAmountLabel: `${formatCurrencyAmount(
173
- roundMoney(line.itemTotal / line.qty),
174
- originalCurrency,
175
- displayLocale,
176
- )} each`,
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
- [displayLocale, originalCurrency, originalReceiptLines],
205
+ [originalReceiptLines],
181
206
  );
182
207
  const amountDueClass =
183
- amountDue < -0.005
208
+ displayedAmountDue < -0.005
184
209
  ? 'text-emerald-700'
185
- : amountDue > 0.005
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={originalReceiptLinesWithUnitAmounts}
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(amountDue, currency, displayLocale)}
243
+ {formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
219
244
  </span>
220
245
  </div>
221
- {changeLines.length === 0 && Math.abs(amountDue) < 0.005 ? (
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={amountDue}
254
+ total={displayedAmountDue}
230
255
  currency={currency}
231
256
  locale={displayLocale}
232
- subtotal={changeSubtotal}
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(newReceipt.total, currency, displayLocale)}
273
+ {formatCurrencyAmount(displayedUpdatedTotal, currency, displayLocale)}
249
274
  </span>
250
275
  </div>
251
276
  <PriceSummary
252
- lines={newPriceSummaryLines.map((line) => ({ ...line, editable: false, lineKey: undefined }))}
253
- total={newReceipt.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={newReceipt.subtotal}
257
- taxAmount={!newPriceSummaryLinesIncludeTaxRow ? newReceipt.tax : 0}
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(amountDue, currency, displayLocale)}
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
- quoteChangeBookingAdminFeReceipt,
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 quoteChangeBookingAdminFeReceipt({
426
+ ? await quoteAdminChangeBookingV2({
434
427
  ...quoteRequestBase,
435
- feReceipt: adminFeAuthoritativeReceipt,
436
- feAmountDueMajorUnits: deterministicAmountDue,
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;
@@ -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> {