""" Internal JSON API for the events app. These endpoints are called by other apps (cart) over HTTP. They are CSRF-exempt because they are server-to-server calls. """ from __future__ import annotations from quart import Blueprint, g, request, jsonify from sqlalchemy import select, update, func from sqlalchemy.orm import selectinload from models.calendars import CalendarEntry, Calendar, Ticket from suma_browser.app.csrf import csrf_exempt def register() -> Blueprint: bp = Blueprint("events_api", __name__, url_prefix="/internal/events") @bp.get("/calendar-entries") @csrf_exempt async def calendar_entries(): """ Return pending calendar entries for a user/session. Used by the cart app to display calendar items in the cart. Query params: user_id, session_id, state (default: pending) """ user_id = request.args.get("user_id", type=int) session_id = request.args.get("session_id") state = request.args.get("state", "pending") filters = [ CalendarEntry.deleted_at.is_(None), CalendarEntry.state == state, ] if user_id is not None: filters.append(CalendarEntry.user_id == user_id) elif session_id: filters.append(CalendarEntry.session_id == session_id) else: return jsonify([]) result = await g.s.execute( select(CalendarEntry) .where(*filters) .options(selectinload(CalendarEntry.calendar)) .order_by(CalendarEntry.start_at.asc()) ) entries = result.scalars().all() return jsonify([ { "id": e.id, "name": e.name, "cost": float(e.cost) if e.cost else 0, "state": e.state, "start_at": e.start_at.isoformat() if e.start_at else None, "end_at": e.end_at.isoformat() if e.end_at else None, "calendar_name": e.calendar.name if e.calendar else None, "calendar_slug": e.calendar.slug if e.calendar else None, } for e in entries ]) @bp.post("/adopt") @csrf_exempt async def adopt(): """ Adopt anonymous calendar entries for a user. Called by the cart app after login. Body: {"user_id": int, "session_id": str} """ data = await request.get_json() or {} user_id = data.get("user_id") session_id = data.get("session_id") if not user_id or not session_id: return jsonify({"ok": False, "error": "user_id and session_id required"}), 400 # Soft-delete existing user entries await g.s.execute( update(CalendarEntry) .where( CalendarEntry.deleted_at.is_(None), CalendarEntry.user_id == user_id, ) .values(deleted_at=func.now()) ) # Adopt anonymous entries cal_result = await g.s.execute( select(CalendarEntry).where( CalendarEntry.deleted_at.is_(None), CalendarEntry.session_id == session_id, ) ) for entry in cal_result.scalars().all(): entry.user_id = user_id return jsonify({"ok": True}) @bp.get("/entry/") @csrf_exempt async def entry_detail(entry_id: int): """ Return entry details for order display. Called by the cart app when showing order items. """ result = await g.s.execute( select(CalendarEntry) .where(CalendarEntry.id == entry_id) .options(selectinload(CalendarEntry.calendar)) ) entry = result.scalar_one_or_none() if not entry: return jsonify(None), 404 return jsonify({ "id": entry.id, "name": entry.name, "cost": float(entry.cost) if entry.cost else 0, "state": entry.state, "start_at": entry.start_at.isoformat() if entry.start_at else None, "end_at": entry.end_at.isoformat() if entry.end_at else None, "calendar_name": entry.calendar.name if entry.calendar else None, "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//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