Files
bakery-ia/services/inventory/app/api/transformations.py
2025-10-06 15:27:01 +02:00

222 lines
8.8 KiB
Python

# 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"
)