feat: ticket purchase flow, QR display, and admin check-in
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Ticket purchase: - tickets blueprint with routes for my tickets list, ticket detail with QR - Buy tickets form on entry detail page (HTMX-powered) - Ticket services: create, query, availability checking Admin check-in: - ticket_admin blueprint with dashboard, lookup, and check-in routes - QR scanner/lookup interface with real-time search - Per-entry ticket list view - Check-in transitions ticket state to checked_in Internal API: - GET /internal/events/tickets endpoint for cross-app queries - POST /internal/events/tickets/<code>/checkin for programmatic check-in Template fixes: - All templates updated: blog.post.calendars.* → calendars.* - Removed slug=post.slug parameters (standalone events service) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ from quart import Blueprint, g, request, jsonify
|
||||
from sqlalchemy import select, update, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from models.calendars import CalendarEntry, Calendar
|
||||
from models.calendars import CalendarEntry, Calendar, Ticket
|
||||
from suma_browser.app.csrf import csrf_exempt
|
||||
|
||||
|
||||
@@ -129,4 +129,70 @@ def register() -> Blueprint:
|
||||
"calendar_slug": entry.calendar.slug if entry.calendar else None,
|
||||
})
|
||||
|
||||
@bp.get("/tickets")
|
||||
@csrf_exempt
|
||||
async def tickets():
|
||||
"""
|
||||
Return tickets for a user/session.
|
||||
Query params: user_id, session_id, order_id, state
|
||||
"""
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
session_id = request.args.get("session_id")
|
||||
order_id = request.args.get("order_id", type=int)
|
||||
state = request.args.get("state")
|
||||
|
||||
filters = []
|
||||
if order_id is not None:
|
||||
filters.append(Ticket.order_id == order_id)
|
||||
elif user_id is not None:
|
||||
filters.append(Ticket.user_id == user_id)
|
||||
elif session_id:
|
||||
filters.append(Ticket.session_id == session_id)
|
||||
else:
|
||||
return jsonify([])
|
||||
|
||||
if state:
|
||||
filters.append(Ticket.state == state)
|
||||
|
||||
result = await g.s.execute(
|
||||
select(Ticket)
|
||||
.where(*filters)
|
||||
.options(
|
||||
selectinload(Ticket.entry).selectinload(CalendarEntry.calendar),
|
||||
selectinload(Ticket.ticket_type),
|
||||
)
|
||||
.order_by(Ticket.created_at.desc())
|
||||
)
|
||||
tix = result.scalars().all()
|
||||
|
||||
return jsonify([
|
||||
{
|
||||
"id": t.id,
|
||||
"code": t.code,
|
||||
"state": t.state,
|
||||
"entry_name": t.entry.name if t.entry else None,
|
||||
"entry_start_at": t.entry.start_at.isoformat() if t.entry and t.entry.start_at else None,
|
||||
"calendar_name": t.entry.calendar.name if t.entry and t.entry.calendar else None,
|
||||
"ticket_type_name": t.ticket_type.name if t.ticket_type else None,
|
||||
"ticket_type_cost": float(t.ticket_type.cost) if t.ticket_type and t.ticket_type.cost else None,
|
||||
"checked_in_at": t.checked_in_at.isoformat() if t.checked_in_at else None,
|
||||
}
|
||||
for t in tix
|
||||
])
|
||||
|
||||
@bp.post("/tickets/<code>/checkin")
|
||||
@csrf_exempt
|
||||
async def checkin(code: str):
|
||||
"""
|
||||
Check in a ticket by code.
|
||||
Used by admin check-in interface.
|
||||
"""
|
||||
from .bp.tickets.services.tickets import checkin_ticket
|
||||
|
||||
success, error = await checkin_ticket(g.s, code)
|
||||
if not success:
|
||||
return jsonify({"ok": False, "error": error}), 400
|
||||
|
||||
return jsonify({"ok": True})
|
||||
|
||||
return bp
|
||||
|
||||
Reference in New Issue
Block a user