# services/inventory/app/api/transformations.py """ API endpoints for product transformations Following standardized URL structure with role-based access control """ from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Path, status from sqlalchemy.ext.asyncio import AsyncSession import structlog from app.core.database import get_db from app.services.transformation_service import TransformationService from app.schemas.inventory import ( ProductTransformationCreate, ProductTransformationResponse ) from app.models.inventory import ProductionStage from shared.auth.decorators import get_current_user_dep from shared.auth.access_control import require_user_role, admin_role_required from shared.routing import RouteBuilder logger = structlog.get_logger() # Create route builder for consistent URL structure route_builder = RouteBuilder('inventory') router = APIRouter(tags=["transformations"]) # Helper function to extract user ID from user object def get_current_user_id(current_user: dict = Depends(get_current_user_dep)) -> UUID: """Extract user ID from current user context""" user_id = current_user.get('user_id') if not user_id: # Handle service tokens that don't have UUID user_ids if current_user.get('type') == 'service': return None raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User ID not found in context" ) try: return UUID(user_id) except (ValueError, TypeError): return None @router.post( route_builder.build_base_route("transformations"), response_model=ProductTransformationResponse, status_code=status.HTTP_201_CREATED ) @require_user_role(['admin', 'owner', 'member']) async def create_transformation( transformation_data: ProductTransformationCreate, tenant_id: UUID = Path(..., description="Tenant ID"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Create a new product transformation (e.g., par-baked to fully baked)""" try: # Extract user ID - handle service tokens user_id = get_current_user_id(current_user) service = TransformationService() transformation = await service.create_transformation(transformation_data, tenant_id, user_id) return transformation except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: logger.error("Failed to create transformation", error=str(e), tenant_id=tenant_id) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create transformation" ) @router.get( route_builder.build_base_route("transformations"), response_model=List[ProductTransformationResponse] ) async def get_transformations( tenant_id: UUID = Path(..., description="Tenant ID"), skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=1000, description="Number of records to return"), ingredient_id: Optional[UUID] = Query(None, description="Filter by ingredient (source or target)"), source_stage: Optional[ProductionStage] = Query(None, description="Filter by source production stage"), target_stage: Optional[ProductionStage] = Query(None, description="Filter by target production stage"), days_back: Optional[int] = Query(None, ge=1, le=365, description="Filter by days back from today"), db: AsyncSession = Depends(get_db) ): """Get product transformations with filtering""" try: service = TransformationService() transformations = await service.get_transformations( tenant_id, skip, limit, ingredient_id, source_stage, target_stage, days_back ) return transformations except Exception as e: logger.error("Failed to get transformations", error=str(e), tenant_id=tenant_id) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get transformations" ) @router.get( route_builder.build_resource_detail_route("transformations", "transformation_id"), response_model=ProductTransformationResponse ) async def get_transformation( transformation_id: UUID = Path(..., description="Transformation ID"), tenant_id: UUID = Path(..., description="Tenant ID"), db: AsyncSession = Depends(get_db) ): """Get specific transformation by ID""" try: service = TransformationService() transformation = await service.get_transformation(transformation_id, tenant_id) if not transformation: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Transformation not found" ) return transformation except HTTPException: raise except Exception as e: logger.error("Failed to get transformation", error=str(e), transformation_id=transformation_id) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get transformation" ) @router.get( route_builder.build_base_route("transformations/summary"), response_model=dict ) async def get_transformation_summary( tenant_id: UUID = Path(..., description="Tenant ID"), days_back: int = Query(30, ge=1, le=365, description="Days back for summary"), db: AsyncSession = Depends(get_db) ): """Get transformation summary for dashboard""" try: service = TransformationService() summary = await service.get_transformation_summary(tenant_id, days_back) return summary except Exception as e: logger.error("Failed to get transformation summary", error=str(e), tenant_id=tenant_id) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get transformation summary" ) @router.post( route_builder.build_operations_route("transformations/par-bake-to-fresh"), response_model=dict ) @require_user_role(['admin', 'owner', 'member']) async def create_par_bake_transformation( source_ingredient_id: UUID = Query(..., description="Par-baked ingredient ID"), target_ingredient_id: UUID = Query(..., description="Fresh baked ingredient ID"), quantity: float = Query(..., gt=0, description="Quantity to transform"), target_batch_number: Optional[str] = Query(None, description="Target batch number"), expiration_hours: int = Query(24, ge=1, le=72, description="Hours until expiration after baking"), notes: Optional[str] = Query(None, description="Process notes"), tenant_id: UUID = Path(..., description="Tenant ID"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Convenience endpoint for par-baked to fresh transformation""" try: # Extract user ID - handle service tokens user_id = get_current_user_id(current_user) # Create transformation data for par-baked to fully baked transformation_data = ProductTransformationCreate( source_ingredient_id=str(source_ingredient_id), target_ingredient_id=str(target_ingredient_id), source_stage=ProductionStage.PAR_BAKED, target_stage=ProductionStage.FULLY_BAKED, source_quantity=quantity, target_quantity=quantity, # Assume 1:1 ratio for par-baked goods expiration_calculation_method="days_from_transformation", expiration_days_offset=max(1, expiration_hours // 24), # Convert hours to days, minimum 1 day process_notes=notes, target_batch_number=target_batch_number ) service = TransformationService() transformation = await service.create_transformation(transformation_data, tenant_id, user_id) return { "transformation_id": transformation.id, "transformation_reference": transformation.transformation_reference, "source_quantity": transformation.source_quantity, "target_quantity": transformation.target_quantity, "expiration_date": transformation.transformation_date, "message": f"Successfully transformed {quantity} units from par-baked to fresh baked" } except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: logger.error("Failed to create par-bake transformation", error=str(e), tenant_id=tenant_id) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create par-bake transformation" )