From 02071e6541a46dcd3ff40148956881f7ea6e7e84 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 24 May 2026 18:45:48 +0200 Subject: [PATCH] feat: events_list_event_tickets RPC for organizer ticket roster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second nostr-transport handler on this branch. Returns paid + registered counts plus the per-ticket roster (id, name, registered status, timestamp) for one calendar event, organizer-only. Backs the door scanner's counts strip and "scanned" list with backend truth so a second organizer scanning on another device, an operator switching from mobile to laptop mid-event, or a refresh in incognito all see the same numbers instead of diverging from a per-device localStorage cache. Same authorisation posture as events_ticket_register: dispatcher binds caller pubkey to wallet via AUTH_WALLET, handler verifies the event's wallet is in the caller's wallet set. Only paid tickets land in the response — proposed/unpaid rows are irrelevant at the door. Webapp consumes this in aiolabs/webapp#73. --- __init__.py | 15 +++++++++++-- crud.py | 9 ++++++++ transport_rpcs.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/__init__.py b/__init__.py index bfe3d45..01b145e 100644 --- a/__init__.py +++ b/__init__.py @@ -55,12 +55,23 @@ def events_start(): register_rpc, ) - from .transport_rpcs import handle_events_ticket_register + from .transport_rpcs import ( + handle_events_list_event_tickets, + handle_events_ticket_register, + ) register_rpc( "events_ticket_register", handle_events_ticket_register, AUTH_WALLET ) - logger.info("[EVENTS] Registered nostr-transport RPC: events_ticket_register") + register_rpc( + "events_list_event_tickets", + handle_events_list_event_tickets, + AUTH_WALLET, + ) + logger.info( + "[EVENTS] Registered nostr-transport RPCs: " + "events_ticket_register, events_list_event_tickets" + ) except ImportError: logger.info( "[EVENTS] nostr_transport not available on this LNbits — " diff --git a/crud.py b/crud.py index a72b3b3..551a3bc 100644 --- a/crud.py +++ b/crud.py @@ -139,6 +139,15 @@ async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: return [Ticket(**_parse_ticket_row(row)) for row in rows] +async def get_tickets_by_event(event_id: str) -> list[Ticket]: + """All ticket rows for the given calendar event id.""" + rows = await db.fetchall( + "SELECT * FROM events.ticket WHERE event = :event_id", + {"event_id": event_id}, + ) + return [Ticket(**_parse_ticket_row(row)) for row in rows] + + async def get_tickets_by_user_id(user_id: str) -> list[Ticket]: """All tickets owned by the given LNbits user_id.""" rows = await db.fetchall( diff --git a/transport_rpcs.py b/transport_rpcs.py index 16060d6..e278f91 100644 --- a/transport_rpcs.py +++ b/transport_rpcs.py @@ -20,7 +20,7 @@ from lnbits.core.crud import get_user from lnbits.core.models import WalletTypeInfo from lnbits.core.services.nostr_transport.models import NostrRpcRequest -from .crud import get_event, get_ticket, update_ticket +from .crud import get_event, get_ticket, get_tickets_by_event, update_ticket async def handle_events_ticket_register( @@ -66,3 +66,55 @@ async def handle_events_ticket_register( ticket.reg_timestamp = datetime.now(timezone.utc) await update_ticket(ticket) return ticket.dict() + + +async def handle_events_list_event_tickets( + auth: WalletTypeInfo, + request: NostrRpcRequest, +) -> dict: + """Return paid + registered counts plus the per-ticket roster for + one calendar event, organizer-only. + + Backs the door scanner's counts strip and "All scanned" tab so the + UI reads authoritative state from the backend instead of relying + on per-device localStorage (which diverges the moment a second + organizer scans, or the operator switches devices). + + The roster only includes paid tickets — proposed/unpaid rows are + irrelevant at the door. + """ + body = request.body or {} + event_id = body.get("event_id") + if not event_id: + raise ValueError("event_id is required") + + event = await get_event(event_id) + if not event: + raise ValueError("Event does not exist") + + user = await get_user(auth.wallet.user) + owned_wallet_ids = user.wallet_ids if user else [auth.wallet.id] + if event.wallet not in owned_wallet_ids: + raise PermissionError("You do not own this event") + + tickets = await get_tickets_by_event(event_id) + paid_tickets = [t for t in tickets if t.paid] + registered_count = sum(1 for t in paid_tickets if t.registered) + + return { + "event_id": event_id, + "sold": len(paid_tickets), + "registered": registered_count, + "remaining": len(paid_tickets) - registered_count, + "tickets": [ + { + "id": t.id, + "name": t.name, + "registered": t.registered, + "registered_at": ( + t.reg_timestamp.isoformat() if t.reg_timestamp else None + ), + } + for t in paid_tickets + ], + }