149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
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()
|
|
]
|