import uuid from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from app.core.database import get_db from app.models import User, Task, Blocker, AuditAction from app.schemas.blocker import ( BlockerCreate, BlockerResolve, BlockerResponse, BlockerListResponse, BlockerUserInfo ) from app.middleware.auth import get_current_user, check_task_access, check_task_edit_access from app.middleware.audit import get_audit_metadata from app.services.notification_service import NotificationService from app.services.audit_service import AuditService router = APIRouter(tags=["blockers"]) def blocker_to_response(blocker: Blocker) -> BlockerResponse: """Convert Blocker model to BlockerResponse.""" return BlockerResponse( id=blocker.id, task_id=blocker.task_id, reason=blocker.reason, resolution_note=blocker.resolution_note, created_at=blocker.created_at, resolved_at=blocker.resolved_at, reporter=BlockerUserInfo( id=blocker.reporter.id, name=blocker.reporter.name, email=blocker.reporter.email, ), resolver=BlockerUserInfo( id=blocker.resolver.id, name=blocker.resolver.name, email=blocker.resolver.email, ) if blocker.resolver else None, ) @router.post("/api/tasks/{task_id}/blockers", response_model=BlockerResponse, status_code=status.HTTP_201_CREATED) async def create_blocker( task_id: str, blocker_data: BlockerCreate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Mark a task as blocked with a reason.""" task = db.query(Task).filter(Task.id == task_id).first() if not task: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Task not found", ) if not check_task_edit_access(current_user, task, task.project): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied", ) # Check if task is already blocked with an unresolved blocker existing_blocker = db.query(Blocker).filter( Blocker.task_id == task_id, Blocker.resolved_at == None, ).first() if existing_blocker: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Task already has an unresolved blocker", ) # Create blocker record blocker = Blocker( id=str(uuid.uuid4()), task_id=task_id, reported_by=current_user.id, reason=blocker_data.reason, ) db.add(blocker) # Update task blocker_flag task.blocker_flag = True # Notify project owner (auto-publishes after commit) NotificationService.notify_blocker(db, task, current_user, blocker_data.reason) # Audit log AuditService.log_event( db=db, event_type="task.blocker", resource_type="task", action=AuditAction.UPDATE, user_id=current_user.id, resource_id=task.id, changes=[{"field": "blocker_flag", "old_value": False, "new_value": True}], request_metadata=get_audit_metadata(request), ) db.commit() db.refresh(blocker) return blocker_to_response(blocker) @router.put("/api/blockers/{blocker_id}/resolve", response_model=BlockerResponse) async def resolve_blocker( blocker_id: str, resolve_data: BlockerResolve, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Resolve a blocker with a resolution note.""" blocker = db.query(Blocker).filter(Blocker.id == blocker_id).first() if not blocker: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Blocker not found", ) if blocker.resolved_at is not None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Blocker is already resolved", ) task = blocker.task if not check_task_edit_access(current_user, task, task.project): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied", ) # Update blocker blocker.resolved_by = current_user.id blocker.resolution_note = resolve_data.resolution_note # Use naive datetime for consistency with database storage blocker.resolved_at = datetime.now(timezone.utc).replace(tzinfo=None) # Check if there are other unresolved blockers other_blockers = db.query(Blocker).filter( Blocker.task_id == task.id, Blocker.id != blocker_id, Blocker.resolved_at == None, ).count() if other_blockers == 0: task.blocker_flag = False # Notify reporter that blocker is resolved (auto-publishes after commit) NotificationService.notify_blocker_resolved(db, task, current_user, blocker.reported_by) db.commit() db.refresh(blocker) return blocker_to_response(blocker) @router.get("/api/tasks/{task_id}/blockers", response_model=BlockerListResponse) async def list_blockers( task_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """List all blockers (history) for a task.""" task = db.query(Task).filter(Task.id == task_id).first() if not task: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Task not found", ) if not check_task_access(current_user, task, task.project): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied", ) blockers = db.query(Blocker).filter( Blocker.task_id == task_id, ).order_by(Blocker.created_at.desc()).all() return BlockerListResponse( blockers=[blocker_to_response(b) for b in blockers], total=len(blockers), ) @router.get("/api/blockers/{blocker_id}", response_model=BlockerResponse) async def get_blocker( blocker_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Get a specific blocker by ID.""" blocker = db.query(Blocker).filter(Blocker.id == blocker_id).first() if not blocker: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Blocker not found", ) task = blocker.task if not check_task_access(current_user, task, task.project): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied", ) return blocker_to_response(blocker)