Files
game-marathon/backend/app/services/disputes.py
2025-12-16 00:33:50 +07:00

104 lines
3.4 KiB
Python

"""
Dispute resolution service.
"""
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models import (
Dispute, DisputeStatus, DisputeVote,
Assignment, AssignmentStatus, Participant,
)
class DisputeService:
"""Service for dispute resolution logic"""
async def resolve_dispute(self, db: AsyncSession, dispute_id: int) -> tuple[str, int, int]:
"""
Resolve a dispute based on votes.
Returns:
Tuple of (result_status, votes_valid, votes_invalid)
"""
# Get dispute with votes and assignment
result = await db.execute(
select(Dispute)
.options(
selectinload(Dispute.votes),
selectinload(Dispute.assignment).selectinload(Assignment.participant),
)
.where(Dispute.id == dispute_id)
)
dispute = result.scalar_one_or_none()
if not dispute:
raise ValueError(f"Dispute {dispute_id} not found")
if dispute.status != DisputeStatus.OPEN.value:
raise ValueError(f"Dispute {dispute_id} is already resolved")
# Count votes
votes_valid = sum(1 for v in dispute.votes if v.vote is True)
votes_invalid = sum(1 for v in dispute.votes if v.vote is False)
# Determine result: tie goes to the accused (valid)
if votes_invalid > votes_valid:
# Proof is invalid - mark assignment as RETURNED
result_status = DisputeStatus.RESOLVED_INVALID.value
await self._handle_invalid_proof(db, dispute)
else:
# Proof is valid (or tie)
result_status = DisputeStatus.RESOLVED_VALID.value
# Update dispute
dispute.status = result_status
dispute.resolved_at = datetime.utcnow()
await db.commit()
return result_status, votes_valid, votes_invalid
async def _handle_invalid_proof(self, db: AsyncSession, dispute: Dispute) -> None:
"""
Handle the case when proof is determined to be invalid.
- Mark assignment as RETURNED
- Subtract points from participant
- Reset streak if it was affected
"""
assignment = dispute.assignment
participant = assignment.participant
# Subtract points that were earned
points_to_subtract = assignment.points_earned
participant.total_points = max(0, participant.total_points - points_to_subtract)
# Reset assignment
assignment.status = AssignmentStatus.RETURNED.value
assignment.points_earned = 0
# Keep proof data so it can be reviewed
print(f"[DisputeService] Assignment {assignment.id} marked as RETURNED, "
f"subtracted {points_to_subtract} points from participant {participant.id}")
async def get_pending_disputes(self, db: AsyncSession, older_than_hours: int = 24) -> list[Dispute]:
"""Get all open disputes older than specified hours"""
from datetime import timedelta
cutoff_time = datetime.utcnow() - timedelta(hours=older_than_hours)
result = await db.execute(
select(Dispute)
.where(
Dispute.status == DisputeStatus.OPEN.value,
Dispute.created_at < cutoff_time,
)
)
return list(result.scalars().all())
# Global service instance
dispute_service = DisputeService()