@ticketboothapp/booking 1.2.134 → 1.2.136

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.134",
3
+ "version": "1.2.136",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -76,6 +76,74 @@ function mapOriginalReceiptLines(originalReceipt: OriginalReceipt): PriceSummary
76
76
  return withStoredReceiptBreakdowns(mapQuoteLineItemsToPriceSummaryLines(originalReceipt.lineItems));
77
77
  }
78
78
 
79
+ function lineIdentity(line: PriceSummaryLine): string {
80
+ const normalizedLabel = (value: string) =>
81
+ value
82
+ .replace(/\s*\([^)]*(?:people|person|x\s*\d+)[^)]*\)\s*$/i, '')
83
+ .replace(/\s+/g, ' ')
84
+ .trim()
85
+ .toUpperCase();
86
+ return line.kind === 'ticket'
87
+ ? `ticket:${line.category.trim().toUpperCase()}`
88
+ : `line:${String(line.type ?? '').trim().toUpperCase()}:${normalizedLabel(line.label)}`;
89
+ }
90
+
91
+ function lineAmount(line: PriceSummaryLine): number {
92
+ return line.kind === 'ticket' ? line.itemTotal : line.amount;
93
+ }
94
+
95
+ function lineQuantity(line: PriceSummaryLine): number | null {
96
+ return line.kind === 'ticket' ? line.qty : line.quantity ?? null;
97
+ }
98
+
99
+ function amendmentDeltaLines(
100
+ originalLines: PriceSummaryLine[],
101
+ proposedLines: PriceSummaryLine[],
102
+ ): PriceSummaryLine[] {
103
+ const remainingOriginal = new Map<string, { amount: number; quantity: number | null }>();
104
+ for (const line of originalLines) {
105
+ const key = lineIdentity(line);
106
+ const previous = remainingOriginal.get(key);
107
+ remainingOriginal.set(key, {
108
+ amount: roundMoney((previous?.amount ?? 0) + lineAmount(line)),
109
+ quantity:
110
+ previous?.quantity == null && lineQuantity(line) == null
111
+ ? null
112
+ : (previous?.quantity ?? 0) + (lineQuantity(line) ?? 0),
113
+ });
114
+ }
115
+
116
+ return proposedLines.flatMap((line): PriceSummaryLine[] => {
117
+ const original = remainingOriginal.get(lineIdentity(line));
118
+ const amount = roundMoney(lineAmount(line) - (original?.amount ?? 0));
119
+ const proposedQuantity = lineQuantity(line);
120
+ const quantity =
121
+ proposedQuantity == null && original?.quantity == null
122
+ ? null
123
+ : (proposedQuantity ?? 0) - (original?.quantity ?? 0);
124
+ if (Math.abs(amount) < 0.005 && (quantity == null || quantity === 0)) return [];
125
+
126
+ if (line.kind === 'ticket') {
127
+ return [{
128
+ ...line,
129
+ qty: Math.abs(quantity ?? 0) || line.qty,
130
+ itemTotal: amount,
131
+ editable: false,
132
+ lineKey: undefined,
133
+ }];
134
+ }
135
+ const isTax = String(line.type ?? '').toUpperCase() === 'TAX';
136
+ return [{
137
+ ...line,
138
+ label: isTax ? (amount >= 0 ? 'Additional tax' : 'Tax reduction') : line.label,
139
+ amount,
140
+ quantity: quantity == null ? null : Math.abs(quantity),
141
+ editable: false,
142
+ lineKey: undefined,
143
+ }];
144
+ });
145
+ }
146
+
79
147
  export function AdminChangeReceiptComparison({
80
148
  originalReceipt,
81
149
  newReceipt,
@@ -88,11 +156,6 @@ export function AdminChangeReceiptComparison({
88
156
  t,
89
157
  taxRate,
90
158
  newReceiptAdjustments,
91
- lineAmountInputs,
92
- onLineAmountInputChange,
93
- onLineAmountInputBlur,
94
- onLineAmountReset,
95
- lineLabelInputs,
96
159
  }: AdminChangeReceiptComparisonProps) {
97
160
  const displayLocale = normalizeLocale(locale);
98
161
  const originalReceiptLines = useMemo(
@@ -100,6 +163,36 @@ export function AdminChangeReceiptComparison({
100
163
  [originalReceipt],
101
164
  );
102
165
  const originalLinesIncludeTaxRow = hasTaxLine(originalReceiptLines);
166
+ const calculatedChangeLines = useMemo(
167
+ () => amendmentDeltaLines(originalReceiptLines, newPriceSummaryLines),
168
+ [newPriceSummaryLines, originalReceiptLines],
169
+ );
170
+ const receiptTaxDelta = roundMoney(newReceipt.tax - originalReceipt.tax);
171
+ const changeLines = useMemo(() => {
172
+ if (hasTaxLine(calculatedChangeLines) || Math.abs(receiptTaxDelta) < 0.005) {
173
+ return calculatedChangeLines;
174
+ }
175
+ return [
176
+ ...calculatedChangeLines,
177
+ {
178
+ kind: 'line' as const,
179
+ label: receiptTaxDelta >= 0 ? 'Additional tax' : 'Tax reduction',
180
+ amount: receiptTaxDelta,
181
+ type: 'TAX',
182
+ },
183
+ ];
184
+ }, [calculatedChangeLines, receiptTaxDelta]);
185
+ const changeLinesIncludeTaxRow = hasTaxLine(changeLines);
186
+ const changeTax = roundMoney(
187
+ changeLines.reduce(
188
+ (sum, line) =>
189
+ line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
190
+ ? sum + line.amount
191
+ : sum,
192
+ 0,
193
+ ),
194
+ );
195
+ const changeSubtotal = roundMoney(amountDue - changeTax);
103
196
  const originalCurrency = originalReceipt.currency ?? currency;
104
197
  const amountDueClass =
105
198
  amountDue < -0.005
@@ -113,7 +206,7 @@ export function AdminChangeReceiptComparison({
113
206
  <div className="grid gap-4 md:grid-cols-2">
114
207
  <section className="min-w-0 overflow-visible rounded-lg border border-stone-200 bg-stone-50/70 p-3">
115
208
  <div className="mb-3 flex items-center justify-between gap-3">
116
- <h3 className="text-sm font-semibold text-stone-900">Current receipt</h3>
209
+ <h3 className="text-sm font-semibold text-stone-900">Existing purchase</h3>
117
210
  <span className="shrink-0 text-xs font-medium text-stone-500">
118
211
  {formatCurrencyAmount(originalReceipt.total, originalCurrency, displayLocale)}
119
212
  </span>
@@ -130,32 +223,67 @@ export function AdminChangeReceiptComparison({
130
223
  t={t}
131
224
  />
132
225
  </section>
133
- <section className="min-w-0 overflow-visible rounded-lg border border-stone-300 bg-white p-3 shadow-sm">
226
+ <section className="min-w-0 overflow-visible rounded-lg border border-sky-200 bg-sky-50/50 p-3">
134
227
  <div className="mb-3 flex items-center justify-between gap-3">
135
- <h3 className="text-sm font-semibold text-stone-900">New receipt</h3>
228
+ <div>
229
+ <h3 className="text-sm font-semibold text-stone-900">This change</h3>
230
+ <p className="mt-0.5 text-xs text-stone-500">Only newly charged, credited, or repriced value</p>
231
+ </div>
136
232
  <span className="shrink-0 text-xs font-medium text-stone-500">
137
- {formatCurrencyAmount(newReceipt.total, currency, displayLocale)}
233
+ {formatCurrencyAmount(amountDue, currency, displayLocale)}
138
234
  </span>
139
235
  </div>
140
- <PriceSummary
141
- lines={newPriceSummaryLines}
142
- total={newReceipt.total}
143
- currency={currency}
144
- locale={displayLocale}
145
- subtotal={newReceipt.subtotal}
146
- taxAmount={!newPriceSummaryLinesIncludeTaxRow ? newReceipt.tax : 0}
147
- taxRate={taxRate}
148
- size="sm"
149
- t={t}
150
- extraBeforeSubtotal={newReceiptAdjustments}
151
- lineAmountInputs={lineAmountInputs}
152
- onLineAmountInputChange={onLineAmountInputChange}
153
- onLineAmountInputBlur={onLineAmountInputBlur}
154
- onLineAmountReset={onLineAmountReset}
155
- lineLabelInputs={lineLabelInputs}
156
- />
236
+ {changeLines.length === 0 && Math.abs(amountDue) < 0.005 ? (
237
+ <div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
238
+ <p className="text-sm font-medium text-stone-700">No pricing changes</p>
239
+ <p className="mt-1 text-xs text-stone-500">The selected booking details match the existing purchase.</p>
240
+ </div>
241
+ ) : (
242
+ <PriceSummary
243
+ lines={changeLines}
244
+ total={amountDue}
245
+ currency={currency}
246
+ locale={displayLocale}
247
+ subtotal={changeSubtotal}
248
+ taxAmount={!changeLinesIncludeTaxRow ? changeTax : 0}
249
+ taxRate={taxRate}
250
+ size="sm"
251
+ t={t}
252
+ />
253
+ )}
157
254
  </section>
158
255
  </div>
256
+ <section className="min-w-0 overflow-visible rounded-lg border border-stone-300 bg-white p-3 shadow-sm">
257
+ <div className="mb-3 flex items-center justify-between gap-3">
258
+ <div>
259
+ <h3 className="text-sm font-semibold text-stone-900">Updated booking receipt</h3>
260
+ <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>
261
+ </div>
262
+ <span className="shrink-0 text-xs font-medium text-stone-500">
263
+ {formatCurrencyAmount(newReceipt.total, currency, displayLocale)}
264
+ </span>
265
+ </div>
266
+ <PriceSummary
267
+ lines={newPriceSummaryLines.map((line) => ({ ...line, editable: false, lineKey: undefined }))}
268
+ total={newReceipt.total}
269
+ currency={currency}
270
+ locale={displayLocale}
271
+ subtotal={newReceipt.subtotal}
272
+ taxAmount={!newPriceSummaryLinesIncludeTaxRow ? newReceipt.tax : 0}
273
+ taxRate={taxRate}
274
+ size="sm"
275
+ t={t}
276
+ />
277
+ </section>
278
+ {newReceiptAdjustments ? (
279
+ <section className="rounded-lg border border-amber-200 bg-amber-50/40 p-3">
280
+ <div className="mb-2">
281
+ <h3 className="text-sm font-semibold text-stone-900">Admin adjustments</h3>
282
+ <p className="mt-0.5 text-xs text-stone-500">Optional structured charges or credits applied to this amendment</p>
283
+ </div>
284
+ {newReceiptAdjustments}
285
+ </section>
286
+ ) : null}
159
287
  <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
160
288
  <span className="text-sm font-semibold text-stone-900">{amountDueLabel}</span>
161
289
  <span className={`text-lg font-semibold tabular-nums ${amountDueClass}`}>