""" BillableTime service layer for business logic and database operations. This module handles all database operations for billable time entries, providing a clean separation between the API routes and data access layer. """ from typing import Optional from uuid import UUID from fastapi import HTTPException, status from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from api.models.billable_time import BillableTime as BillableTimeModel from api.models.client import Client from api.models.session import Session as SessionModel from api.models.work_item import WorkItem from api.schemas.billable_time import BillableTimeCreate, BillableTimeUpdate def get_billable_time_entries(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[BillableTimeModel], int]: """ Retrieve a paginated list of billable time entries. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of billable time entries, total count) Example: ```python entries, total = get_billable_time_entries(db, skip=0, limit=50) print(f"Retrieved {len(entries)} of {total} billable time entries") ``` """ # Get total count total = db.query(BillableTimeModel).count() # Get paginated results, ordered by start_time descending (newest first) entries = ( db.query(BillableTimeModel) .order_by(BillableTimeModel.start_time.desc()) .offset(skip) .limit(limit) .all() ) return entries, total def get_billable_time_by_id(db: Session, billable_time_id: UUID) -> BillableTimeModel: """ Retrieve a single billable time entry by its ID. Args: db: Database session billable_time_id: UUID of the billable time entry to retrieve Returns: BillableTimeModel: The billable time entry object Raises: HTTPException: 404 if billable time entry not found Example: ```python entry = get_billable_time_by_id(db, billable_time_id) print(f"Found entry: {entry.description}") ``` """ entry = db.query(BillableTimeModel).filter(BillableTimeModel.id == str(billable_time_id)).first() if not entry: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Billable time entry with ID {billable_time_id} not found" ) return entry def get_billable_time_by_session(db: Session, session_id: UUID, skip: int = 0, limit: int = 100) -> tuple[list[BillableTimeModel], int]: """ Retrieve billable time entries for a specific session. Args: db: Database session session_id: UUID of the session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of billable time entries, total count) Example: ```python entries, total = get_billable_time_by_session(db, session_id) print(f"Found {total} billable time entries for session") ``` """ # Get total count total = db.query(BillableTimeModel).filter(BillableTimeModel.session_id == str(session_id)).count() # Get paginated results entries = ( db.query(BillableTimeModel) .filter(BillableTimeModel.session_id == str(session_id)) .order_by(BillableTimeModel.start_time.desc()) .offset(skip) .limit(limit) .all() ) return entries, total def get_billable_time_by_work_item(db: Session, work_item_id: UUID, skip: int = 0, limit: int = 100) -> tuple[list[BillableTimeModel], int]: """ Retrieve billable time entries for a specific work item. Args: db: Database session work_item_id: UUID of the work item skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of billable time entries, total count) Example: ```python entries, total = get_billable_time_by_work_item(db, work_item_id) print(f"Found {total} billable time entries for work item") ``` """ # Get total count total = db.query(BillableTimeModel).filter(BillableTimeModel.work_item_id == str(work_item_id)).count() # Get paginated results entries = ( db.query(BillableTimeModel) .filter(BillableTimeModel.work_item_id == str(work_item_id)) .order_by(BillableTimeModel.start_time.desc()) .offset(skip) .limit(limit) .all() ) return entries, total def create_billable_time(db: Session, billable_time_data: BillableTimeCreate) -> BillableTimeModel: """ Create a new billable time entry. Args: db: Database session billable_time_data: Billable time creation data Returns: BillableTimeModel: The created billable time entry object Raises: HTTPException: 404 if referenced client, session, or work item not found HTTPException: 422 if validation fails HTTPException: 500 if database error occurs Example: ```python entry_data = BillableTimeCreate( client_id="123e4567-e89b-12d3-a456-426614174000", start_time=datetime.now(), duration_minutes=60, hourly_rate=150.00, total_amount=150.00, description="Database optimization", category="development" ) entry = create_billable_time(db, entry_data) print(f"Created billable time entry: {entry.id}") ``` """ try: # Validate foreign keys # Client is required client = db.query(Client).filter(Client.id == str(billable_time_data.client_id)).first() if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Client with ID {billable_time_data.client_id} not found" ) # Session is optional if billable_time_data.session_id: session = db.query(SessionModel).filter(SessionModel.id == str(billable_time_data.session_id)).first() if not session: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Session with ID {billable_time_data.session_id} not found" ) # Work item is optional if billable_time_data.work_item_id: work_item = db.query(WorkItem).filter(WorkItem.id == str(billable_time_data.work_item_id)).first() if not work_item: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Work item with ID {billable_time_data.work_item_id} not found" ) # Create new billable time entry instance db_billable_time = BillableTimeModel(**billable_time_data.model_dump()) # Add to database db.add(db_billable_time) db.commit() db.refresh(db_billable_time) return db_billable_time except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() # Handle foreign key constraint violations if "client_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid client_id: {billable_time_data.client_id}" ) elif "session_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid session_id: {billable_time_data.session_id}" ) elif "work_item_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid work_item_id: {billable_time_data.work_item_id}" ) else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create billable time entry: {str(e)}" ) def update_billable_time(db: Session, billable_time_id: UUID, billable_time_data: BillableTimeUpdate) -> BillableTimeModel: """ Update an existing billable time entry. Args: db: Database session billable_time_id: UUID of the billable time entry to update billable_time_data: Billable time update data (only provided fields will be updated) Returns: BillableTimeModel: The updated billable time entry object Raises: HTTPException: 404 if billable time entry, client, session, or work item not found HTTPException: 422 if validation fails HTTPException: 500 if database error occurs Example: ```python update_data = BillableTimeUpdate( duration_minutes=90, total_amount=225.00 ) entry = update_billable_time(db, billable_time_id, update_data) print(f"Updated billable time entry: {entry.description}") ``` """ # Get existing billable time entry entry = get_billable_time_by_id(db, billable_time_id) try: # Update only provided fields update_data = billable_time_data.model_dump(exclude_unset=True) # Validate foreign keys if being updated if "client_id" in update_data and update_data["client_id"]: client = db.query(Client).filter(Client.id == str(update_data["client_id"])).first() if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Client with ID {update_data['client_id']} not found" ) if "session_id" in update_data and update_data["session_id"]: session = db.query(SessionModel).filter(SessionModel.id == str(update_data["session_id"])).first() if not session: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Session with ID {update_data['session_id']} not found" ) if "work_item_id" in update_data and update_data["work_item_id"]: work_item = db.query(WorkItem).filter(WorkItem.id == str(update_data["work_item_id"])).first() if not work_item: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Work item with ID {update_data['work_item_id']} not found" ) # Validate end_time if being updated along with start_time if "end_time" in update_data and update_data["end_time"]: start_time = update_data.get("start_time", entry.start_time) if update_data["end_time"] < start_time: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="end_time must be after start_time" ) # Apply updates for field, value in update_data.items(): setattr(entry, field, value) db.commit() db.refresh(entry) return entry except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() if "client_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid client_id" ) elif "session_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid session_id" ) elif "work_item_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid work_item_id" ) else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update billable time entry: {str(e)}" ) def delete_billable_time(db: Session, billable_time_id: UUID) -> dict: """ Delete a billable time entry by its ID. Args: db: Database session billable_time_id: UUID of the billable time entry to delete Returns: dict: Success message Raises: HTTPException: 404 if billable time entry not found HTTPException: 500 if database error occurs Example: ```python result = delete_billable_time(db, billable_time_id) print(result["message"]) # "Billable time entry deleted successfully" ``` """ # Get existing billable time entry (raises 404 if not found) entry = get_billable_time_by_id(db, billable_time_id) try: db.delete(entry) db.commit() return { "message": "Billable time entry deleted successfully", "billable_time_id": str(billable_time_id) } except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete billable time entry: {str(e)}" )