from fastapi import APIRouter, Depends, HTTPException, status 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, Ticket, TicketStatus, Tournament, User from database.session import get_db 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, current_user: User = Depends(get_current_superuser), db: AsyncSession = Depends(get_db), ) -> Tournament: tournament = Tournament( title=body.title, description=body.description, event_date=body.event_date, ) db.add(tournament) await db.commit() await db.refresh(tournament) return tournament @router.post( "/{tournament_id}/generate-seats", status_code=status.HTTP_200_OK, ) async def generate_seats( tournament_id: int, request: SeatGenerateRequest, current_user: User = Depends(get_current_superuser), db: AsyncSession = Depends(get_db), ) -> dict[str, str]: # 1. Проверяем существование турнира result = await db.execute(select(Tournament).where(Tournament.id == tournament_id)) tournament: Tournament | None = result.scalar_one_or_none() if tournament is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tournament not found") # 2. Проверяем, что места ещё не генерировались (защита от дублей) count_result = await db.execute( select(func.count()).where(Seat.tournament_id == tournament_id) ) existing_count: int = count_result.scalar_one() if existing_count > 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Места уже сгенерированы", ) # 3. Генерируем места для каждого сектора seats: list[Seat] = [] for sector_cfg in request.sectors: for row in range(1, sector_cfg.rows + 1): for number in range(1, sector_cfg.seats_per_row + 1): seats.append( Seat( tournament_id=tournament_id, sector=sector_cfg.sector_name, row=row, number=number, price=sector_cfg.price, ) ) # 4. Массовая вставка db.add_all(seats) await db.commit() return {"message": f"Generated {len(seats)} seats successfully"}