104 lines
3.4 KiB
Python
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()
|