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