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

397 lines
14 KiB
Python

# services/production/app/api/production_operations.py
"""
Production Operations API - Business operations for production management
Includes: batch start/complete, schedule finalize/optimize, capacity management, transformations, stats
"""
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from typing import Optional
from datetime import date, datetime, timedelta
from uuid import UUID
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from app.services.production_service import ProductionService
from app.schemas.production import (
ProductionBatchResponse,
ProductionScheduleResponse
)
from app.core.config import settings
logger = structlog.get_logger()
route_builder = RouteBuilder('production')
router = APIRouter(tags=["production-operations"])
def get_production_service() -> ProductionService:
"""Dependency injection for production service"""
from app.core.database import database_manager
return ProductionService(database_manager, settings)
# ===== BATCH OPERATIONS =====
@router.post(
route_builder.build_nested_resource_route("batches", "batch_id", "start"),
response_model=ProductionBatchResponse
)
async def start_production_batch(
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Mark batch as started (updates actual_start_time)"""
try:
batch = await production_service.start_production_batch(tenant_id, batch_id)
logger.info("Started production batch",
batch_id=str(batch_id), tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except ValueError as e:
logger.warning("Cannot start batch", error=str(e), batch_id=str(batch_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error starting production batch",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to start production batch")
@router.post(
route_builder.build_nested_resource_route("batches", "batch_id", "complete"),
response_model=ProductionBatchResponse
)
async def complete_production_batch(
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
completion_data: Optional[dict] = None,
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Complete batch — auto-calculates yield, duration, cost summary"""
try:
batch = await production_service.complete_production_batch(tenant_id, batch_id, completion_data)
logger.info("Completed production batch",
batch_id=str(batch_id), tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except ValueError as e:
logger.warning("Cannot complete batch", error=str(e), batch_id=str(batch_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error completing production batch",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to complete production batch")
@router.get(
route_builder.build_operations_route("batches/stats"),
response_model=dict
)
async def get_production_batch_stats(
tenant_id: UUID = Path(...),
start_date: Optional[date] = Query(None, description="Start date for stats"),
end_date: Optional[date] = Query(None, description="End date for stats"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Aggregated stats: completed vs failed, avg yield, on-time rate"""
try:
# Default to last 30 days if no dates provided
if not start_date:
start_date = (datetime.now() - timedelta(days=30)).date()
if not end_date:
end_date = datetime.now().date()
stats = await production_service.get_batch_statistics(tenant_id, start_date, end_date)
logger.info("Retrieved production batch statistics",
tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat())
return stats
except Exception as e:
logger.error("Error getting production batch stats",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get production batch stats")
# ===== SCHEDULE OPERATIONS =====
@router.post(
route_builder.build_nested_resource_route("schedules", "schedule_id", "finalize"),
response_model=ProductionScheduleResponse
)
async def finalize_production_schedule(
tenant_id: UUID = Path(...),
schedule_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Lock schedule; prevents further changes"""
try:
schedule = await production_service.finalize_production_schedule(tenant_id, schedule_id)
logger.info("Finalized production schedule",
schedule_id=str(schedule_id), tenant_id=str(tenant_id))
return ProductionScheduleResponse.model_validate(schedule)
except ValueError as e:
logger.warning("Cannot finalize schedule", error=str(e), schedule_id=str(schedule_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error finalizing production schedule",
error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to finalize production schedule")
@router.get(
route_builder.build_operations_route("schedules/optimize"),
response_model=dict
)
async def optimize_production_schedule(
tenant_id: UUID = Path(...),
target_date: date = Query(..., description="Date to optimize"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Trigger AI-based rescheduling suggestion based on demand/capacity"""
try:
optimization_result = await production_service.optimize_schedule(tenant_id, target_date)
logger.info("Generated schedule optimization suggestions",
tenant_id=str(tenant_id), date=target_date.isoformat())
return optimization_result
except Exception as e:
logger.error("Error optimizing production schedule",
error=str(e), tenant_id=str(tenant_id), date=target_date.isoformat())
raise HTTPException(status_code=500, detail="Failed to optimize production schedule")
@router.get(
route_builder.build_operations_route("schedules/capacity-usage"),
response_model=dict
)
async def get_schedule_capacity_usage(
tenant_id: UUID = Path(...),
start_date: Optional[date] = Query(None),
end_date: Optional[date] = Query(None),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get capacity usage report for scheduling period"""
try:
if not start_date:
start_date = datetime.now().date()
if not end_date:
end_date = start_date + timedelta(days=7)
usage_report = await production_service.get_capacity_usage_report(tenant_id, start_date, end_date)
logger.info("Retrieved capacity usage report",
tenant_id=str(tenant_id),
start_date=start_date.isoformat(),
end_date=end_date.isoformat())
return usage_report
except Exception as e:
logger.error("Error getting capacity usage",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get capacity usage")
# ===== CAPACITY MANAGEMENT =====
@router.get(
route_builder.build_operations_route("capacity/status"),
response_model=dict
)
async def get_capacity_status(
tenant_id: UUID = Path(...),
target_date: Optional[date] = Query(None),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get real-time capacity status"""
try:
if not target_date:
target_date = datetime.now().date()
status = await production_service.get_capacity_status(tenant_id, target_date)
logger.info("Retrieved capacity status",
tenant_id=str(tenant_id), date=target_date.isoformat())
return status
except Exception as e:
logger.error("Error getting capacity status",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get capacity status")
@router.get(
route_builder.build_operations_route("capacity/availability"),
response_model=dict
)
async def check_resource_availability(
tenant_id: UUID = Path(...),
target_date: date = Query(...),
required_capacity: float = Query(..., gt=0),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Check if capacity is available for scheduling"""
try:
availability = await production_service.check_resource_availability(
tenant_id, target_date, required_capacity
)
logger.info("Checked resource availability",
tenant_id=str(tenant_id),
date=target_date.isoformat(),
required=required_capacity)
return availability
except Exception as e:
logger.error("Error checking resource availability",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to check resource availability")
@router.post(
route_builder.build_operations_route("capacity/reserve"),
response_model=dict
)
async def reserve_capacity(
tenant_id: UUID = Path(...),
target_date: date = Query(...),
capacity_amount: float = Query(..., gt=0),
batch_id: UUID = Query(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Reserve capacity for a batch"""
try:
reservation = await production_service.reserve_capacity(
tenant_id, target_date, capacity_amount, batch_id
)
logger.info("Reserved production capacity",
tenant_id=str(tenant_id),
date=target_date.isoformat(),
amount=capacity_amount,
batch_id=str(batch_id))
return reservation
except ValueError as e:
logger.warning("Cannot reserve capacity", error=str(e))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error reserving capacity",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to reserve capacity")
@router.get(
route_builder.build_operations_route("capacity/bottlenecks"),
response_model=dict
)
async def get_capacity_bottlenecks(
tenant_id: UUID = Path(...),
days_ahead: int = Query(7, ge=1, le=30),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Identify capacity bottlenecks in upcoming period"""
try:
bottlenecks = await production_service.predict_capacity_bottlenecks(tenant_id, days_ahead)
logger.info("Retrieved capacity bottlenecks prediction",
tenant_id=str(tenant_id), days_ahead=days_ahead)
return bottlenecks
except Exception as e:
logger.error("Error getting capacity bottlenecks",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get capacity bottlenecks")
# ===== TRANSFORMATION OPERATIONS =====
@router.post(
route_builder.build_operations_route("batches/complete-with-transformation"),
response_model=dict
)
async def complete_batch_with_transformation(
tenant_id: UUID = Path(...),
batch_id: UUID = Query(...),
transformation_data: dict = None,
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Complete batch and create product transformation record"""
try:
result = await production_service.complete_batch_with_transformation(
tenant_id, batch_id, transformation_data
)
logger.info("Completed batch with transformation",
tenant_id=str(tenant_id),
batch_id=str(batch_id))
return result
except ValueError as e:
logger.warning("Cannot complete batch with transformation", error=str(e))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error completing batch with transformation",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to complete batch with transformation")
@router.post(
route_builder.build_operations_route("transform-par-baked"),
response_model=dict
)
async def transform_par_baked_products(
tenant_id: UUID = Path(...),
source_batch_id: UUID = Query(...),
target_quantity: float = Query(..., gt=0),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Transform par-baked products to fully baked"""
try:
result = await production_service.transform_par_baked_to_fresh(
tenant_id, source_batch_id, target_quantity
)
logger.info("Transformed par-baked products",
tenant_id=str(tenant_id),
source_batch_id=str(source_batch_id),
quantity=target_quantity)
return result
except ValueError as e:
logger.warning("Cannot transform products", error=str(e))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error transforming products",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to transform products")