""" Billable Time API router for ClaudeTools. This module defines all REST API endpoints for managing billable time entries, including CRUD operations with proper authentication, validation, and error handling. """ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from api.database import get_db from api.middleware.auth import get_current_user from api.schemas.billable_time import ( BillableTimeCreate, BillableTimeResponse, BillableTimeUpdate, ) from api.services import billable_time_service # Create router with prefix and tags router = APIRouter() @router.get( "", response_model=dict, summary="List all billable time entries", description="Retrieve a paginated list of all billable time entries", status_code=status.HTTP_200_OK, ) def list_billable_time_entries( skip: int = Query( default=0, ge=0, description="Number of records to skip for pagination" ), limit: int = Query( default=100, ge=1, le=1000, description="Maximum number of records to return (max 1000)" ), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ List all billable time entries with pagination. - **skip**: Number of entries to skip (default: 0) - **limit**: Maximum number of entries to return (default: 100, max: 1000) Returns a list of billable time entries with pagination metadata. **Example Request:** ``` GET /api/billable-time?skip=0&limit=50 Authorization: Bearer ``` **Example Response:** ```json { "total": 25, "skip": 0, "limit": 50, "billable_time": [ { "id": "123e4567-e89b-12d3-a456-426614174000", "client_id": "456e7890-e89b-12d3-a456-426614174001", "session_id": "789e0123-e89b-12d3-a456-426614174002", "start_time": "2024-01-15T09:00:00Z", "duration_minutes": 120, "hourly_rate": 150.00, "total_amount": 300.00, "is_billable": true, "description": "Database optimization work", "category": "development", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ] } ``` """ try: entries, total = billable_time_service.get_billable_time_entries(db, skip, limit) return { "total": total, "skip": skip, "limit": limit, "billable_time": [BillableTimeResponse.model_validate(entry) for entry in entries] } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to retrieve billable time entries: {str(e)}" ) @router.get( "/{billable_time_id}", response_model=BillableTimeResponse, summary="Get billable time entry by ID", description="Retrieve a single billable time entry by its unique identifier", status_code=status.HTTP_200_OK, responses={ 200: { "description": "Billable time entry found and returned", "model": BillableTimeResponse, }, 404: { "description": "Billable time entry not found", "content": { "application/json": { "example": {"detail": "Billable time entry with ID 123e4567-e89b-12d3-a456-426614174000 not found"} } }, }, }, ) def get_billable_time_entry( billable_time_id: UUID, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Get a specific billable time entry by ID. - **billable_time_id**: UUID of the billable time entry to retrieve Returns the complete billable time entry details. **Example Request:** ``` GET /api/billable-time/123e4567-e89b-12d3-a456-426614174000 Authorization: Bearer ``` **Example Response:** ```json { "id": "123e4567-e89b-12d3-a456-426614174000", "work_item_id": "012e3456-e89b-12d3-a456-426614174003", "session_id": "789e0123-e89b-12d3-a456-426614174002", "client_id": "456e7890-e89b-12d3-a456-426614174001", "start_time": "2024-01-15T09:00:00Z", "end_time": "2024-01-15T11:00:00Z", "duration_minutes": 120, "hourly_rate": 150.00, "total_amount": 300.00, "is_billable": true, "description": "Database optimization and performance tuning", "category": "development", "notes": "Optimized queries and added indexes", "invoiced_at": null, "invoice_id": null, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ``` """ entry = billable_time_service.get_billable_time_by_id(db, billable_time_id) return BillableTimeResponse.model_validate(entry) @router.post( "", response_model=BillableTimeResponse, summary="Create new billable time entry", description="Create a new billable time entry with the provided details", status_code=status.HTTP_201_CREATED, responses={ 201: { "description": "Billable time entry created successfully", "model": BillableTimeResponse, }, 404: { "description": "Referenced client, session, or work item not found", "content": { "application/json": { "example": {"detail": "Client with ID 123e4567-e89b-12d3-a456-426614174000 not found"} } }, }, 422: { "description": "Validation error", "content": { "application/json": { "example": { "detail": [ { "loc": ["body", "client_id"], "msg": "field required", "type": "value_error.missing" } ] } } }, }, }, ) def create_billable_time_entry( billable_time_data: BillableTimeCreate, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Create a new billable time entry. Requires a valid JWT token with appropriate permissions. **Example Request:** ```json POST /api/billable-time Authorization: Bearer Content-Type: application/json { "client_id": "456e7890-e89b-12d3-a456-426614174001", "session_id": "789e0123-e89b-12d3-a456-426614174002", "work_item_id": "012e3456-e89b-12d3-a456-426614174003", "start_time": "2024-01-15T09:00:00Z", "end_time": "2024-01-15T11:00:00Z", "duration_minutes": 120, "hourly_rate": 150.00, "total_amount": 300.00, "is_billable": true, "description": "Database optimization and performance tuning", "category": "development", "notes": "Optimized queries and added indexes" } ``` **Example Response:** ```json { "id": "123e4567-e89b-12d3-a456-426614174000", "client_id": "456e7890-e89b-12d3-a456-426614174001", "start_time": "2024-01-15T09:00:00Z", "duration_minutes": 120, "hourly_rate": 150.00, "total_amount": 300.00, "is_billable": true, "category": "development", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ``` """ entry = billable_time_service.create_billable_time(db, billable_time_data) return BillableTimeResponse.model_validate(entry) @router.put( "/{billable_time_id}", response_model=BillableTimeResponse, summary="Update billable time entry", description="Update an existing billable time entry's details", status_code=status.HTTP_200_OK, responses={ 200: { "description": "Billable time entry updated successfully", "model": BillableTimeResponse, }, 404: { "description": "Billable time entry, client, session, or work item not found", "content": { "application/json": { "example": {"detail": "Billable time entry with ID 123e4567-e89b-12d3-a456-426614174000 not found"} } }, }, 422: { "description": "Validation error", "content": { "application/json": { "example": {"detail": "Invalid client_id"} } }, }, }, ) def update_billable_time_entry( billable_time_id: UUID, billable_time_data: BillableTimeUpdate, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Update an existing billable time entry. - **billable_time_id**: UUID of the billable time entry to update Only provided fields will be updated. All fields are optional. **Example Request:** ```json PUT /api/billable-time/123e4567-e89b-12d3-a456-426614174000 Authorization: Bearer Content-Type: application/json { "duration_minutes": 150, "total_amount": 375.00, "notes": "Additional optimization work performed" } ``` **Example Response:** ```json { "id": "123e4567-e89b-12d3-a456-426614174000", "client_id": "456e7890-e89b-12d3-a456-426614174001", "start_time": "2024-01-15T09:00:00Z", "duration_minutes": 150, "hourly_rate": 150.00, "total_amount": 375.00, "notes": "Additional optimization work performed", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T14:20:00Z" } ``` """ entry = billable_time_service.update_billable_time(db, billable_time_id, billable_time_data) return BillableTimeResponse.model_validate(entry) @router.delete( "/{billable_time_id}", response_model=dict, summary="Delete billable time entry", description="Delete a billable time entry by its ID", status_code=status.HTTP_200_OK, responses={ 200: { "description": "Billable time entry deleted successfully", "content": { "application/json": { "example": { "message": "Billable time entry deleted successfully", "billable_time_id": "123e4567-e89b-12d3-a456-426614174000" } } }, }, 404: { "description": "Billable time entry not found", "content": { "application/json": { "example": {"detail": "Billable time entry with ID 123e4567-e89b-12d3-a456-426614174000 not found"} } }, }, }, ) def delete_billable_time_entry( billable_time_id: UUID, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Delete a billable time entry. - **billable_time_id**: UUID of the billable time entry to delete This is a permanent operation and cannot be undone. **Example Request:** ``` DELETE /api/billable-time/123e4567-e89b-12d3-a456-426614174000 Authorization: Bearer ``` **Example Response:** ```json { "message": "Billable time entry deleted successfully", "billable_time_id": "123e4567-e89b-12d3-a456-426614174000" } ``` """ return billable_time_service.delete_billable_time(db, billable_time_id) @router.get( "/by-session/{session_id}", response_model=dict, summary="Get billable time by session", description="Retrieve billable time entries for a specific session", status_code=status.HTTP_200_OK, responses={ 200: { "description": "Billable time entries retrieved successfully", "content": { "application/json": { "example": { "total": 3, "skip": 0, "limit": 100, "billable_time": [] } } }, }, }, ) def get_billable_time_by_session( session_id: UUID, skip: int = Query( default=0, ge=0, description="Number of records to skip for pagination" ), limit: int = Query( default=100, ge=1, le=1000, description="Maximum number of records to return (max 1000)" ), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Get billable time entries for a specific session. - **session_id**: UUID of the session - **skip**: Number of entries to skip (default: 0) - **limit**: Maximum number of entries to return (default: 100, max: 1000) Returns a paginated list of billable time entries for the session. **Example Request:** ``` GET /api/billable-time/by-session/789e0123-e89b-12d3-a456-426614174002?skip=0&limit=50 Authorization: Bearer ``` **Example Response:** ```json { "total": 3, "skip": 0, "limit": 50, "billable_time": [ { "id": "123e4567-e89b-12d3-a456-426614174000", "session_id": "789e0123-e89b-12d3-a456-426614174002", "duration_minutes": 120, "total_amount": 300.00, "description": "Database optimization", "created_at": "2024-01-15T10:30:00Z" } ] } ``` """ try: entries, total = billable_time_service.get_billable_time_by_session(db, session_id, skip, limit) return { "total": total, "skip": skip, "limit": limit, "billable_time": [BillableTimeResponse.model_validate(entry) for entry in entries] } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to retrieve billable time entries: {str(e)}" ) @router.get( "/by-work-item/{work_item_id}", response_model=dict, summary="Get billable time by work item", description="Retrieve billable time entries for a specific work item", status_code=status.HTTP_200_OK, responses={ 200: { "description": "Billable time entries retrieved successfully", "content": { "application/json": { "example": { "total": 5, "skip": 0, "limit": 100, "billable_time": [] } } }, }, }, ) def get_billable_time_by_work_item( work_item_id: UUID, skip: int = Query( default=0, ge=0, description="Number of records to skip for pagination" ), limit: int = Query( default=100, ge=1, le=1000, description="Maximum number of records to return (max 1000)" ), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Get billable time entries for a specific work item. - **work_item_id**: UUID of the work item - **skip**: Number of entries to skip (default: 0) - **limit**: Maximum number of entries to return (default: 100, max: 1000) Returns a paginated list of billable time entries for the work item. **Example Request:** ``` GET /api/billable-time/by-work-item/012e3456-e89b-12d3-a456-426614174003?skip=0&limit=50 Authorization: Bearer ``` **Example Response:** ```json { "total": 5, "skip": 0, "limit": 50, "billable_time": [ { "id": "123e4567-e89b-12d3-a456-426614174000", "work_item_id": "012e3456-e89b-12d3-a456-426614174003", "duration_minutes": 120, "total_amount": 300.00, "description": "Bug fix and testing", "created_at": "2024-01-15T10:30:00Z" } ] } ``` """ try: entries, total = billable_time_service.get_billable_time_by_work_item(db, work_item_id, skip, limit) return { "total": total, "skip": skip, "limit": limit, "billable_time": [BillableTimeResponse.model_validate(entry) for entry in entries] } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to retrieve billable time entries: {str(e)}" )