""" Enhanced file management API endpoints """ from typing import List, Optional, Union, Dict, Any from datetime import date, datetime from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from pydantic import BaseModel, Field, ConfigDict from app.database.base import get_db from app.models import ( File, FileStatus, FileType, Employee, User, FileStatusHistory, FileTransferHistory, FileArchiveInfo, FileClosureChecklist, FileAlert ) from app.services.file_management import FileManagementService, FileManagementError, FileStatusWorkflow from app.auth.security import get_current_user from app.utils.logging import app_logger router = APIRouter() logger = app_logger # Pydantic schemas for requests/responses class FileStatusChangeRequest(BaseModel): """Request to change file status""" new_status: str = Field(..., description="New status code") notes: Optional[str] = Field(None, description="Notes about the status change") validate_transition: bool = Field(True, description="Whether to validate the status transition") class FileClosureRequest(BaseModel): """Request to close a file""" force_close: bool = Field(False, description="Force closure even if there are warnings") final_payment_amount: Optional[float] = Field(None, gt=0, description="Final payment amount") closing_notes: Optional[str] = Field(None, description="Notes about file closure") class FileReopenRequest(BaseModel): """Request to reopen a closed file""" new_status: str = Field("ACTIVE", description="Status to reopen file to") notes: Optional[str] = Field(None, description="Notes about reopening") class FileTransferRequest(BaseModel): """Request to transfer file to different attorney""" new_attorney_id: str = Field(..., description="Employee ID of new attorney") transfer_reason: Optional[str] = Field(None, description="Reason for transfer") class FileArchiveRequest(BaseModel): """Request to archive a file""" archive_location: Optional[str] = Field(None, description="Physical or digital archive location") notes: Optional[str] = Field(None, description="Archive notes") class BulkStatusUpdateRequest(BaseModel): """Request to update status for multiple files""" file_numbers: List[str] = Field(..., max_length=100, description="List of file numbers") new_status: str = Field(..., description="New status for all files") notes: Optional[str] = Field(None, description="Notes for all status changes") class FileStatusHistoryResponse(BaseModel): """Response for file status history""" id: int old_status: str new_status: str change_date: datetime changed_by: str notes: Optional[str] = None system_generated: bool class FileTransferHistoryResponse(BaseModel): """Response for file transfer history""" id: int old_attorney_id: str new_attorney_id: str transfer_date: datetime authorized_by_name: str reason: Optional[str] = None old_hourly_rate: Optional[float] = None new_hourly_rate: Optional[float] = None model_config = ConfigDict(from_attributes=True) class FileClosureSummaryResponse(BaseModel): """Response for file closure summary""" file_no: str closure_date: date actions_taken: List[str] warnings: List[str] final_balance: float trust_balance: float class FileValidationResponse(BaseModel): """Response for file validation checks""" file_no: str current_status: str valid_transitions: List[str] can_close: bool blocking_issues: List[str] warnings: List[str] class ClosureCandidateResponse(BaseModel): """Response for file closure candidates""" file_no: str client_name: str attorney: str opened_date: date last_activity: Optional[date] = None outstanding_balance: float status: str class BulkOperationResult(BaseModel): """Result of bulk operation""" successful: List[str] failed: List[Dict[str, str]] total: int # File status management endpoints @router.post("/{file_no}/change-status") async def change_file_status( file_no: str, request: FileStatusChangeRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Change file status with workflow validation""" try: service = FileManagementService(db) # Get the old status before changing old_file = db.query(File).filter(File.file_no == file_no).first() old_status = old_file.status if old_file else None file_obj = service.change_file_status( file_no=file_no, new_status=request.new_status, user_id=current_user.id, notes=request.notes, validate_transition=request.validate_transition ) # Log workflow event for file status change try: from app.services.workflow_integration import log_file_status_change_sync log_file_status_change_sync( db=db, file_no=file_no, old_status=old_status, new_status=request.new_status, user_id=current_user.id, notes=request.notes ) except Exception as e: # Don't fail the operation if workflow logging fails logger.warning(f"Failed to log workflow event for file {file_no}: {str(e)}") return { "message": f"File {file_no} status changed to {request.new_status}", "file_no": file_obj.file_no, "old_status": file_obj.status, "new_status": request.new_status } except FileManagementError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) @router.get("/{file_no}/valid-transitions") async def get_valid_status_transitions( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get valid status transitions for a file""" file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) workflow = FileStatusWorkflow() valid_transitions = workflow.get_valid_transitions(file_obj.status) return { "file_no": file_no, "current_status": file_obj.status, "valid_transitions": valid_transitions } @router.get("/{file_no}/closure-validation", response_model=FileValidationResponse) async def validate_file_closure( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Validate if file is ready for closure""" try: service = FileManagementService(db) file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) validation_result = service._validate_file_closure(file_obj) workflow = FileStatusWorkflow() return FileValidationResponse( file_no=file_no, current_status=file_obj.status, valid_transitions=workflow.get_valid_transitions(file_obj.status), can_close=validation_result["can_close"], blocking_issues=validation_result.get("blocking_issues", []), warnings=validation_result.get("warnings", []) ) except FileManagementError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) @router.post("/{file_no}/close", response_model=FileClosureSummaryResponse) async def close_file( file_no: str, request: FileClosureRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Close a file with automated closure process""" try: service = FileManagementService(db) closure_summary = service.close_file( file_no=file_no, user_id=current_user.id, force_close=request.force_close, final_payment_amount=request.final_payment_amount, closing_notes=request.closing_notes ) return FileClosureSummaryResponse(**closure_summary) except FileManagementError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) @router.post("/{file_no}/reopen") async def reopen_file( file_no: str, request: FileReopenRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Reopen a closed file""" try: service = FileManagementService(db) file_obj = service.reopen_file( file_no=file_no, user_id=current_user.id, new_status=request.new_status, notes=request.notes ) return { "message": f"File {file_no} reopened with status {request.new_status}", "file_no": file_obj.file_no, "new_status": file_obj.status } except FileManagementError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) @router.post("/{file_no}/transfer") async def transfer_file( file_no: str, request: FileTransferRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Transfer file to a different attorney""" try: service = FileManagementService(db) file_obj = service.transfer_file( file_no=file_no, new_attorney_id=request.new_attorney_id, user_id=current_user.id, transfer_reason=request.transfer_reason ) return { "message": f"File {file_no} transferred to attorney {request.new_attorney_id}", "file_no": file_obj.file_no, "new_attorney": file_obj.empl_num, "new_rate": file_obj.rate_per_hour } except FileManagementError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) @router.post("/{file_no}/archive") async def archive_file( file_no: str, request: FileArchiveRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Archive a closed file""" try: service = FileManagementService(db) file_obj = service.archive_file( file_no=file_no, user_id=current_user.id, archive_location=request.archive_location, notes=request.notes ) return { "message": f"File {file_no} has been archived", "file_no": file_obj.file_no, "status": file_obj.status, "archive_location": request.archive_location } except FileManagementError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) # File history endpoints @router.get("/{file_no}/status-history", response_model=List[FileStatusHistoryResponse]) async def get_file_status_history( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get status change history for a file""" # Verify file exists file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) service = FileManagementService(db) history = service.get_file_status_history(file_no) return [FileStatusHistoryResponse(**item) for item in history] @router.get("/{file_no}/transfer-history", response_model=List[FileTransferHistoryResponse]) async def get_file_transfer_history( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get transfer history for a file""" # Verify file exists file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) transfers = db.query(FileTransferHistory).filter( FileTransferHistory.file_no == file_no ).order_by(FileTransferHistory.transfer_date.desc()).all() return transfers # Bulk operations @router.post("/bulk-status-update", response_model=BulkOperationResult) async def bulk_status_update( request: BulkStatusUpdateRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Update status for multiple files""" try: service = FileManagementService(db) results = service.bulk_status_update( file_numbers=request.file_numbers, new_status=request.new_status, user_id=current_user.id, notes=request.notes ) return BulkOperationResult(**results) except FileManagementError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) # Checklist endpoints class ChecklistItemRequest(BaseModel): item_name: str item_description: Optional[str] = None is_required: bool = True sort_order: int = 0 class ChecklistItemUpdateRequest(BaseModel): item_name: Optional[str] = None item_description: Optional[str] = None is_required: Optional[bool] = None is_completed: Optional[bool] = None sort_order: Optional[int] = None notes: Optional[str] = None @router.get("/{file_no}/closure-checklist") async def get_closure_checklist( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) return service.get_closure_checklist(file_no) @router.post("/{file_no}/closure-checklist") async def add_checklist_item( file_no: str, request: ChecklistItemRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: item = service.add_checklist_item( file_no=file_no, item_name=request.item_name, item_description=request.item_description, is_required=request.is_required, sort_order=request.sort_order, ) return { "id": item.id, "file_no": item.file_no, "item_name": item.item_name, "item_description": item.item_description, "is_required": item.is_required, "is_completed": item.is_completed, "sort_order": item.sort_order, } except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.put("/closure-checklist/{item_id}") async def update_checklist_item( item_id: int, request: ChecklistItemUpdateRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: item = service.update_checklist_item( item_id=item_id, item_name=request.item_name, item_description=request.item_description, is_required=request.is_required, is_completed=request.is_completed, sort_order=request.sort_order, user_id=current_user.id, notes=request.notes, ) return { "id": item.id, "file_no": item.file_no, "item_name": item.item_name, "item_description": item.item_description, "is_required": item.is_required, "is_completed": item.is_completed, "completed_date": item.completed_date, "completed_by_name": item.completed_by_name, "notes": item.notes, "sort_order": item.sort_order, } except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.delete("/closure-checklist/{item_id}") async def delete_checklist_item( item_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: service.delete_checklist_item(item_id=item_id) return {"message": "Checklist item deleted"} except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) # Alerts endpoints class AlertCreateRequest(BaseModel): alert_type: str title: str message: str alert_date: date notify_attorney: bool = True notify_admin: bool = False notification_days_advance: int = 7 class AlertUpdateRequest(BaseModel): title: Optional[str] = None message: Optional[str] = None alert_date: Optional[date] = None is_active: Optional[bool] = None @router.post("/{file_no}/alerts") async def create_alert( file_no: str, request: AlertCreateRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: alert = service.create_alert( file_no=file_no, alert_type=request.alert_type, title=request.title, message=request.message, alert_date=request.alert_date, notify_attorney=request.notify_attorney, notify_admin=request.notify_admin, notification_days_advance=request.notification_days_advance, ) return { "id": alert.id, "file_no": alert.file_no, "alert_type": alert.alert_type, "title": alert.title, "message": alert.message, "alert_date": alert.alert_date, "is_active": alert.is_active, } except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.get("/{file_no}/alerts") async def get_alerts( file_no: str, active_only: bool = Query(True), upcoming_only: bool = Query(False), limit: int = Query(100, ge=1, le=500), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) alerts = service.get_alerts( file_no=file_no, active_only=active_only, upcoming_only=upcoming_only, limit=limit, ) return [ { "id": a.id, "file_no": a.file_no, "alert_type": a.alert_type, "title": a.title, "message": a.message, "alert_date": a.alert_date, "is_active": a.is_active, "is_acknowledged": a.is_acknowledged, } for a in alerts ] @router.post("/alerts/{alert_id}/acknowledge") async def acknowledge_alert( alert_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: alert = service.acknowledge_alert(alert_id=alert_id, user_id=current_user.id) return {"message": "Alert acknowledged", "id": alert.id} except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.put("/alerts/{alert_id}") async def update_alert( alert_id: int, request: AlertUpdateRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: alert = service.update_alert( alert_id=alert_id, title=request.title, message=request.message, alert_date=request.alert_date, is_active=request.is_active, ) return {"message": "Alert updated", "id": alert.id} except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.delete("/alerts/{alert_id}") async def delete_alert( alert_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: service.delete_alert(alert_id=alert_id) return {"message": "Alert deleted"} except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) # Relationships endpoints class RelationshipCreateRequest(BaseModel): target_file_no: str relationship_type: str notes: Optional[str] = None @router.post("/{file_no}/relationships") async def create_relationship( file_no: str, request: RelationshipCreateRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: rel = service.create_relationship( source_file_no=file_no, target_file_no=request.target_file_no, relationship_type=request.relationship_type, user_id=current_user.id, notes=request.notes, ) return { "id": rel.id, "source_file_no": rel.source_file_no, "target_file_no": rel.target_file_no, "relationship_type": rel.relationship_type, "notes": rel.notes, } except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.get("/{file_no}/relationships") async def get_relationships( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) return service.get_relationships(file_no=file_no) @router.delete("/relationships/{relationship_id}") async def delete_relationship( relationship_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): service = FileManagementService(db) try: service.delete_relationship(relationship_id=relationship_id) return {"message": "Relationship deleted"} except FileManagementError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) # File queries and reports @router.get("/by-status/{status}") async def get_files_by_status( status: str, attorney_id: Optional[str] = Query(None, description="Filter by attorney ID"), limit: int = Query(100, ge=1, le=500, description="Maximum number of files to return"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get files by status with optional attorney filter""" service = FileManagementService(db) files = service.get_files_by_status(status, attorney_id, limit) return [ { "file_no": f.file_no, "client_name": f"{f.owner.first or ''} {f.owner.last}".strip() if f.owner else "Unknown", "regarding": f.regarding, "attorney": f.empl_num, "opened_date": f.opened, "closed_date": f.closed, "status": f.status } for f in files ] @router.get("/closure-candidates", response_model=List[ClosureCandidateResponse]) async def get_closure_candidates( days_inactive: int = Query(90, ge=30, le=365, description="Days since last activity"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get files that are candidates for closure""" service = FileManagementService(db) candidates = service.get_closure_candidates(days_inactive) return [ClosureCandidateResponse(**candidate) for candidate in candidates] # Lookup endpoints @router.get("/statuses") async def get_file_statuses( active_only: bool = Query(True, description="Return only active statuses"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get available file statuses""" query = db.query(FileStatus) if active_only: query = query.filter(FileStatus.active == True) statuses = query.order_by(FileStatus.sort_order, FileStatus.status_code).all() return [ { "status_code": s.status_code, "description": s.description, "active": s.active, "send": s.send, "footer_code": s.footer_code } for s in statuses ] @router.get("/types") async def get_file_types( active_only: bool = Query(True, description="Return only active file types"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get available file types""" query = db.query(FileType) if active_only: query = query.filter(FileType.active == True) types = query.order_by(FileType.type_code).all() return [ { "type_code": t.type_code, "description": t.description, "default_rate": t.default_rate, "active": t.active } for t in types ] @router.get("/attorneys") async def get_attorneys( active_only: bool = Query(True, description="Return only active attorneys"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get available attorneys for file assignment""" query = db.query(Employee) if active_only: query = query.filter(Employee.active == True) attorneys = query.order_by(Employee.last_name, Employee.first_name).all() return [ { "empl_num": a.empl_num, "name": f"{a.first_name or ''} {a.last_name}".strip(), "title": a.title, "rate_per_hour": a.rate_per_hour, "active": a.active } for a in attorneys ]