# app/api/v1/endpoints/items.py import logging from typing import List as PyList, Optional from fastapi import APIRouter, Depends, HTTPException, status, Response, Query from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.api.dependencies import get_current_user # --- Import Models Correctly --- from app.models import User as UserModel from app.models import Item as ItemModel # <-- IMPORT Item and alias it # --- End Import Models --- from app.schemas.item import ItemCreate, ItemUpdate, ItemPublic from app.crud import item as crud_item from app.crud import list as crud_list from app.core.exceptions import ItemNotFoundError, ListPermissionError, ConflictError logger = logging.getLogger(__name__) router = APIRouter() # --- Helper Dependency for Item Permissions --- # Now ItemModel is defined before being used as a type hint async def get_item_and_verify_access( item_id: int, db: AsyncSession = Depends(get_db), current_user: UserModel = Depends(get_current_user) ) -> ItemModel: """Dependency to get an item and verify the user has access to its list.""" item_db = await crud_item.get_item_by_id(db, item_id=item_id) if not item_db: raise ItemNotFoundError(item_id) # Check permission on the parent list try: await crud_list.check_list_permission(db=db, list_id=item_db.list_id, user_id=current_user.id) except ListPermissionError as e: # Re-raise with a more specific message raise ListPermissionError(item_db.list_id, "access this item's list") return item_db # --- Endpoints --- @router.post( "/lists/{list_id}/items", # Nested under lists response_model=ItemPublic, status_code=status.HTTP_201_CREATED, summary="Add Item to List", tags=["Items"] ) async def create_list_item( list_id: int, item_in: ItemCreate, db: AsyncSession = Depends(get_db), current_user: UserModel = Depends(get_current_user), ): """Adds a new item to a specific list. User must have access to the list.""" user_email = current_user.email # Access email attribute before async operations logger.info(f"User {user_email} adding item to list {list_id}: {item_in.name}") # Verify user has access to the target list try: await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id) except ListPermissionError as e: # Re-raise with a more specific message raise ListPermissionError(list_id, "add items to this list") created_item = await crud_item.create_item( db=db, item_in=item_in, list_id=list_id, user_id=current_user.id ) logger.info(f"Item '{created_item.name}' (ID: {created_item.id}) added to list {list_id} by user {user_email}.") return created_item @router.get( "/lists/{list_id}/items", # Nested under lists response_model=PyList[ItemPublic], summary="List Items in List", tags=["Items"] ) async def read_list_items( list_id: int, db: AsyncSession = Depends(get_db), current_user: UserModel = Depends(get_current_user), # Add sorting/filtering params later if needed: sort_by: str = 'created_at', order: str = 'asc' ): """Retrieves all items for a specific list if the user has access.""" user_email = current_user.email # Access email attribute before async operations logger.info(f"User {user_email} listing items for list {list_id}") # Verify user has access to the list try: await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id) except ListPermissionError as e: # Re-raise with a more specific message raise ListPermissionError(list_id, "view items in this list") items = await crud_item.get_items_by_list_id(db=db, list_id=list_id) return items @router.put( "/items/{item_id}", # Operate directly on item ID response_model=ItemPublic, summary="Update Item", tags=["Items"], responses={ status.HTTP_409_CONFLICT: {"description": "Conflict: Item has been modified by someone else"} } ) async def update_item( item_id: int, # Item ID from path item_in: ItemUpdate, item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access db: AsyncSession = Depends(get_db), current_user: UserModel = Depends(get_current_user), # Need user ID for completed_by ): """ Updates an item's details (name, quantity, is_complete, price). User must have access to the list the item belongs to. The client MUST provide the current `version` of the item in the `item_in` payload. If the version does not match, a 409 Conflict is returned. Sets/unsets `completed_by_id` based on `is_complete` flag. """ user_email = current_user.email # Access email attribute before async operations logger.info(f"User {user_email} attempting to update item ID: {item_id} with version {item_in.version}") # Permission check is handled by get_item_and_verify_access dependency try: updated_item = await crud_item.update_item( db=db, item_db=item_db, item_in=item_in, user_id=current_user.id ) logger.info(f"Item {item_id} updated successfully by user {user_email} to version {updated_item.version}.") return updated_item except ConflictError as e: logger.warning(f"Conflict updating item {item_id} for user {user_email}: {str(e)}") raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e)) except Exception as e: logger.error(f"Error updating item {item_id} for user {user_email}: {str(e)}") raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred while updating the item.") @router.delete( "/items/{item_id}", # Operate directly on item ID status_code=status.HTTP_204_NO_CONTENT, summary="Delete Item", tags=["Items"], responses={ status.HTTP_409_CONFLICT: {"description": "Conflict: Item has been modified, cannot delete specified version"} } ) async def delete_item( item_id: int, # Item ID from path expected_version: Optional[int] = Query(None, description="The expected version of the item to delete for optimistic locking."), item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access db: AsyncSession = Depends(get_db), current_user: UserModel = Depends(get_current_user), # Log who deleted it ): """ Deletes an item. User must have access to the list the item belongs to. If `expected_version` is provided and does not match the item's current version, a 409 Conflict is returned. """ user_email = current_user.email # Access email attribute before async operations logger.info(f"User {user_email} attempting to delete item ID: {item_id}, expected version: {expected_version}") # Permission check is handled by get_item_and_verify_access dependency if expected_version is not None and item_db.version != expected_version: logger.warning( f"Conflict deleting item {item_id} for user {user_email}. " f"Expected version {expected_version}, actual version {item_db.version}." ) raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Item has been modified. Expected version {expected_version}, but current version is {item_db.version}. Please refresh." ) await crud_item.delete_item(db=db, item_db=item_db) logger.info(f"Item {item_id} (version {item_db.version}) deleted successfully by user {user_email}.") return Response(status_code=status.HTTP_204_NO_CONTENT)