Files
delphi-database/app/api/file_management.py
HotSwapp ae4484381f progress
2025-08-16 10:05:42 -05:00

515 lines
16 KiB
Python

"""
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
)
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)
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
)
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)
)
# 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
]