feat: multi-ticket purchases as N rows sharing one payment_hash

Replaces the previous "one row, N seats via extra.quantity" model
with proper one-row-per-attendee semantics. Each attendee gets a
unique scannable id; the door PUT /register/{ticket_id} marks
them registered independently — so a buyer can purchase 3 tickets,
hand 2 QRs to friends arriving separately, and each attendee can
enter on their own schedule.

Schema (migrations_fork.py m002):
- ticket.payment_hash: new TEXT column shared across all rows of
  a multi-ticket purchase. Backfilled `payment_hash = id` for
  pre-migration rows (id WAS the payment_hash by invariant).

Wire:
- TicketPaymentRequest grows `ticket_ids: list[str]` so the
  webapp gets every scannable id back in the create response.
- POST /tickets/{event_id}/{payment_hash} polling endpoint now
  reports `ticket_ids` (every row) + keeps `ticket_id` for
  back-compat.
- api_ticket_create loops quantity times; the first row reuses
  payment_hash as id (preserves legacy `id == payment_hash`
  invariant for single-ticket purchases), the rest get
  urlsafe_short_hash() uuids.

Payment flow:
- on_invoice_paid fetches all rows by payment_hash and marks each
  paid via set_ticket_paid, which now increments event.sold by 1
  per row (was N per row via extra.quantity — simpler now). The
  per-event asyncio lock still serializes counter + republish so
  concurrent multi-ticket purchases for the same event don't
  reorder the published Nostr state.
- Each paid row triggers its own send_ticket_notification_in_
  background call — no-op for buyers without nostr_identifier /
  email, useful when the buyer set those on the row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-23 22:35:56 +02:00
commit 59068fe09d
5 changed files with 157 additions and 45 deletions

View file

@ -103,3 +103,28 @@ async def m001_aio_event_schema(db):
await _alter_add_column_safe(
db, "ALTER TABLE events.events ADD COLUMN categories TEXT"
)
async def m002_ticket_payment_hash(db):
"""
Add `ticket.payment_hash` for multi-ticket purchases.
Multi-ticket purchases land as N rows sharing one LNbits invoice
(so each attendee gets a distinct scannable QR but the buyer
pays once). `ticket.id` stays the row primary key for legacy
single-purchase rows it equals payment_hash; for multi-purchase
children it's a uuid generated at create-time. `payment_hash`
is the new join key for invoice lookup.
Backfill existing rows from id so the
GET-tickets-by-payment-hash path keeps working for pre-migration
data (id was the payment_hash by invariant before this column).
"""
await _alter_add_column_safe(
db, "ALTER TABLE events.ticket ADD COLUMN payment_hash TEXT"
)
await db.execute(
"UPDATE events.ticket SET payment_hash = id "
"WHERE payment_hash IS NULL OR payment_hash = ''"
)