Files
ticket-system/backend/api/routers/tournaments.py
2026-03-06 20:03:16 +00:00

126 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"}