@ticketboothapp/booking 1.2.141 → 1.2.142

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.141",
3
+ "version": "1.2.142",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -155,7 +155,9 @@ const checkoutPanelPropKeys = [
155
155
  'receiptTotal',
156
156
  'originalReceipt',
157
157
  'priceSummaryLinesIncludeTaxRow',
158
- 'isServerAmendmentQuote',
158
+ 'serverAmendmentLines',
159
+ 'serverAmountDue',
160
+ 'serverUpdatedTotal',
159
161
  'isTaxIncludedInPrice',
160
162
  'taxRate',
161
163
  'currency',
@@ -971,7 +971,18 @@ export function AdminChangeBookingFlow({
971
971
  receiptTax: adminFeAuthoritativeReceipt.tax,
972
972
  receiptTotal: adminFeAuthoritativeReceipt.total,
973
973
  originalReceipt,
974
- isServerAmendmentQuote: latestChangeQuote?.pricingVersion === 'booking-amendment-v1',
974
+ serverAmendmentLines:
975
+ latestChangeQuote?.pricingVersion === 'booking-amendment-v1'
976
+ ? latestChangeQuote.serverPreview?.priceSummaryLines ?? []
977
+ : null,
978
+ serverAmountDue:
979
+ latestChangeQuote?.pricingVersion === 'booking-amendment-v1'
980
+ ? latestChangeQuote.serverPreview?.amountDue ?? null
981
+ : null,
982
+ serverUpdatedTotal:
983
+ latestChangeQuote?.pricingVersion === 'booking-amendment-v1'
984
+ ? latestChangeQuote.serverPreview?.totalNewBooking ?? null
985
+ : null,
975
986
  ...{ priceSummaryLinesIncludeTaxRow, isTaxIncludedInPrice, showProviderPricingInlineEditor },
976
987
  taxRate: pricingConfig?.taxRate,
977
988
  ...{ providerPricingUi, providerQuotedLines },
@@ -30,7 +30,9 @@ export interface AdminChangeCheckoutPanelProps {
30
30
  receiptTotal: number;
31
31
  originalReceipt: ChangeBookingFlowProps['originalReceipt'];
32
32
  priceSummaryLinesIncludeTaxRow: boolean;
33
- isServerAmendmentQuote?: boolean;
33
+ serverAmendmentLines: PriceSummaryLine[] | null;
34
+ serverAmountDue: number | null;
35
+ serverUpdatedTotal: number | null;
34
36
  isTaxIncludedInPrice: boolean;
35
37
  taxRate?: number;
36
38
  currency: Currency;
@@ -100,7 +102,9 @@ export function AdminChangeCheckoutPanel({
100
102
  receiptTotal,
101
103
  originalReceipt,
102
104
  priceSummaryLinesIncludeTaxRow,
103
- isServerAmendmentQuote,
105
+ serverAmendmentLines,
106
+ serverAmountDue,
107
+ serverUpdatedTotal,
104
108
  isTaxIncludedInPrice,
105
109
  taxRate,
106
110
  currency,
@@ -183,26 +187,15 @@ export function AdminChangeCheckoutPanel({
183
187
  isAdmin && originalReceipt ? (
184
188
  <AdminChangeReceiptComparison
185
189
  originalReceipt={originalReceipt}
186
- newReceipt={{
187
- subtotal: receiptSubtotal,
188
- tax: receiptTax,
189
- total: receiptTotal,
190
- }}
191
- newPriceSummaryLines={priceSummaryLines}
192
- newPriceSummaryLinesIncludeTaxRow={priceSummaryLinesIncludeTaxRow}
193
- isServerAmendmentQuote={isServerAmendmentQuote}
194
- amountDue={changeFlowAmountDue}
190
+ amendmentLines={serverAmendmentLines}
191
+ amountDue={serverAmountDue}
192
+ updatedTotal={serverUpdatedTotal}
195
193
  amountDueLabel={totalSummaryLabel}
196
194
  currency={currency}
197
195
  locale={locale}
198
196
  t={t}
199
197
  taxRate={taxRate}
200
- newReceiptAdjustments={pricingAdjustments}
201
- lineAmountInputs={lineAmountInputs}
202
- onLineAmountInputChange={onLineAmountInputChange}
203
- onLineAmountInputBlur={onLineAmountInputBlur}
204
- onLineAmountReset={onLineAmountReset}
205
- lineLabelInputs={lineLabelInputs}
198
+ adjustments={pricingAdjustments}
206
199
  />
207
200
  ) : null;
208
201
 
@@ -1,6 +1,4 @@
1
- import { useMemo, type ReactNode } from 'react';
2
- import type { AdminFeAuthoritativeReceipt } from '../../lib/booking-api';
3
- import { roundMoney } from '../../lib/booking/change-flow-pricing';
1
+ import type { ReactNode } from 'react';
4
2
  import { mapQuoteLineItemsToPriceSummaryLines } from '../../lib/booking/change-booking-server-preview';
5
3
  import { formatCurrencyAmount } from '../../lib/currency';
6
4
  import type { Locale } from '../../lib/booking/i18n/config';
@@ -13,240 +11,61 @@ type OriginalReceipt = NonNullable<ChangeBookingFlowProps['originalReceipt']>;
13
11
 
14
12
  export interface AdminChangeReceiptComparisonProps {
15
13
  originalReceipt: OriginalReceipt;
16
- newReceipt: Pick<AdminFeAuthoritativeReceipt, 'subtotal' | 'tax' | 'total'>;
17
- newPriceSummaryLines: PriceSummaryLine[];
18
- newPriceSummaryLinesIncludeTaxRow: boolean;
19
- isServerAmendmentQuote?: boolean;
20
- amountDue: number;
14
+ /** Amendment-only rows returned by the Pricing V2 server quote. */
15
+ amendmentLines: PriceSummaryLine[] | null;
16
+ /** Signed server balance delta. Never calculated by this component. */
17
+ amountDue: number | null;
18
+ /** Full resulting booking total returned by the same server quote. */
19
+ updatedTotal: number | null;
21
20
  amountDueLabel: string;
22
21
  currency: Currency;
23
22
  locale: string;
24
23
  t: TranslationFn;
25
24
  taxRate?: number;
26
- newReceiptAdjustments?: ReactNode;
27
- lineAmountInputs?: Record<string, string>;
28
- onLineAmountInputChange?: (lineKey: string, value: string) => void;
29
- onLineAmountInputBlur?: (lineKey: string) => void;
30
- onLineAmountReset?: (lineKey: string) => void;
31
- lineLabelInputs?: Record<string, string>;
25
+ adjustments?: ReactNode;
32
26
  }
33
27
 
34
28
  function normalizeLocale(locale: string): Locale {
35
29
  return locale === 'fr' ? 'fr' : 'en';
36
30
  }
37
31
 
32
+ function originalLines(receipt: OriginalReceipt): PriceSummaryLine[] {
33
+ return receipt.lineItems?.length
34
+ ? mapQuoteLineItemsToPriceSummaryLines(receipt.lineItems)
35
+ : [];
36
+ }
37
+
38
38
  function hasTaxLine(lines: PriceSummaryLine[]): boolean {
39
39
  return lines.some(
40
40
  (line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX',
41
41
  );
42
42
  }
43
43
 
44
- function mapOriginalReceiptLines(originalReceipt: OriginalReceipt): PriceSummaryLine[] {
45
- if (!originalReceipt.lineItems?.length) return [];
46
- return mapQuoteLineItemsToPriceSummaryLines(originalReceipt.lineItems);
47
- }
48
-
49
- function lineIdentity(line: PriceSummaryLine): string {
50
- const normalizedLabel = (value: string) =>
51
- value
52
- .replace(/\s*\([^)]*(?:people|person|x\s*\d+)[^)]*\)\s*$/i, '')
53
- .replace(/\s+/g, ' ')
54
- .trim()
55
- .toUpperCase();
56
- return line.kind === 'ticket'
57
- ? `ticket:${line.category.trim().toUpperCase()}`
58
- : `line:${String(line.type ?? '').trim().toUpperCase()}:${normalizedLabel(line.label)}`;
59
- }
60
-
61
- function lineAmount(line: PriceSummaryLine): number {
62
- return line.kind === 'ticket' ? line.itemTotal : line.amount;
63
- }
64
-
65
- function lineQuantity(line: PriceSummaryLine): number | null {
66
- return line.kind === 'ticket' ? line.qty : line.quantity ?? null;
67
- }
68
-
69
- function amendmentDeltaLines(
70
- originalLines: PriceSummaryLine[],
71
- proposedLines: PriceSummaryLine[],
72
- ): PriceSummaryLine[] {
73
- const remainingOriginal = new Map<string, { amount: number; quantity: number | null }>();
74
- for (const line of originalLines) {
75
- const key = lineIdentity(line);
76
- const previous = remainingOriginal.get(key);
77
- remainingOriginal.set(key, {
78
- amount: roundMoney((previous?.amount ?? 0) + lineAmount(line)),
79
- quantity:
80
- previous?.quantity == null && lineQuantity(line) == null
81
- ? null
82
- : (previous?.quantity ?? 0) + (lineQuantity(line) ?? 0),
83
- });
84
- }
85
-
86
- return proposedLines.flatMap((line): PriceSummaryLine[] => {
87
- const original = remainingOriginal.get(lineIdentity(line));
88
- const proposedQuantity = lineQuantity(line);
89
- const quantity =
90
- proposedQuantity == null && original?.quantity == null
91
- ? null
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
- );
99
- if (Math.abs(amount) < 0.005 && (quantity == null || quantity === 0)) return [];
100
-
101
- if (line.kind === 'ticket') {
102
- return [{
103
- ...line,
104
- qty: Math.abs(quantity ?? 0) || line.qty,
105
- itemTotal: amount,
106
- editable: false,
107
- lineKey: undefined,
108
- }];
109
- }
110
- const isTax = String(line.type ?? '').toUpperCase() === 'TAX';
111
- return [{
112
- ...line,
113
- label: isTax
114
- ? (amount >= 0 ? 'Additional tax' : 'Tax reduction')
115
- : line.label.replace(/\s*\([^)]*(?:people|person|x\s*\d+)[^)]*\)\s*$/i, '').trim(),
116
- amount,
117
- quantity: quantity == null ? null : Math.abs(quantity),
118
- editable: false,
119
- lineKey: undefined,
120
- }];
121
- });
122
- }
123
-
124
44
  export function AdminChangeReceiptComparison({
125
45
  originalReceipt,
126
- newReceipt,
127
- newPriceSummaryLines,
128
- isServerAmendmentQuote = false,
46
+ amendmentLines,
47
+ amountDue,
48
+ updatedTotal,
129
49
  amountDueLabel,
130
50
  currency,
131
51
  locale,
132
52
  t,
133
53
  taxRate,
134
- newReceiptAdjustments,
54
+ adjustments,
135
55
  }: AdminChangeReceiptComparisonProps) {
136
56
  const displayLocale = normalizeLocale(locale);
137
- const originalReceiptLines = useMemo(
138
- () => mapOriginalReceiptLines(originalReceipt),
139
- [originalReceipt],
140
- );
141
- const originalLinesIncludeTaxRow = hasTaxLine(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,
191
- );
192
- const changeLines = useMemo(() => {
193
- if (hasTaxLine(calculatedChangeLines) || Math.abs(receiptTaxDelta) < 0.005) {
194
- return calculatedChangeLines;
195
- }
196
- return [
197
- ...calculatedChangeLines,
198
- {
199
- kind: 'line' as const,
200
- label: receiptTaxDelta >= 0 ? 'Additional tax' : 'Tax reduction',
201
- amount: receiptTaxDelta,
202
- type: 'TAX',
203
- },
204
- ];
205
- }, [calculatedChangeLines, receiptTaxDelta]);
206
- const changeLinesIncludeTaxRow = hasTaxLine(changeLines);
207
- const changeTax = roundMoney(
208
- changeLines.reduce(
209
- (sum, line) =>
210
- line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
211
- ? sum + line.amount
212
- : sum,
213
- 0,
214
- ),
215
- );
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);
57
+ const existingLines = originalLines(originalReceipt);
220
58
  const originalCurrency = originalReceipt.currency ?? currency;
221
- const originalReceiptLinesWithComparableBreakdowns = useMemo(
222
- () =>
223
- originalReceiptLines.map((line) =>
224
- line.kind === 'ticket' && line.qty > 0
225
- ? {
226
- ...line,
227
- breakdown: line.breakdown ?? {
228
- lineItems: [],
229
- subtotalAfterAdjustments: roundMoney(line.itemTotal / line.qty),
230
- feeLines: [],
231
- taxRate: 0,
232
- taxAmount: 0,
233
- isTaxIncluded: false,
234
- finalPrice: roundMoney(line.itemTotal / line.qty),
235
- },
236
- }
237
- : line,
238
- ),
239
- [originalReceiptLines],
240
- );
59
+ const quoteReady = amendmentLines != null && amountDue != null && updatedTotal != null;
241
60
  const amountDueClass =
242
- displayedAmountDue < -0.005
61
+ amountDue != null && amountDue < -0.005
243
62
  ? 'text-emerald-700'
244
- : displayedAmountDue > 0.005
63
+ : amountDue != null && amountDue > 0.005
245
64
  ? 'text-stone-900'
246
65
  : 'text-stone-700';
247
66
 
248
67
  return (
249
- <div className="space-y-3 min-w-0 overflow-visible">
68
+ <div className="min-w-0 space-y-3 overflow-visible">
250
69
  <div className="grid gap-4 md:grid-cols-2">
251
70
  <section className="min-w-0 overflow-visible rounded-lg border border-stone-200 bg-stone-50/70 p-3">
252
71
  <div className="mb-3 flex items-center justify-between gap-3">
@@ -256,89 +75,72 @@ export function AdminChangeReceiptComparison({
256
75
  </span>
257
76
  </div>
258
77
  <PriceSummary
259
- lines={originalReceiptLinesWithComparableBreakdowns}
78
+ lines={existingLines}
260
79
  total={originalReceipt.total}
261
80
  currency={originalCurrency}
262
81
  locale={displayLocale}
263
82
  subtotal={originalReceipt.subtotal}
264
- taxAmount={!originalLinesIncludeTaxRow ? originalReceipt.tax : 0}
83
+ taxAmount={hasTaxLine(existingLines) ? 0 : originalReceipt.tax}
265
84
  taxRate={taxRate}
266
85
  size="sm"
267
86
  t={t}
268
87
  />
269
88
  </section>
89
+
270
90
  <section className="min-w-0 overflow-visible rounded-lg border border-sky-200 bg-sky-50/50 p-3">
271
91
  <div className="mb-3 flex items-center justify-between gap-3">
272
92
  <div>
273
93
  <h3 className="text-sm font-semibold text-stone-900">This change</h3>
274
- <p className="mt-0.5 text-xs text-stone-500">Only newly charged, credited, or repriced value</p>
94
+ <p className="mt-0.5 text-xs text-stone-500">Server-priced amendment</p>
275
95
  </div>
276
96
  <span className="shrink-0 text-xs font-medium text-stone-500">
277
- {formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
97
+ {amountDue == null ? '—' : formatCurrencyAmount(amountDue, currency, displayLocale)}
278
98
  </span>
279
99
  </div>
280
- {changeLines.length === 0 && Math.abs(displayedAmountDue) < 0.005 ? (
100
+ {!quoteReady ? (
101
+ <div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
102
+ <p className="text-sm font-medium text-stone-700">Waiting for server price…</p>
103
+ </div>
104
+ ) : amendmentLines.length === 0 && Math.abs(amountDue) < 0.005 ? (
281
105
  <div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
282
106
  <p className="text-sm font-medium text-stone-700">No pricing changes</p>
283
- <p className="mt-1 text-xs text-stone-500">The selected booking details match the existing purchase.</p>
284
107
  </div>
285
108
  ) : (
286
109
  <PriceSummary
287
- lines={changeLines}
288
- total={displayedAmountDue}
110
+ lines={amendmentLines}
111
+ total={amountDue}
289
112
  currency={currency}
290
113
  locale={displayLocale}
291
- subtotal={nonTaxChangeSubtotal}
292
- taxAmount={!changeLinesIncludeTaxRow ? changeTax : 0}
293
- taxRate={taxRate}
294
114
  size="sm"
295
115
  t={t}
296
116
  />
297
117
  )}
298
118
  </section>
299
119
  </div>
300
- <section className="min-w-0 overflow-visible rounded-lg border border-stone-300 bg-white p-3 shadow-sm">
301
- <div className="mb-3 flex items-center justify-between gap-3">
120
+
121
+ {quoteReady ? (
122
+ <section className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-stone-300 bg-white px-3 py-3 shadow-sm">
302
123
  <div>
303
- <h3 className="text-sm font-semibold text-stone-900">Updated booking receipt</h3>
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>
124
+ <h3 className="text-sm font-semibold text-stone-900">Updated booking total</h3>
125
+ <p className="mt-0.5 text-xs text-stone-500">Authoritative total returned by the server quote</p>
305
126
  </div>
306
- <span className="shrink-0 text-xs font-medium text-stone-500">
307
- {formatCurrencyAmount(displayedUpdatedTotal, currency, displayLocale)}
127
+ <span className="text-lg font-semibold tabular-nums text-stone-900">
128
+ {formatCurrencyAmount(updatedTotal, currency, displayLocale)}
308
129
  </span>
309
- </div>
310
- <PriceSummary
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}
320
- currency={currency}
321
- locale={displayLocale}
322
- subtotal={displayedUpdatedSubtotal}
323
- taxAmount={displayedUpdatedTax}
324
- taxRate={taxRate}
325
- size="sm"
326
- t={t}
327
- />
328
- </section>
329
- {newReceiptAdjustments ? (
130
+ </section>
131
+ ) : null}
132
+
133
+ {adjustments ? (
330
134
  <section className="rounded-lg border border-amber-200 bg-amber-50/40 p-3">
331
- <div className="mb-2">
332
- <h3 className="text-sm font-semibold text-stone-900">Admin adjustments</h3>
333
- <p className="mt-0.5 text-xs text-stone-500">Optional structured charges or credits applied to this amendment</p>
334
- </div>
335
- {newReceiptAdjustments}
135
+ <h3 className="mb-2 text-sm font-semibold text-stone-900">Admin adjustments</h3>
136
+ {adjustments}
336
137
  </section>
337
138
  ) : null}
139
+
338
140
  <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
339
141
  <span className="text-sm font-semibold text-stone-900">{amountDueLabel}</span>
340
142
  <span className={`text-lg font-semibold tabular-nums ${amountDueClass}`}>
341
- {formatCurrencyAmount(displayedAmountDue, currency, displayLocale)}
143
+ {amountDue == null ? '—' : formatCurrencyAmount(amountDue, currency, displayLocale)}
342
144
  </span>
343
145
  </div>
344
146
  </div>