Keep event revenue at what was paid when the ticket price changes.
The event header revenue and the door summary's pre-sale total were computed as settled tickets × the current event price, so editing the price rewrote revenue for tickets already sold. They now sum the paid payment amounts; the header no longer falls back to count × price. Admin analytics per-event revenue gets the same fix. Bookings that haven't been paid yet owe the current price, so a price or currency change now reprices open `pending` payments in the same transaction as the event update. Paid/refunded history, pending_approval, on_hold and Lightning invoices keep their amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e203fb6c74
commit
b3584e6c4d
@@ -455,21 +455,15 @@ export function EventDetailView({ eventId, backHref }: { eventId: string; backHr
|
||||
const checkedInCount = getTicketsByStatus('checked_in').length;
|
||||
const cancelledCount = getTicketsByStatus('cancelled').length;
|
||||
const onHoldCount = getTicketsByStatus('on_hold').length;
|
||||
// Revenue counts only settled tickets: unpaid (balance due) and comp (guest)
|
||||
// tickets are excluded; legacy rows without paymentStatus fall back to !isGuest
|
||||
const isRevenueTicket = (t: Ticket) => (t.paymentStatus ? t.paymentStatus === 'paid' : !t.isGuest);
|
||||
const paidConfirmedCount = getTicketsByStatus('confirmed').filter(isRevenueTicket).length;
|
||||
const paidCheckedInCount = getTicketsByStatus('checked_in').filter(isRevenueTicket).length;
|
||||
// Door sales can be taken at a custom amount (someone paying for their whole
|
||||
// group), so once the door summary is loaded it is the authority on the total:
|
||||
// pre-sale tickets at face value plus whatever was actually taken on the night.
|
||||
const presaleRevenue = doorSummary
|
||||
? doorSummary.presale.total
|
||||
: (paidConfirmedCount + paidCheckedInCount) * event.price;
|
||||
// Revenue comes only from the door summary, which adds up what was actually
|
||||
// paid. Never derive it from ticket count × event.price: the price can change
|
||||
// after tickets have been sold.
|
||||
const presaleRevenue = doorSummary?.presale.total ?? 0;
|
||||
const doorRevenue = doorSummary?.door.total ?? 0;
|
||||
const revenue = presaleRevenue + doorRevenue;
|
||||
// Header money follows the UI language's thousands separator.
|
||||
const money = (amount: number) => formatCurrency(amount, event.currency, locale);
|
||||
const revenueLabel = doorSummary ? money(revenue) : '—';
|
||||
// "confirmed" tickets become "checked_in" at the door, so this counts the
|
||||
// guests who have a ticket and have not arrived yet (not all confirmed ones).
|
||||
const notCheckedInLabel = t('admin.eventStats.notCheckedIn');
|
||||
@@ -587,7 +581,7 @@ export function EventDetailView({ eventId, backHref }: { eventId: string; backHr
|
||||
{ label: t('admin.eventStats.checkedIn'), value: checkedInCount, icon: TicketIcon, color: 'bg-purple-50 text-purple-600' },
|
||||
...(!showRevenue ? [] : [{
|
||||
label: t('admin.eventStats.revenue'),
|
||||
value: money(revenue),
|
||||
value: revenueLabel,
|
||||
icon: CurrencyDollarIcon,
|
||||
color: 'bg-gray-50 text-gray-600',
|
||||
detail: doorSummary
|
||||
@@ -625,7 +619,7 @@ export function EventDetailView({ eventId, backHref }: { eventId: string; backHr
|
||||
{showRevenue && (
|
||||
<>
|
||||
<span className="text-xs text-gray-400">|</span>
|
||||
<span className="text-sm whitespace-nowrap">{money(revenue)}</span>
|
||||
<span className="text-sm whitespace-nowrap">{revenueLabel}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -639,7 +633,7 @@ export function EventDetailView({ eventId, backHref }: { eventId: string; backHr
|
||||
{ label: t('admin.eventStats.checkedIn'), value: checkedInCount, icon: TicketIcon, color: 'text-purple-600 bg-purple-50' },
|
||||
...(!showRevenue ? [] : [{
|
||||
label: t('admin.eventStats.revenue'),
|
||||
value: money(revenue),
|
||||
value: revenueLabel,
|
||||
icon: CurrencyDollarIcon,
|
||||
color: 'text-gray-600 bg-gray-50',
|
||||
detail: doorSummary
|
||||
|
||||
Reference in New Issue
Block a user