diff --git a/backend/api/routers/tournaments.py b/backend/api/routers/tournaments.py index 5b8467d..545b0b7 100644 --- a/backend/api/routers/tournaments.py +++ b/backend/api/routers/tournaments.py @@ -1,15 +1,64 @@ from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy import func, select +from sqlalchemy import and_, case, func, select from sqlalchemy.ext.asyncio import AsyncSession from api.deps import get_current_superuser -from database.models import Seat, Tournament, User +from database.models import Seat, Ticket, TicketStatus, Tournament, User from database.session import get_db -from schemas.tournament import SeatGenerateRequest, TournamentCreate, TournamentResponse +from schemas.tournament import SeatGenerateRequest, SeatResponse, TournamentCreate, TournamentResponse router = APIRouter(prefix="/api/tournaments", tags=["tournaments"]) +@router.get("/{tournament_id}/seats", response_model=list[SeatResponse]) +async def get_tournament_seats( + tournament_id: int, + db: AsyncSession = Depends(get_db), +) -> list[SeatResponse]: + """ + Публичный эндпоинт. Возвращает все места турнира с флагом is_available. + + Место недоступно (is_available=False), если связанный Ticket + находится в статусе LOCKED или PAID. + Используется LEFT OUTER JOIN + CASE для вычисления флага одним запросом. + """ + # 1. Вычисляем is_available через CASE на стороне БД + is_available_expr = case( + ( + and_( + Ticket.id.is_not(None), + Ticket.status.in_([TicketStatus.LOCKED, TicketStatus.PAID]), + ), + False, + ), + else_=True, + ).label("is_available") + + # 2. LEFT JOIN: берём все места турнира, присоединяем Ticket (если есть) + stmt = ( + select(Seat, is_available_expr) + .outerjoin(Ticket, Ticket.seat_id == Seat.id) + .where(Seat.tournament_id == tournament_id) + .order_by(Seat.sector, Seat.row, Seat.number) + ) + + # 3. Выполняем запрос и формируем ответ + result = await db.execute(stmt) + rows = result.all() + + return [ + SeatResponse( + id=seat.id, + sector=seat.sector, + row=seat.row, + number=seat.number, + price=seat.price, + is_available=bool(is_available), + ) + for seat, is_available in rows + ] + + @router.post("", response_model=TournamentResponse, status_code=status.HTTP_201_CREATED) async def create_tournament( body: TournamentCreate, diff --git a/backend/schemas/tournament.py b/backend/schemas/tournament.py index 317aab5..fba56f2 100644 --- a/backend/schemas/tournament.py +++ b/backend/schemas/tournament.py @@ -34,3 +34,12 @@ class TournamentResponse(BaseModel): event_date: datetime model_config = ConfigDict(from_attributes=True) + + +class SeatResponse(BaseModel): + id: int + sector: str + row: int + number: int + price: int + is_available: bool