from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, Form from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from typing import Optional, Dict, Any import logging from app.core.database import get_db from app.services.file_service import file_service from app.models.trip import Trip from app.models.expense import Expense logger = logging.getLogger(__name__) router = APIRouter() @router.post("/receipt") async def upload_receipt( file: UploadFile = File(...), trip_id: int = Form(...), expense_id: Optional[int] = Form(None), db: Session = Depends(get_db) ): """ Upload a receipt image for a trip expense. Args: file: Image file to upload trip_id: ID of the trip expense_id: Optional ID of the expense (for better file organization) db: Database session Returns: JSON response with file URLs and metadata """ try: # Validate that trip exists and user has access trip = db.query(Trip).filter(Trip.id == trip_id, Trip.is_active == "active").first() if not trip: raise HTTPException(status_code=404, detail="Trip not found") # If expense_id is provided, validate it exists and belongs to the trip if expense_id: expense = db.query(Expense).filter( Expense.id == expense_id, Expense.trip_id == trip_id ).first() if not expense: raise HTTPException(status_code=404, detail="Expense not found or doesn't belong to this trip") # Upload and process the file file_url, thumbnail_url, metadata = await file_service.save_upload_file( file=file, trip_id=trip_id, expense_id=expense_id ) # Return success response response_data = { "success": True, "file_url": file_url, "thumbnail_url": thumbnail_url, "metadata": { "filename": metadata.get("filename"), "file_size": metadata.get("file_size"), "original_size": metadata.get("original_size"), "resized": metadata.get("resized"), "new_size": metadata.get("new_size"), "format": metadata.get("format") } } logger.info(f"Successfully uploaded receipt for trip {trip_id}, expense {expense_id}: {file_url}") return JSONResponse(content=response_data) except HTTPException: # Re-raise HTTP exceptions (validation errors, etc.) raise except Exception as e: logger.error(f"Error uploading receipt: {e}") raise HTTPException(status_code=500, detail="Failed to upload receipt") @router.delete("/receipt") async def delete_receipt( file_url: str, db: Session = Depends(get_db) ): """ Delete a receipt image file. Args: file_url: URL of the file to delete db: Database session Returns: JSON response indicating success or failure """ try: # Validate file exists if not file_service.file_exists(file_url): raise HTTPException(status_code=404, detail="File not found") # Delete the file success = file_service.delete_file(file_url) if success: logger.info(f"Successfully deleted receipt: {file_url}") return JSONResponse(content={"success": True, "message": "File deleted successfully"}) else: raise HTTPException(status_code=500, detail="Failed to delete file") except HTTPException: # Re-raise HTTP exceptions raise except Exception as e: logger.error(f"Error deleting receipt: {e}") raise HTTPException(status_code=500, detail="Failed to delete receipt") @router.get("/receipt/validate") async def validate_receipt_url( file_url: str, db: Session = Depends(get_db) ): """ Validate that a receipt file exists and is accessible. Args: file_url: URL of the file to validate db: Database session Returns: JSON response with validation result """ try: exists = file_service.file_exists(file_url) file_path = file_service.get_file_path(file_url) if exists: # Get additional file info stat = file_path.stat() return JSONResponse(content={ "success": True, "exists": True, "file_size": stat.st_size, "modified": stat.st_mtime }) else: return JSONResponse(content={ "success": False, "exists": False, "message": "File not found" }) except Exception as e: logger.error(f"Error validating receipt URL: {e}") raise HTTPException(status_code=500, detail="Failed to validate receipt") @router.post("/cleanup") async def cleanup_orphaned_files( valid_file_urls: list[str], db: Session = Depends(get_db) ): """ Clean up orphaned receipt files that are no longer referenced. This is a maintenance endpoint that should be called periodically to clean up files that are no longer associated with any expenses. Args: valid_file_urls: List of file URLs that should be kept db: Database session Returns: JSON response with cleanup results """ try: deleted_count = file_service.cleanup_orphaned_files(valid_file_urls) logger.info(f"Cleanup completed. Deleted {deleted_count} orphaned files") return JSONResponse(content={ "success": True, "deleted_count": deleted_count, "message": f"Cleaned up {deleted_count} orphaned files" }) except Exception as e: logger.error(f"Error during cleanup: {e}") raise HTTPException(status_code=500, detail="Failed to cleanup orphaned files") @router.delete("/receipt/{expense_id}") async def delete_expense_receipt( expense_id: int, db: Session = Depends(get_db) ): """ Delete receipt files associated with a specific expense. Args: expense_id: ID of the expense whose receipt should be deleted db: Database session Returns: JSON response indicating success or failure """ try: # Validate that expense exists expense = db.query(Expense).filter(Expense.id == expense_id).first() if not expense: raise HTTPException(status_code=404, detail="Expense not found") # Delete the receipt files success = file_service.delete_expense_files(expense_id, expense.receipt_url) if success: # Update the expense to remove the receipt_url reference expense.receipt_url = None db.commit() logger.info(f"Successfully deleted receipt files for expense {expense_id}") return JSONResponse(content={ "success": True, "message": "Receipt files deleted successfully" }) else: raise HTTPException(status_code=500, detail="Failed to delete receipt files") except HTTPException: # Re-raise HTTP exceptions raise except Exception as e: logger.error(f"Error deleting receipt files for expense {expense_id}: {e}") raise HTTPException(status_code=500, detail="Failed to delete receipt files") @router.delete("/files/batch") async def batch_delete_receipt_files( file_urls: list[str], db: Session = Depends(get_db) ): """ Delete multiple receipt files in batch. Args: file_urls: List of file URLs to delete db: Database session Returns: JSON response with batch deletion results """ try: if not file_urls: raise HTTPException(status_code=400, detail="No file URLs provided") deleted_count = 0 failed_files = [] for file_url in file_urls: if file_service.delete_receipt_file(file_url): deleted_count += 1 else: failed_files.append(file_url) logger.info(f"Batch deletion completed. Deleted: {deleted_count}, Failed: {len(failed_files)}") return JSONResponse(content={ "success": True, "deleted_count": deleted_count, "failed_count": len(failed_files), "failed_files": failed_files, "message": f"Deleted {deleted_count} files, {len(failed_files)} failed" }) except HTTPException: # Re-raise HTTP exceptions raise except Exception as e: logger.error(f"Error during batch file deletion: {e}") raise HTTPException(status_code=500, detail="Failed to delete files")