import os import uuid import httpx from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from yookassa import Configuration, Payment from core.rabbitmq import publish_ticket_task from api.deps import get_current_user from database.models import Seat, Ticket, TicketStatus, User from database.session import get_db from schemas.ticket import ( TicketResponse, TicketScanRequest, TicketScanResponse, TicketBookRequest, TicketBookResponse, YookassaWebhook ) # Если у тебя есть готовая функция для Раббита, раскомментируй импорт (путь может отличаться) # from core.rabbitmq import publish_ticket_task router = APIRouter(prefix="/api/tickets", tags=["tickets"]) @router.get("/me", response_model=list[TicketResponse]) async def get_my_tickets( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> list[Ticket]: """Возвращает все оплаченные билеты текущего пользователя.""" result = await db.execute( select(Ticket) .where(Ticket.user_id == current_user.id, Ticket.status == TicketStatus.PAID) .options(selectinload(Ticket.seat).selectinload(Seat.tournament)) .order_by(Ticket.created_at.desc()) ) return list(result.scalars().all()) @router.post("/scan", response_model=TicketScanResponse) async def scan_ticket( body: TicketScanRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> TicketScanResponse: """Сканирует билет по secret_token (QR-код).""" result = await db.execute(select(Ticket).where(Ticket.secret_token == body.token)) ticket: Ticket | None = result.scalar_one_or_none() if ticket is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=TicketScanResponse(success=False, message="Билет не найден или подделка").model_dump(), ) if ticket.status == TicketStatus.SCANNED: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=TicketScanResponse(success=False, message="Билет уже отсканирован!", ticket_id=ticket.id).model_dump(), ) if ticket.status != TicketStatus.PAID: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=TicketScanResponse(success=False, message=f"Проход запрещен: статус билета '{ticket.status.value}'", ticket_id=ticket.id).model_dump(), ) ticket.status = TicketStatus.SCANNED await db.commit() return TicketScanResponse(success=True, message="Проход разрешен", ticket_id=ticket.id) @router.post("/book", response_model=TicketBookResponse) async def book_ticket( body: TicketBookRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Блокирует место на 15 минут и генерирует ссылку на оплату.""" seat_query = await db.execute( select(Seat).options(selectinload(Seat.tournament)).where(Seat.id == body.seat_id) ) seat = seat_query.scalar_one_or_none() if not seat: raise HTTPException(status_code=404, detail="Место не найдено") ticket_query = await db.execute(select(Ticket).where(Ticket.seat_id == seat.id)) existing_ticket = ticket_query.scalar_one_or_none() now = datetime.now(timezone.utc) # Проверка блокировок if existing_ticket: if existing_ticket.status == TicketStatus.PAID: raise HTTPException(status_code=400, detail="Место уже выкуплено") if existing_ticket.status == TicketStatus.LOCKED and existing_ticket.expires_at and existing_ticket.expires_at > now: if existing_ticket.user_id != current_user.id: raise HTTPException(status_code=400, detail="Место временно забронировано другим пользователем") description = f"Билет: {seat.tournament.title}, Сектор {seat.sector}, Ряд {seat.row}, Место {seat.number}"[:128] shop_id = os.getenv("YOOKASSA_SHOP_ID", "dummy") if shop_id == "test_shop": # Стучимся в наш мок-сервер mock_url = os.getenv("MOCK_API_URL", "http://192.168.149.101:8083/v3/payments") async with httpx.AsyncClient() as client: response = await client.post( mock_url, json={ "amount": {"value": f"{seat.price}.00", "currency": "RUB"}, "confirmation": {"type": "redirect", "return_url": "https://openticket.artifitial.ru/tickets"}, "capture": True, "description": description }, headers={"Idempotence-Key": str(uuid.uuid4())} ) response.raise_for_status() payment_data = response.json() payment_id = payment_data["id"] payment_url = payment_data["confirmation"]["confirmation_url"] else: # Реальная ЮKassa Configuration.account_id = shop_id Configuration.secret_key = os.getenv("YOOKASSA_SECRET_KEY") payment = Payment.create({ "amount": {"value": f"{seat.price}.00", "currency": "RUB"}, "confirmation": {"type": "redirect", "return_url": "https://openticket.artifitial.ru/tickets"}, "capture": True, "description": description }, str(uuid.uuid4())) payment_id = payment.id payment_url = payment.confirmation.confirmation_url # Фиксируем бронь if not existing_ticket: ticket = Ticket( seat_id=seat.id, user_id=current_user.id, status=TicketStatus.LOCKED, payment_id=payment_id, payment_url=payment_url, expires_at=now + timedelta(minutes=15) ) db.add(ticket) else: existing_ticket.user_id = current_user.id existing_ticket.status = TicketStatus.LOCKED existing_ticket.payment_id = payment_id existing_ticket.payment_url = payment_url existing_ticket.expires_at = now + timedelta(minutes=15) ticket = existing_ticket await db.commit() return TicketBookResponse(ticket_id=ticket.id, payment_url=payment_url) @router.post("/webhook/yookassa") async def yookassa_webhook( payload: YookassaWebhook, db: AsyncSession = Depends(get_db) ): """Принимает уведомления от платежного шлюза.""" query = await db.execute(select(Ticket).where(Ticket.payment_id == payload.object.id)) ticket = query.scalar_one_or_none() if not ticket: return {"status": "ignored", "reason": "ticket not found"} if payload.event == "payment.succeeded" and payload.object.status == "succeeded": if ticket.status != TicketStatus.PAID: ticket.status = TicketStatus.PAID await db.commit() # ---> ЗДЕСЬ ПУШИМ ЗАДАЧУ В RABBITMQ <--- # Если у тебя есть функция публикации: # ... ticket.status = TicketStatus.PAID await db.commit() # Отправляем в очередь await publish_ticket_task(ticket.id) # ... print(f"Оплата получена. Задача на PDF для билета {ticket.id} отправлена в очередь.") elif payload.event == "payment.canceled": if ticket.status == TicketStatus.LOCKED: await db.delete(ticket) await db.commit() return {"status": "ok"}