import uuid from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user from app.api.schemas import ParticipantResponse from app.core.database import get_db from app.models.lottery import Lottery, LotteryStatus from app.models.participant import Participant from app.models.user import User from app.services.membership_service import check_membership router = APIRouter(tags=["participants"]) @router.post("/lotteries/{lottery_id}/join") async def join_lottery( lottery_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ) -> dict: # Lock the lottery row so the capacity check + insert is atomic. # Without this, concurrent joins can all pass the count check and exceed # max_participants before any of them commits. result = await db.execute( select(Lottery).where(Lottery.id == lottery_id).with_for_update() ) lottery = result.scalar_one_or_none() if lottery is None: raise HTTPException(404, "Lottery not found") if lottery.status != LotteryStatus.ACTIVE: raise HTTPException(400, "Lottery is not active") # Check max participants inside the lock if lottery.max_participants is not None: count_result = await db.execute( select(func.count(Participant.id)).where(Participant.lottery_id == lottery_id) ) count = count_result.scalar() or 0 if count >= lottery.max_participants: raise HTTPException(400, "Lottery is full") # Check membership if required if lottery.require_membership: chat_ids = lottery.required_chat_ids or ( [lottery.target_chat_id] if lottery.target_chat_id else [] ) for i, cid in enumerate(chat_ids): is_member = await check_membership(cid, current_user.id) if not is_member: titles = lottery.required_chat_titles or [] label = titles[i] if i < len(titles) else str(cid) raise HTTPException(403, f"You must join {label} first") # Check passphrase if set for this group passphrases = lottery.required_chat_passphrases or [] if i < len(passphrases) and passphrases[i]: from app.models.passphrase_verification import PassphraseVerification pv = await db.execute( select(PassphraseVerification).where( PassphraseVerification.lottery_id == lottery_id, PassphraseVerification.chat_id == cid, PassphraseVerification.user_id == current_user.id, ) ) if pv.scalar_one_or_none() is None: titles = lottery.required_chat_titles or [] label = titles[i] if i < len(titles) else str(cid) raise HTTPException(403, f"Please send the passphrase in {label} first") # Check already joined existing = await db.execute( select(Participant).where( Participant.lottery_id == lottery_id, Participant.user_id == current_user.id, ) ) if existing.scalar_one_or_none() is not None: raise HTTPException(409, "Already joined this lottery") participant = Participant(lottery_id=lottery_id, user_id=current_user.id) db.add(participant) await db.commit() # Refresh count for post-join checks count_result = await db.execute( select(func.count(Participant.id)).where(Participant.lottery_id == lottery_id) ) new_count = count_result.scalar() or 0 # Always update the group announcement with the new participant count if lottery.announcement_targets or (lottery.announcement_message_id and lottery.target_chat_id): try: from app.api.routers.lotteries import sync_announcement await sync_announcement(db, lottery, force_send=False) except Exception: pass # Trigger auto-draw if participant count reached the threshold if lottery.auto_draw_count and new_count >= lottery.auto_draw_count: import asyncio from app.services.scheduler import run_auto_draw asyncio.create_task(run_auto_draw(lottery_id, trigger="auto_count")) return {"status": "joined"} @router.get("/lotteries/{lottery_id}/passphrase-status") async def get_passphrase_status( lottery_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ) -> dict: from app.models.passphrase_verification import PassphraseVerification result = await db.execute( select(PassphraseVerification.chat_id).where( PassphraseVerification.lottery_id == lottery_id, PassphraseVerification.user_id == current_user.id, ) ) return {"verified_chat_ids": [row[0] for row in result.all()]} @router.get("/lotteries/{lottery_id}/participants", response_model=list[ParticipantResponse]) async def list_participants( lottery_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ) -> list[ParticipantResponse]: result = await db.execute( select(Participant, User) .join(User, Participant.user_id == User.id) .where(Participant.lottery_id == lottery_id) .order_by(Participant.joined_at) ) return [ ParticipantResponse( user_id=user.id, username=user.username, first_name=user.first_name, last_name=user.last_name, joined_at=participant.joined_at, ) for participant, user in result.all() ]