298 lines
12 KiB
Python
298 lines
12 KiB
Python
# services/production/app/api/production_batches.py
|
|
"""
|
|
Production Batches API - ATOMIC CRUD operations on ProductionBatch model
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
|
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
|
|
from shared.routing import RouteBuilder
|
|
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
|
from app.core.database import get_db
|
|
from app.services.production_service import ProductionService
|
|
from app.models import AuditLog
|
|
from app.schemas.production import (
|
|
ProductionBatchCreate,
|
|
ProductionBatchUpdate,
|
|
ProductionBatchStatusUpdate,
|
|
ProductionBatchResponse,
|
|
ProductionBatchListResponse,
|
|
ProductionStatusEnum
|
|
)
|
|
from app.core.config import settings
|
|
from app.utils.cache import get_cached, set_cached, make_cache_key
|
|
|
|
logger = structlog.get_logger()
|
|
route_builder = RouteBuilder('production')
|
|
router = APIRouter(tags=["production-batches"])
|
|
|
|
# Initialize audit logger with the production service's AuditLog model
|
|
audit_logger = create_audit_logger("production-service", AuditLog)
|
|
|
|
|
|
def get_production_service() -> ProductionService:
|
|
"""Dependency injection for production service"""
|
|
from app.core.database import database_manager
|
|
return ProductionService(database_manager, settings)
|
|
|
|
|
|
@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)
|
|
):
|
|
"""List batches with filters: date, status, product, order_id (with Redis caching - 20s TTL)"""
|
|
try:
|
|
# 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)
|
|
|
|
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)
|
|
|
|
# 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)
|
|
|
|
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),
|
|
production_service: ProductionService = Depends(get_production_service)
|
|
):
|
|
"""Create a new production batch"""
|
|
try:
|
|
batch = await production_service.create_production_batch(tenant_id, batch_data)
|
|
|
|
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)
|
|
|
|
batch = await batch_repo.get(batch_id)
|
|
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'])
|
|
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)"""
|
|
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))
|
|
|
|
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")
|