""" 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 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, }) return bp