From 902bafe7f20dfbd2f8535afbcf277517a78c244e Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 23 May 2026 21:06:03 +0200 Subject: [PATCH] feat: POST /tickets/{event_id}/{payment_hash} polling endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The webapp's useTicketPurchase polls this every 2s after firing Pay with Wallet (or after presenting the QR) to confirm payment before advancing to the ticket-QR success state. Without this endpoint the post-payment poll loop returns 404 indefinitely and the buyer never sees their ticket land — even though set_ticket_paid fired on the invoice listener and the row is correctly marked paid in the DB. Returns {paid: bool, ticket_id?: str}. A missing or cross-event ticket returns paid: false rather than 404 so the poll loop doesn't need to special-case the not-yet-created race. The WebSocket at /tickets/ws/{payment_hash} is more efficient for push notifications — this POST is the fallback for clients that can't open a relay-side socket. Co-Authored-By: Claude Opus 4.7 (1M context) --- views_api.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/views_api.py b/views_api.py index 35da0cd..f04f24e 100644 --- a/views_api.py +++ b/views_api.py @@ -623,6 +623,29 @@ async def api_ticket_create( ) +@tickets_api_router.post("/{event_id}/{payment_hash}") +async def api_ticket_payment_status(event_id: str, payment_hash: str) -> dict: + """Poll-style payment confirmation for a pending ticket. + + The webapp's `useTicketPurchase` polls this every 2s after firing + `Pay with Wallet` (or after presenting the QR for an external + wallet) until `paid: true` comes back, then advances to the + ticket-QR success state. The companion WebSocket at + `/tickets/ws/{payment_hash}` is more efficient for pushes — this + endpoint is the fallback for clients that can't open a relay-side + socket. + + Returns `{paid: bool, ticket_id?: str}` so the client can hand off + to the ticket-detail flow without an extra GET. A missing / + cross-event ticket returns `paid: false` rather than 404 so the + poll loop doesn't have to special-case the not-yet-created race. + """ + ticket = await get_ticket(payment_hash) + if not ticket or ticket.event != event_id: + return {"paid": False} + return {"paid": ticket.paid, "ticket_id": ticket.id} + + @tickets_api_router.websocket("/ws/{payment_hash}") async def websocket_endpoint(payment_hash: str, websocket: WebSocket) -> None: await websocket.accept()