Files
bakery-ia/services/production/app/api/production_batches.py

358 lines
14 KiB
Python
Raw Normal View History

2025-10-06 15:27:01 +02:00
# services/production/app/api/production_batches.py
"""
Production Batches API - ATOMIC CRUD operations on ProductionBatch model
"""
2025-12-13 23:57:54 +01:00
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
2025-10-06 15:27:01 +02:00
from typing import Optional
from datetime import date
from uuid import UUID
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import require_user_role
2025-10-06 15:27:01 +02:00
from shared.routing import RouteBuilder
from shared.security import create_audit_logger, AuditSeverity, AuditAction
2025-10-06 15:27:01 +02:00
from app.core.database import get_db
from app.services.production_service import ProductionService
2025-10-29 06:58:05 +01:00
from app.models import AuditLog
2025-10-06 15:27:01 +02:00
from app.schemas.production import (
ProductionBatchCreate,
ProductionBatchUpdate,
ProductionBatchStatusUpdate,
ProductionBatchResponse,
ProductionBatchListResponse,
ProductionStatusEnum
)
from app.core.config import settings
2025-12-05 20:07:01 +01:00
from app.utils.cache import get_cached, set_cached, make_cache_key
2025-12-13 23:57:54 +01:00
from app.services.production_alert_service import ProductionAlertService
2025-10-06 15:27:01 +02:00
logger = structlog.get_logger()
2025-12-13 23:57:54 +01:00
async def get_production_alert_service(request: Request) -> ProductionAlertService:
"""Dependency injection for production alert service"""
# Get the alert service from app state, which is where it's stored during app startup
alert_service = getattr(request.app.state, 'production_alert_service', None)
if not alert_service:
logger.warning("Production alert service not available in app state")
return None
return alert_service
2025-10-06 15:27:01 +02:00
route_builder = RouteBuilder('production')
router = APIRouter(tags=["production-batches"])
2025-10-29 06:58:05 +01:00
# Initialize audit logger with the production service's AuditLog model
audit_logger = create_audit_logger("production-service", AuditLog)
2025-10-06 15:27:01 +02:00
2025-12-13 23:57:54 +01:00
def get_production_service(request: Request) -> ProductionService:
2025-10-06 15:27:01 +02:00
"""Dependency injection for production service"""
from app.core.database import database_manager
2025-12-13 23:57:54 +01:00
notification_service = getattr(request.app.state, 'notification_service', None)
return ProductionService(database_manager, settings, notification_service)
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_base_route("batches"),
response_model=ProductionBatchListResponse
)
async def list_production_batches(
tenant_id: UUID = Path(...),
status: Optional[ProductionStatusEnum] = Query(None, description="Filter by status"),
product_id: Optional[UUID] = Query(None, description="Filter by product"),
order_id: Optional[UUID] = Query(None, description="Filter by order"),
start_date: Optional[date] = Query(None, description="Filter from date"),
end_date: Optional[date] = Query(None, description="Filter to date"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=100, description="Page size"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
2025-12-05 20:07:01 +01:00
"""List batches with filters: date, status, product, order_id (with Redis caching - 20s TTL)"""
2025-10-06 15:27:01 +02:00
try:
2025-12-05 20:07:01 +01:00
# PERFORMANCE OPTIMIZATION: Cache frequently accessed queries (status filter, first page)
cache_key = None
if page == 1 and product_id is None and order_id is None and start_date is None and end_date is None:
# Cache simple status-filtered queries (common for dashboards)
cache_key = make_cache_key(
"production_batches",
str(tenant_id),
status=status.value if status else None,
page_size=page_size
)
cached_result = await get_cached(cache_key)
if cached_result is not None:
logger.debug("Cache hit for production batches", cache_key=cache_key, tenant_id=str(tenant_id), status=status)
return ProductionBatchListResponse(**cached_result)
2025-10-06 15:27:01 +02:00
filters = {
"status": status,
"product_id": str(product_id) if product_id else None,
"order_id": str(order_id) if order_id else None,
"start_date": start_date,
"end_date": end_date
}
batch_list = await production_service.get_production_batches_list(tenant_id, filters, page, page_size)
2025-12-05 20:07:01 +01:00
# Cache the result if applicable (20s TTL for production batches)
if cache_key:
await set_cached(cache_key, batch_list.model_dump(), ttl=20)
logger.debug("Cached production batches", cache_key=cache_key, ttl=20, tenant_id=str(tenant_id), status=status)
2025-10-06 15:27:01 +02:00
logger.info("Retrieved production batches list",
tenant_id=str(tenant_id), filters=filters)
return batch_list
except Exception as e:
logger.error("Error listing production batches",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to list production batches")
@router.post(
route_builder.build_base_route("batches"),
response_model=ProductionBatchResponse
)
async def create_production_batch(
batch_data: ProductionBatchCreate,
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
2025-12-13 23:57:54 +01:00
production_service: ProductionService = Depends(get_production_service),
request: Request = None,
alert_service: ProductionAlertService = Depends(get_production_alert_service)
2025-10-06 15:27:01 +02:00
):
"""Create a new production batch"""
try:
batch = await production_service.create_production_batch(tenant_id, batch_data)
2025-12-13 23:57:54 +01:00
# Trigger Start Production alert
if alert_service:
try:
# Generate reasoning data for the batch
reasoning_data = {
"type": "manual_creation",
"parameters": {
"product_name": batch.product_name,
"planned_quantity": batch.planned_quantity,
"priority": batch.priority.value if batch.priority else "MEDIUM"
},
"urgency": {
"level": "normal",
"ready_by_time": batch.planned_start_time.strftime('%H:%M') if batch.planned_start_time else "unknown"
},
"metadata": {
"trigger_source": "manual_creation",
"created_by": current_user.get("user_id", "unknown"),
"is_ai_assisted": False
}
}
# Update batch with reasoning data
from app.core.database import get_db
db = next(get_db())
batch.reasoning_data = reasoning_data
await db.commit()
# Emit Start Production alert
await alert_service.emit_start_production_alert(
tenant_id=tenant_id,
batch_id=batch.id,
product_name=batch.product_name,
batch_number=batch.batch_number,
reasoning_data=reasoning_data,
planned_start_time=batch.planned_start_time.isoformat() if batch.planned_start_time else None
)
logger.info("Start Production alert triggered for batch",
batch_id=str(batch.id), tenant_id=str(tenant_id))
except Exception as alert_error:
logger.error("Failed to trigger Start Production alert",
error=str(alert_error), batch_id=str(batch.id))
# Don't fail the batch creation if alert fails
2025-10-06 15:27:01 +02:00
logger.info("Created production batch",
batch_id=str(batch.id), tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except ValueError as e:
logger.warning("Invalid batch data", error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error creating production batch",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to create production batch")
@router.get(
route_builder.build_base_route("batches/active"),
response_model=ProductionBatchListResponse
)
async def get_active_batches(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Get currently active production batches"""
try:
from app.repositories.production_batch_repository import ProductionBatchRepository
batch_repo = ProductionBatchRepository(db)
batches = await batch_repo.get_active_batches(str(tenant_id))
batch_responses = [ProductionBatchResponse.model_validate(batch) for batch in batches]
logger.info("Retrieved active production batches",
count=len(batches), tenant_id=str(tenant_id))
return ProductionBatchListResponse(
batches=batch_responses,
total_count=len(batches),
page=1,
page_size=len(batches)
)
except Exception as e:
logger.error("Error getting active batches",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get active batches")
@router.get(
route_builder.build_resource_detail_route("batches", "batch_id"),
response_model=ProductionBatchResponse
)
async def get_batch_details(
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Get detailed information about a production batch"""
try:
from app.repositories.production_batch_repository import ProductionBatchRepository
batch_repo = ProductionBatchRepository(db)
2025-12-09 10:21:41 +01:00
batch = await batch_repo.get_by_id(batch_id)
2025-10-06 15:27:01 +02:00
if not batch or str(batch.tenant_id) != str(tenant_id):
raise HTTPException(status_code=404, detail="Production batch not found")
logger.info("Retrieved production batch details",
batch_id=str(batch_id), tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except HTTPException:
raise
except Exception as e:
logger.error("Error getting batch details",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get batch details")
@router.put(
route_builder.build_nested_resource_route("batches", "batch_id", "status"),
response_model=ProductionBatchResponse
)
async def update_batch_status(
status_update: ProductionBatchStatusUpdate,
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Update production batch status"""
try:
batch = await production_service.update_batch_status(tenant_id, batch_id, status_update)
logger.info("Updated production batch status",
batch_id=str(batch_id),
new_status=status_update.status.value,
tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except ValueError as e:
logger.warning("Invalid status update", error=str(e), batch_id=str(batch_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating batch status",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to update batch status")
@router.put(
route_builder.build_resource_detail_route("batches", "batch_id"),
response_model=ProductionBatchResponse
)
async def update_production_batch(
batch_update: ProductionBatchUpdate,
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Update batch (e.g., start time, notes, status)"""
try:
batch = await production_service.update_production_batch(tenant_id, batch_id, batch_update)
logger.info("Updated production batch",
batch_id=str(batch_id), tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except ValueError as e:
logger.warning("Invalid batch update", error=str(e), batch_id=str(batch_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating production batch",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to update production batch")
@router.delete(
route_builder.build_resource_detail_route("batches", "batch_id")
)
@require_user_role(['admin', 'owner'])
2025-10-06 15:27:01 +02:00
async def delete_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)
):
"""Cancel/delete draft batch (Admin+ only, soft delete preferred)"""
2025-10-06 15:27:01 +02:00
try:
await production_service.delete_production_batch(tenant_id, batch_id)
# Log audit event for batch deletion
try:
db = next(get_db())
await audit_logger.log_deletion(
db_session=db,
tenant_id=str(tenant_id),
user_id=current_user["user_id"],
resource_type="production_batch",
resource_id=str(batch_id),
description=f"Deleted production batch",
endpoint=f"/batches/{batch_id}",
method="DELETE"
)
except Exception as audit_error:
logger.warning("Failed to log audit event", error=str(audit_error))
2025-10-06 15:27:01 +02:00
logger.info("Deleted production batch",
batch_id=str(batch_id), tenant_id=str(tenant_id))
return {"message": "Production batch deleted successfully"}
except ValueError as e:
logger.warning("Cannot delete batch", error=str(e), batch_id=str(batch_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error deleting production batch",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to delete production batch")