Add dispute system
This commit is contained in:
103
backend/app/services/disputes.py
Normal file
103
backend/app/services/disputes.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user