This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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()
|
||||
]
|
||||
Reference in New Issue
Block a user