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:
parent
36568d3eee
commit
59068fe09d
5 changed files with 157 additions and 45 deletions
13
models.py
13
models.py
|
|
@ -133,6 +133,9 @@ class CreateTicket(BaseModel):
|
|||
nostr_identifier: str | None = None
|
||||
payment_method: str | None = None
|
||||
fiat_provider: str | None = None
|
||||
# Number of tickets to buy on this single invoice. Bounded so a
|
||||
# bad client can't run away with the organizer's capacity.
|
||||
quantity: int = Field(default=1, ge=1, le=10)
|
||||
|
||||
@root_validator
|
||||
def validate_identifiers(cls, values):
|
||||
|
|
@ -158,6 +161,11 @@ class Ticket(BaseModel):
|
|||
time: datetime
|
||||
reg_timestamp: datetime
|
||||
extra: TicketExtra = Field(default_factory=TicketExtra)
|
||||
# Shared LNbits invoice payment_hash. Equals `id` for single-ticket
|
||||
# purchases (legacy + post-migration default). Multi-ticket
|
||||
# purchases create N rows sharing one payment_hash so each attendee
|
||||
# gets a distinct scannable id while the buyer pays once.
|
||||
payment_hash: str | None = None
|
||||
|
||||
|
||||
class PublicTicket(BaseModel):
|
||||
|
|
@ -175,3 +183,8 @@ class TicketPaymentRequest(BaseModel):
|
|||
fiat_payment_request: str | None = None
|
||||
fiat_provider: str | None = None
|
||||
is_fiat: bool = False
|
||||
# Row ids created on this invoice — one for single-ticket
|
||||
# purchases, N for multi-ticket (each independently scannable at
|
||||
# the door). Buyers fetch these after payment to render N QRs in
|
||||
# My Tickets.
|
||||
ticket_ids: list[str] = Field(default_factory=list)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue