From 4238b41f108fd1b883a023998e254a5d8fec024a Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 3 Jun 2026 19:47:49 +0200 Subject: [PATCH] feat: GET /tickets/event/{event_id}/stats for door-scanner roster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the events_list_event_tickets nostr-transport RPC for callers that don't hold a raw user prvkey (the webapp post-#9, in particular — useTicketScanner.refreshStats now has a working HTTP path). Auth: wallet admin_key + the event's wallet must be in the caller's wallet set, matching the register endpoint's owner check. Without this endpoint the activities scanner page loaded its initial counts (via no-op fallbacks) but every post-scan refreshStats returned 404, leaving the Scanned counter stuck at 0 even though registrations landed correctly. Surfaced by aio-demo manual test on 2026-06-03. Co-Authored-By: Claude Opus 4.7 (1M context) --- views_api.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/views_api.py b/views_api.py index 3edd953..14d60e2 100644 --- a/views_api.py +++ b/views_api.py @@ -47,6 +47,7 @@ from .crud import ( get_settings, get_ticket, get_tickets, + get_tickets_by_event, get_tickets_by_payment_hash, get_tickets_by_user_id, purge_unpaid_tickets, @@ -832,3 +833,52 @@ async def api_event_register_ticket( ticket.reg_timestamp = datetime.now(timezone.utc) ticket = await update_ticket(ticket) return ticket + + +@tickets_api_router.get("/event/{event_id}/stats") +async def api_event_ticket_stats( + event_id: str, + key_info: WalletTypeInfo = Depends(require_admin_key), +) -> dict: + """Door-scanner roster + counts for one event, organizer-only. + + Mirrors the `events_list_event_tickets` nostr-transport RPC for + callers that don't hold a raw user prvkey (the webapp post-#9, in + particular). Auth: wallet admin_key + the event's wallet must be + in the caller's wallet set. + """ + event = await get_event(event_id) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." + ) + + user = await get_user(key_info.wallet.user) + owned_wallet_ids = user.wallet_ids if user else [key_info.wallet.id] + if event.wallet not in owned_wallet_ids: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="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 + ], + }