""" 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()