@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.139",
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,7 +179,10 @@ 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
187
  const originalReceiptLinesWithComparableBreakdowns = useMemo(
167
188
  () =>
@@ -184,9 +205,9 @@ export function AdminChangeReceiptComparison({
184
205
  [originalReceiptLines],
185
206
  );
186
207
  const amountDueClass =
187
- amountDue < -0.005
208
+ displayedAmountDue < -0.005
188
209
  ? 'text-emerald-700'
189
- : amountDue > 0.005
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(amountDue, currency, displayLocale)}
243
+ {formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
223
244
  </span>
224
245
  </div>
225
- {changeLines.length === 0 && Math.abs(amountDue) < 0.005 ? (
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={amountDue}
254
+ total={displayedAmountDue}
234
255
  currency={currency}
235
256
  locale={displayLocale}
236
- subtotal={changeSubtotal}
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(newReceipt.total, currency, displayLocale)}
273
+ {formatCurrencyAmount(displayedUpdatedTotal, currency, displayLocale)}
253
274
  </span>
254
275
  </div>
255
276
  <PriceSummary
256
- lines={newPriceSummaryLines.map((line) => ({ ...line, editable: false, lineKey: undefined }))}
257
- total={newReceipt.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={newReceipt.subtotal}
261
- taxAmount={!newPriceSummaryLinesIncludeTaxRow ? newReceipt.tax : 0}
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(amountDue, currency, displayLocale)}
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
- 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> {