""" Event invitation endpoints — invite users to events, respond, override per-occurrence, leave. Two routers: - events_router: mounted at /api/events for POST/GET /{event_id}/invitations - router: mounted at /api/event-invitations for respond/override/delete/pending """ from fastapi import APIRouter, Depends, HTTPException, Path from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.database import get_db from app.models.calendar_event import CalendarEvent from app.models.event_invitation import EventInvitation from app.models.user import User from app.routers.auth import get_current_user from app.schemas.event_invitation import ( EventInvitationCreate, EventInvitationRespond, EventInvitationOverrideCreate, ) from app.services.calendar_sharing import get_user_permission from app.services.event_invitation import ( send_event_invitations, respond_to_invitation, override_occurrence_status, dismiss_invitation, dismiss_invitation_by_owner, get_event_invitations, get_pending_invitations, ) # Mounted at /api/events — event-scoped invitation endpoints events_router = APIRouter() # Mounted at /api/event-invitations — invitation-scoped endpoints router = APIRouter() async def _get_event_with_access_check( db: AsyncSession, event_id: int, user_id: int ) -> CalendarEvent: """Fetch event and verify the user has access (owner, shared member, or invitee).""" result = await db.execute( select(CalendarEvent).where(CalendarEvent.id == event_id) ) event = result.scalar_one_or_none() if not event: raise HTTPException(status_code=404, detail="Event not found") # Check calendar access perm = await get_user_permission(db, event.calendar_id, user_id) if perm is not None: return event # Check if invitee (also check parent for recurring children) event_ids_to_check = [event_id] if event.parent_event_id: event_ids_to_check.append(event.parent_event_id) inv_result = await db.execute( select(EventInvitation.id).where( EventInvitation.event_id.in_(event_ids_to_check), EventInvitation.user_id == user_id, ) ) if inv_result.first() is not None: return event raise HTTPException(status_code=404, detail="Event not found") # ── Event-scoped endpoints (mounted at /api/events) ── @events_router.post("/{event_id}/invitations", status_code=201) async def invite_to_event( body: EventInvitationCreate, event_id: int = Path(ge=1, le=2147483647), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Invite connected users to an event. Requires event ownership or create_modify+ permission.""" result = await db.execute( select(CalendarEvent).where(CalendarEvent.id == event_id) ) event = result.scalar_one_or_none() if not event: raise HTTPException(status_code=404, detail="Event not found") # Permission check: owner or create_modify+ perm = await get_user_permission(db, event.calendar_id, current_user.id) if perm is None: raise HTTPException(status_code=404, detail="Event not found") if perm not in ("owner", "create_modify", "full_access"): raise HTTPException(status_code=403, detail="Insufficient permission") # For recurring child events, invite to the parent (series) target_event_id = event.parent_event_id if event.parent_event_id else event_id invitations = await send_event_invitations( db=db, event_id=target_event_id, user_ids=body.user_ids, invited_by=current_user.id, ) await db.commit() return {"invited": len(invitations), "event_id": target_event_id} @events_router.get("/{event_id}/invitations") async def list_event_invitations( event_id: int = Path(ge=1, le=2147483647), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """List all invitees and their statuses for an event.""" event = await _get_event_with_access_check(db, event_id, current_user.id) # For recurring children, also fetch parent's invitations target_id = event.parent_event_id if event.parent_event_id else event_id invitations = await get_event_invitations(db, target_id) return invitations # ── Invitation-scoped endpoints (mounted at /api/event-invitations) ── @router.get("/pending") async def my_pending_invitations( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Get all pending event invitations for the current user.""" return await get_pending_invitations(db, current_user.id) @router.put("/{invitation_id}/respond") async def respond_invitation( body: EventInvitationRespond, invitation_id: int = Path(ge=1, le=2147483647), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Accept, tentative, or decline an event invitation.""" invitation = await respond_to_invitation( db=db, invitation_id=invitation_id, user_id=current_user.id, status=body.status, ) # Build response before commit (ORM objects expire after commit) response_data = { "id": invitation.id, "event_id": invitation.event_id, "status": invitation.status, "responded_at": invitation.responded_at, } await db.commit() return response_data @router.put("/{invitation_id}/respond/{occurrence_id}") async def override_occurrence( body: EventInvitationOverrideCreate, invitation_id: int = Path(ge=1, le=2147483647), occurrence_id: int = Path(ge=1, le=2147483647), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Override invitation status for a specific occurrence of a recurring event.""" override = await override_occurrence_status( db=db, invitation_id=invitation_id, occurrence_id=occurrence_id, user_id=current_user.id, status=body.status, ) response_data = { "invitation_id": override.invitation_id, "occurrence_id": override.occurrence_id, "status": override.status, } await db.commit() return response_data @router.delete("/{invitation_id}", status_code=204) async def leave_or_revoke_invitation( invitation_id: int = Path(ge=1, le=2147483647), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Leave an event (invitee) or revoke an invitation (event owner). Invitees can only delete their own invitations. Event owners can delete any invitation for their events. """ inv_result = await db.execute( select(EventInvitation).where(EventInvitation.id == invitation_id) ) invitation = inv_result.scalar_one_or_none() if not invitation: raise HTTPException(status_code=404, detail="Invitation not found") if invitation.user_id == current_user.id: # Invitee leaving await dismiss_invitation(db, invitation_id, current_user.id) else: # Check if current user is the event owner event_result = await db.execute( select(CalendarEvent).where(CalendarEvent.id == invitation.event_id) ) event = event_result.scalar_one_or_none() if not event: raise HTTPException(status_code=404, detail="Event not found") perm = await get_user_permission(db, event.calendar_id, current_user.id) if perm != "owner": raise HTTPException(status_code=403, detail="Only the event owner can revoke invitations") await dismiss_invitation_by_owner(db, invitation_id) await db.commit() return None