@ticketboothapp/booking 1.2.139 → 1.2.141

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.141",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -155,6 +155,7 @@ const checkoutPanelPropKeys = [
155
155
  'receiptTotal',
156
156
  'originalReceipt',
157
157
  'priceSummaryLinesIncludeTaxRow',
158
+ 'isServerAmendmentQuote',
158
159
  'isTaxIncludedInPrice',
159
160
  'taxRate',
160
161
  'currency',
@@ -971,6 +971,7 @@ export function AdminChangeBookingFlow({
971
971
  receiptTax: adminFeAuthoritativeReceipt.tax,
972
972
  receiptTotal: adminFeAuthoritativeReceipt.total,
973
973
  originalReceipt,
974
+ isServerAmendmentQuote: latestChangeQuote?.pricingVersion === 'booking-amendment-v1',
974
975
  ...{ priceSummaryLinesIncludeTaxRow, isTaxIncludedInPrice, showProviderPricingInlineEditor },
975
976
  taxRate: pricingConfig?.taxRate,
976
977
  ...{ providerPricingUi, providerQuotedLines },
@@ -30,6 +30,7 @@ export interface AdminChangeCheckoutPanelProps {
30
30
  receiptTotal: number;
31
31
  originalReceipt: ChangeBookingFlowProps['originalReceipt'];
32
32
  priceSummaryLinesIncludeTaxRow: boolean;
33
+ isServerAmendmentQuote?: boolean;
33
34
  isTaxIncludedInPrice: boolean;
34
35
  taxRate?: number;
35
36
  currency: Currency;
@@ -99,6 +100,7 @@ export function AdminChangeCheckoutPanel({
99
100
  receiptTotal,
100
101
  originalReceipt,
101
102
  priceSummaryLinesIncludeTaxRow,
103
+ isServerAmendmentQuote,
102
104
  isTaxIncludedInPrice,
103
105
  taxRate,
104
106
  currency,
@@ -188,6 +190,7 @@ export function AdminChangeCheckoutPanel({
188
190
  }}
189
191
  newPriceSummaryLines={priceSummaryLines}
190
192
  newPriceSummaryLinesIncludeTaxRow={priceSummaryLinesIncludeTaxRow}
193
+ isServerAmendmentQuote={isServerAmendmentQuote}
191
194
  amountDue={changeFlowAmountDue}
192
195
  amountDueLabel={totalSummaryLabel}
193
196
  currency={currency}
@@ -16,6 +16,7 @@ export interface AdminChangeReceiptComparisonProps {
16
16
  newReceipt: Pick<AdminFeAuthoritativeReceipt, 'subtotal' | 'tax' | 'total'>;
17
17
  newPriceSummaryLines: PriceSummaryLine[];
18
18
  newPriceSummaryLinesIncludeTaxRow: boolean;
19
+ isServerAmendmentQuote?: boolean;
19
20
  amountDue: number;
20
21
  amountDueLabel: string;
21
22
  currency: Currency;
@@ -84,12 +85,17 @@ function amendmentDeltaLines(
84
85
 
85
86
  return proposedLines.flatMap((line): PriceSummaryLine[] => {
86
87
  const original = remainingOriginal.get(lineIdentity(line));
87
- const amount = roundMoney(lineAmount(line) - (original?.amount ?? 0));
88
88
  const proposedQuantity = lineQuantity(line);
89
89
  const quantity =
90
90
  proposedQuantity == null && original?.quantity == null
91
91
  ? null
92
92
  : (proposedQuantity ?? 0) - (original?.quantity ?? 0);
93
+ const proposedAmount = lineAmount(line);
94
+ const amount = roundMoney(
95
+ quantity != null && quantity > 0 && proposedQuantity != null && proposedQuantity > 0
96
+ ? (proposedAmount / proposedQuantity) * quantity
97
+ : proposedAmount - (original?.amount ?? 0),
98
+ );
93
99
  if (Math.abs(amount) < 0.005 && (quantity == null || quantity === 0)) return [];
94
100
 
95
101
  if (line.kind === 'ticket') {
@@ -104,7 +110,9 @@ function amendmentDeltaLines(
104
110
  const isTax = String(line.type ?? '').toUpperCase() === 'TAX';
105
111
  return [{
106
112
  ...line,
107
- label: isTax ? (amount >= 0 ? 'Additional tax' : 'Tax reduction') : line.label,
113
+ label: isTax
114
+ ? (amount >= 0 ? 'Additional tax' : 'Tax reduction')
115
+ : line.label.replace(/\s*\([^)]*(?:people|person|x\s*\d+)[^)]*\)\s*$/i, '').trim(),
108
116
  amount,
109
117
  quantity: quantity == null ? null : Math.abs(quantity),
110
118
  editable: false,
@@ -117,8 +125,7 @@ export function AdminChangeReceiptComparison({
117
125
  originalReceipt,
118
126
  newReceipt,
119
127
  newPriceSummaryLines,
120
- newPriceSummaryLinesIncludeTaxRow,
121
- amountDue,
128
+ isServerAmendmentQuote = false,
122
129
  amountDueLabel,
123
130
  currency,
124
131
  locale,
@@ -132,11 +139,56 @@ export function AdminChangeReceiptComparison({
132
139
  [originalReceipt],
133
140
  );
134
141
  const originalLinesIncludeTaxRow = hasTaxLine(originalReceiptLines);
135
- const calculatedChangeLines = useMemo(
136
- () => amendmentDeltaLines(originalReceiptLines, newPriceSummaryLines),
137
- [newPriceSummaryLines, originalReceiptLines],
142
+ const calculatedChangeLines = useMemo(() => {
143
+ if (!isServerAmendmentQuote) {
144
+ return amendmentDeltaLines(originalReceiptLines, newPriceSummaryLines);
145
+ }
146
+ const nonTax = newPriceSummaryLines.filter(
147
+ (line) => !(line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'),
148
+ );
149
+ const tax = roundMoney(
150
+ newPriceSummaryLines.reduce(
151
+ (sum, line) =>
152
+ line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
153
+ ? sum + line.amount
154
+ : sum,
155
+ 0,
156
+ ),
157
+ );
158
+ return Math.abs(tax) < 0.005
159
+ ? nonTax
160
+ : [
161
+ ...nonTax,
162
+ {
163
+ kind: 'line' as const,
164
+ label: tax >= 0 ? 'Additional tax' : 'Tax reduction',
165
+ amount: tax,
166
+ type: 'TAX',
167
+ },
168
+ ];
169
+ }, [isServerAmendmentQuote, newPriceSummaryLines, originalReceiptLines]);
170
+ const nonTaxChangeSubtotal = roundMoney(
171
+ calculatedChangeLines.reduce(
172
+ (sum, line) =>
173
+ line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
174
+ ? sum
175
+ : sum + lineAmount(line),
176
+ 0,
177
+ ),
178
+ );
179
+ const receiptTaxDelta = roundMoney(
180
+ isServerAmendmentQuote
181
+ ? calculatedChangeLines.reduce(
182
+ (sum, line) =>
183
+ line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
184
+ ? sum + line.amount
185
+ : sum,
186
+ 0,
187
+ )
188
+ : taxRate != null && nonTaxChangeSubtotal > 0
189
+ ? nonTaxChangeSubtotal * taxRate
190
+ : newReceipt.tax - originalReceipt.tax,
138
191
  );
139
- const receiptTaxDelta = roundMoney(newReceipt.tax - originalReceipt.tax);
140
192
  const changeLines = useMemo(() => {
141
193
  if (hasTaxLine(calculatedChangeLines) || Math.abs(receiptTaxDelta) < 0.005) {
142
194
  return calculatedChangeLines;
@@ -161,7 +213,10 @@ export function AdminChangeReceiptComparison({
161
213
  0,
162
214
  ),
163
215
  );
164
- const changeSubtotal = roundMoney(amountDue - changeTax);
216
+ const displayedAmountDue = roundMoney(changeLines.reduce((sum, line) => sum + lineAmount(line), 0));
217
+ const displayedUpdatedTotal = roundMoney(originalReceipt.total + displayedAmountDue);
218
+ const displayedUpdatedSubtotal = roundMoney(originalReceipt.subtotal + nonTaxChangeSubtotal);
219
+ const displayedUpdatedTax = roundMoney(originalReceipt.tax + changeTax);
165
220
  const originalCurrency = originalReceipt.currency ?? currency;
166
221
  const originalReceiptLinesWithComparableBreakdowns = useMemo(
167
222
  () =>
@@ -184,9 +239,9 @@ export function AdminChangeReceiptComparison({
184
239
  [originalReceiptLines],
185
240
  );
186
241
  const amountDueClass =
187
- amountDue < -0.005
242
+ displayedAmountDue < -0.005
188
243
  ? 'text-emerald-700'
189
- : amountDue > 0.005
244
+ : displayedAmountDue > 0.005
190
245
  ? 'text-stone-900'
191
246
  : 'text-stone-700';
192
247
 
@@ -219,10 +274,10 @@ export function AdminChangeReceiptComparison({
219
274
  <p className="mt-0.5 text-xs text-stone-500">Only newly charged, credited, or repriced value</p>
220
275
  </div>
221
276
  <span className="shrink-0 text-xs font-medium text-stone-500">
222
- {formatCurrencyAmount(amountDue, currency, displayLocale)}
277
+ {formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
223
278
  </span>
224
279
  </div>
225
- {changeLines.length === 0 && Math.abs(amountDue) < 0.005 ? (
280
+ {changeLines.length === 0 && Math.abs(displayedAmountDue) < 0.005 ? (
226
281
  <div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
227
282
  <p className="text-sm font-medium text-stone-700">No pricing changes</p>
228
283
  <p className="mt-1 text-xs text-stone-500">The selected booking details match the existing purchase.</p>
@@ -230,10 +285,10 @@ export function AdminChangeReceiptComparison({
230
285
  ) : (
231
286
  <PriceSummary
232
287
  lines={changeLines}
233
- total={amountDue}
288
+ total={displayedAmountDue}
234
289
  currency={currency}
235
290
  locale={displayLocale}
236
- subtotal={changeSubtotal}
291
+ subtotal={nonTaxChangeSubtotal}
237
292
  taxAmount={!changeLinesIncludeTaxRow ? changeTax : 0}
238
293
  taxRate={taxRate}
239
294
  size="sm"
@@ -249,16 +304,23 @@ export function AdminChangeReceiptComparison({
249
304
  <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
305
  </div>
251
306
  <span className="shrink-0 text-xs font-medium text-stone-500">
252
- {formatCurrencyAmount(newReceipt.total, currency, displayLocale)}
307
+ {formatCurrencyAmount(displayedUpdatedTotal, currency, displayLocale)}
253
308
  </span>
254
309
  </div>
255
310
  <PriceSummary
256
- lines={newPriceSummaryLines.map((line) => ({ ...line, editable: false, lineKey: undefined }))}
257
- total={newReceipt.total}
311
+ lines={[
312
+ ...originalReceiptLines.filter(
313
+ (line) => !(line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'),
314
+ ),
315
+ ...changeLines.filter(
316
+ (line) => !(line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'),
317
+ ),
318
+ ].map((line) => ({ ...line, editable: false, lineKey: undefined }))}
319
+ total={displayedUpdatedTotal}
258
320
  currency={currency}
259
321
  locale={displayLocale}
260
- subtotal={newReceipt.subtotal}
261
- taxAmount={!newPriceSummaryLinesIncludeTaxRow ? newReceipt.tax : 0}
322
+ subtotal={displayedUpdatedSubtotal}
323
+ taxAmount={displayedUpdatedTax}
262
324
  taxRate={taxRate}
263
325
  size="sm"
264
326
  t={t}
@@ -276,7 +338,7 @@ export function AdminChangeReceiptComparison({
276
338
  <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
339
  <span className="text-sm font-semibold text-stone-900">{amountDueLabel}</span>
278
340
  <span className={`text-lg font-semibold tabular-nums ${amountDueClass}`}>
279
- {formatCurrencyAmount(amountDue, currency, displayLocale)}
341
+ {formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
280
342
  </span>
281
343
  </div>
282
344
  </div>
@@ -336,7 +336,7 @@ export function useAdminChangePriceSummary({
336
336
 
337
337
  const checkoutPriceSummaryLinesForCheckout = useMemo(() => {
338
338
  let raw: PriceSummaryLine[];
339
- if (!useAdminFeAuthoritativeQuote && suppressSelfServeCurrencyUi && selfServePricingConfirmed) {
339
+ if (suppressSelfServeCurrencyUi && selfServePricingConfirmed) {
340
340
  raw =
341
341
  latestServerPriceSummaryLines && latestServerPriceSummaryLines.length > 0
342
342
  ? latestServerPriceSummaryLines
@@ -366,7 +366,6 @@ export function useAdminChangePriceSummary({
366
366
  }, [
367
367
  suppressSelfServeCurrencyUi,
368
368
  selfServePricingConfirmed,
369
- useAdminFeAuthoritativeQuote,
370
369
  checkoutPriceSummaryLines,
371
370
  latestServerPriceSummaryLines,
372
371
  effectivePromoDiscountAmount,
@@ -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';
@@ -262,9 +261,11 @@ export function useAdminChangeQuotePreview({
262
261
  hasChangesFromInitial: hasEffectiveChangeSelection,
263
262
  selectionTotal:
264
263
  originalReceipt
265
- ? suppressSelfServeCurrencyUi && !selfServePricingConfirmed && !useAdminFeAuthoritativeQuote
266
- ? null
267
- : displayChangeFlowProposedTotalWithEditableLines
264
+ ? selfServePricingConfirmed && latestChangeQuote?.serverPreview?.totalNewBooking != null
265
+ ? latestChangeQuote.serverPreview.totalNewBooking
266
+ : suppressSelfServeCurrencyUi
267
+ ? null
268
+ : displayChangeFlowProposedTotalWithEditableLines
268
269
  : totalPrice,
269
270
  selectionCurrency: currency,
270
271
  };
@@ -282,7 +283,7 @@ export function useAdminChangeQuotePreview({
282
283
  displayChangeFlowProposedTotalWithEditableLines,
283
284
  suppressSelfServeCurrencyUi,
284
285
  selfServePricingConfirmed,
285
- useAdminFeAuthoritativeQuote,
286
+ latestChangeQuote?.serverPreview?.totalNewBooking,
286
287
  ]);
287
288
 
288
289
  useEffect(() => {
@@ -423,18 +424,11 @@ export function useAdminChangeQuotePreview({
423
424
  previousReturnAvailabilityId: initialValues.returnAvailabilityId ?? null,
424
425
  },
425
426
  };
426
- const deterministicClientProposedTotal = useAdminFeAuthoritativeQuote
427
- ? adminFeAuthoritativeReceipt.total
428
- : changeFlowNewBookingTotal;
429
- const deterministicAmountDue = originalReceipt
430
- ? roundMoney(adminFeAuthoritativeReceipt.total - originalReceipt.total)
431
- : roundMoney(changeFlowClientEstimateDue);
432
427
  const quote = useAdminFeAuthoritativeQuote
433
- ? await quoteChangeBookingAdminFeReceipt({
428
+ ? await quoteAdminChangeBookingV2({
434
429
  ...quoteRequestBase,
435
- feReceipt: adminFeAuthoritativeReceipt,
436
- feAmountDueMajorUnits: deterministicAmountDue,
437
- clientProposedTotal: deterministicClientProposedTotal,
430
+ // V2 is server-authoritative. Do not bind it to the outbound-only FE estimate.
431
+ clientProposedTotal: undefined,
438
432
  })
439
433
  : await quoteChangeBooking(quoteRequestBase);
440
434
  if (seq !== changeQuoteRequestSeq.current) return;
@@ -188,7 +188,12 @@ export function useAdminChangeReceiptDerivation({
188
188
  });
189
189
  })();
190
190
  const changeFlowClientEstimateDue = normalizeNearZeroOwed(
191
- roundMoney(changeFlowClientEstimateDueBase + editableSummaryPreSubtotalDelta),
191
+ roundMoney(
192
+ changeFlowClientEstimateDueBase +
193
+ (latestChangeQuote?.pricingVersion === 'booking-amendment-v1'
194
+ ? 0
195
+ : editableSummaryPreSubtotalDelta),
196
+ ),
192
197
  );
193
198
  const changeFlowAmountDue = normalizeNearZeroOwed(changeFlowClientEstimateDue);
194
199
 
@@ -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> {