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>
Event finance
- Finance tab on the event page: P&L summary with a revenue-to-result
waterfall, costs and other income in one ledger, and the partner split
with payouts. Lifecycle stepper (Selling, Adding costs, Ready to close,
Finalized) with what is left to do; closing the books goes through a
checklist dialog that freezes the numbers.
- Expense modal shows only the fields each calculation type needs, a live
preview with the event's real counts, and a category suggested from the
description. Date inputs follow the UI language.
- Global Finance page: profit per event with outliers clipped and labelled,
and a "Ready to close" list of past events whose books are still open.
- Calculation service (integer PYG, basis points) with pinned regression
scenarios; PYG formatting centralised in lib/money with locale-aware
separators.
Event team permissions
- Per-event members with role presets and requireEventPermission; the
header stat "Confirmed" is relabelled "Not checked in yet", which is
what it counts.
Public sales state
- One sales state (online, door, sold out, ended, external, cancelled) for
the event page, listings and JSON-LD, with the door price and tenders
shown only while people can still pay at the door.
Frontend unit tests run with vitest (npm test in frontend/).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Events can now set an optional walk-in price (events.walk_in_price):
null falls back to the ticket price, 0 is a free walk-in. It is set in
the Create/Edit Event modal (EN/ES) and carried through create, update
and duplicate, but it is internal: public event responses and the
attendee-facing ticket/dashboard endpoints drop it, and only admin,
organizer and staff callers receive it.
The door screen no longer trusts the amount the client sends. It sends
a quantity and the server prices the charge from the event record:
walk-ins pay the walk-in price, existing tickets the ticket price. A
typed amount is only honoured with amountOverride from admin/organizer
and is written to audit_logs in the same transaction. The charged
amount stays snapshotted on the payment row.
POS is a new door tender for the physical card terminal. Staff pick
POS, see the amount to key in, charge the card, then confirm with
"Mark as paid"; it is then recorded like any other door payment
(provider and method 'pos'). It can be switched off globally or per
event via payment options, and shows up in the payments filters,
bookings, revenue summaries and door takings. PosChargePanel is where
an automatic terminal push would go later; nothing calls a terminal
today.
tickets.booking_source (online | walk_in | admin) records how a booking
was made. The migration backfills walk-ins from the door screen's
idempotency records; everything else stays online.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>