# Архитектура и бизнес-правила (Ticket System) ## 1. Базовые принципы - **Стек:** FastAPI, SQLAlchemy 2.0 (asyncpg), PostgreSQL, Redis, RabbitMQ. - **Модели данных:** Описаны в `backend/database/models.py`. - **Ключевые сущности:** User, Tournament, Seat, Ticket. ## 2. Жизненный цикл билета (State Machine) Статус билета (`TicketStatus` в таблице `tickets`) строго ограничен: 1. `AVAILABLE` — место свободно для бронирования. 2. `LOCKED` — место временно захвачено пользователем (15 минут на оплату). 3. `PAID` — оплата прошла успешно. 4. `SCANNED` — билет погашен на входе. 5. `REFUNDED` — возврат. ## 3. Сценарий А: Конкурентное бронирование (Критический путь) Захват места должен исключать "состояние гонки" (race condition). 1. При POST-запросе на захват места (`/api/seats/{seat_id}/lock`), FastAPI обращается к Redis. 2. Пытается установить ключ `lock:seat:{seat_id}` с помощью `SETNX` и TTL 15 минут. 3. **Успех:** Если Redis вернул 1, обновляем статус билета в БД на `LOCKED` и привязываем `user_id`. Возвращаем HTTP 200. 4. **Отказ:** Если ключ уже существует, немедленно возвращаем HTTP 409 Conflict. БД не трогаем. ## 4. Сценарий Б: Асинхронная выдача билета (Idempotency) Защита от двойных списаний и зависаний интерфейса при долгой генерации PDF. 1. Платежный шлюз присылает Webhook об успешной оплате. 2. FastAPI проверяет поле `idempotency_key` в таблице `tickets`. Если ключ уже обработан — игнорируем запрос. 3. Обновляем статус билета на `PAID`. 4. Отправляем событие `ticket_paid` (содержит `ticket_id` и данные пользователя) в очередь RabbitMQ. Отвечаем шлюзу HTTP 200. 5. Фоновый воркер забирает задачу, генерирует PDF (reportlab), грузит в MinIO (boto3) и сохраняет ссылку в БД.