import json from datetime import datetime, timezone from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy import select from sqlalchemy.orm import Session from app.core.database import get_db from app.core.security import create_access_token, decode_access_token from app.models import Deposit, SpinSession, User from app.schemas import ( DepositRequest, DepositResponse, DevLoginRequest, GameInfo, PaylineWinResponse, SessionSpinResponse, SpinOutcomeResponse, SpinRequest, TelegramAuthRequest, TokenResponse, UserResponse, WalletResponse, ) from app.slots.registry import get_slot, list_slots router = APIRouter() security = HTTPBearer() ALLOWED_DEPOSIT_AMOUNTS = {Decimal("100"), Decimal("500"), Decimal("1000")} def user_to_response(user: User) -> UserResponse: return UserResponse( id=user.id, telegram_id=user.telegram_id, username=user.username, first_name=user.first_name, balance=user.balance, agreement_accepted=user.agreement_accepted_at is not None, agreement_accepted_at=user.agreement_accepted_at, created_at=user.created_at, ) def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db), ) -> User: user_id = decode_access_token(credentials.credentials) if user_id is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") user = db.execute(select(User).where(User.id == int(user_id))).scalar_one_or_none() if user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") return user def slot_to_info(slot) -> GameInfo: data = slot.info() return GameInfo(**data) @router.post("/auth/dev-login", response_model=TokenResponse) def dev_login(payload: DevLoginRequest, db: Session = Depends(get_db)) -> TokenResponse: user = db.execute(select(User).where(User.telegram_id == payload.telegram_id)).scalar_one_or_none() if user is None: user = User( telegram_id=payload.telegram_id, username=payload.username, first_name=payload.first_name, balance=Decimal("0.00"), ) db.add(user) db.commit() db.refresh(user) else: user.username = payload.username user.first_name = payload.first_name db.commit() db.refresh(user) token = create_access_token(str(user.id)) return TokenResponse(access_token=token) @router.post("/auth/telegram", response_model=TokenResponse) def telegram_auth(_payload: TelegramAuthRequest) -> TokenResponse: # BOT_TOKEN not configured yet — wire real initData validation later. raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Telegram auth requires BOT_TOKEN. Use /auth/dev-login for local development.", ) @router.get("/me", response_model=UserResponse) def get_me(user: User = Depends(get_current_user)) -> UserResponse: return user_to_response(user) @router.post("/me/agreement", response_model=UserResponse) def accept_agreement( user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> UserResponse: if user.agreement_accepted_at is None: user.agreement_accepted_at = datetime.now(timezone.utc) db.commit() db.refresh(user) return user_to_response(user) @router.get("/wallet", response_model=WalletResponse) def get_wallet( user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> WalletResponse: deposits = ( db.execute( select(Deposit).where(Deposit.user_id == user.id).order_by(Deposit.created_at.desc()).limit(20) ) .scalars() .all() ) return WalletResponse( balance=user.balance, deposits=[DepositResponse.model_validate(d) for d in deposits], ) @router.post("/wallet/deposit", response_model=WalletResponse) def deposit_stub( payload: DepositRequest, user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> WalletResponse: if user.agreement_accepted_at is None: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Accept user agreement first", ) amount = payload.amount.quantize(Decimal("0.01")) if amount not in ALLOWED_DEPOSIT_AMOUNTS: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Amount must be one of: {sorted(ALLOWED_DEPOSIT_AMOUNTS)}", ) deposit = Deposit(user_id=user.id, amount=amount) user.balance = (user.balance or Decimal("0.00")) + amount db.add(deposit) db.commit() db.refresh(user) deposits = ( db.execute( select(Deposit).where(Deposit.user_id == user.id).order_by(Deposit.created_at.desc()).limit(20) ) .scalars() .all() ) return WalletResponse( balance=user.balance, deposits=[DepositResponse.model_validate(d) for d in deposits], ) @router.get("/games", response_model=list[GameInfo]) def list_games() -> list[GameInfo]: return [slot_to_info(slot) for slot in list_slots()] @router.get("/games/{slug}", response_model=GameInfo) def get_game(slug: str) -> GameInfo: slot = get_slot(slug) if slot is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Game not found") return slot_to_info(slot) @router.post("/games/{slug}/spin", response_model=SessionSpinResponse) def spin_game( slug: str, payload: SpinRequest, user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> SessionSpinResponse: if user.agreement_accepted_at is None: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Accept user agreement first", ) slot = get_slot(slug) if slot is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Game not found") if slot.status != "available": raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Game is not available yet", ) bet = payload.bet.quantize(Decimal("0.01")) spin_count = payload.spin_count try: slot.validate_bet(bet) slot.validate_spin_count(spin_count) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc total_bet = (bet * spin_count).quantize(Decimal("0.01")) balance = user.balance or Decimal("0.00") if balance < total_bet: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient balance") user.balance = balance - total_bet try: session_result = slot.play_session(bet, spin_count) except Exception: db.rollback() raise user.balance = (user.balance or Decimal("0.00")) + session_result.total_win record = SpinSession( user_id=user.id, slug=slug, bet=bet, spin_count=spin_count, total_win=session_result.total_win, payload_json=json.dumps(session_result.to_payload(), ensure_ascii=False), ) db.add(record) db.commit() db.refresh(user) return SessionSpinResponse( slug=slug, bet=session_result.bet, spin_count=session_result.spin_count, total_bet=session_result.total_bet, total_win=session_result.total_win, max_level=session_result.max_level, trigger_count=session_result.trigger_count, balance=user.balance, spins=[ SpinOutcomeResponse( index=s.index, grid=s.grid, line_wins=[ PaylineWinResponse( line_index=w.line_index, symbol=w.symbol, count=w.count, amount=w.amount, path=w.path, ) for w in s.line_wins ], scatter_count=s.scatter_count, scatter_win=s.scatter_win, total_win=s.total_win, level=s.level, multiplier=s.multiplier, meta_triggered=s.meta_triggered, ) for s in session_result.spins ], )