@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
|
@@ -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">
|
|
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-
|
|
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
|
-
<
|
|
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(
|
|
233
|
+
{formatCurrencyAmount(amountDue, currency, displayLocale)}
|
|
138
234
|
</span>
|
|
139
235
|
</div>
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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}`}>
|