# ================================================================ # services/production/app/api/production.py # ================================================================ """ Production API endpoints """ from fastapi import APIRouter, Depends, HTTPException, Path, Query from typing import Optional, List from datetime import date, datetime from uuid import UUID import structlog from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep from app.core.database import get_db from app.services.production_service import ProductionService from app.schemas.production import ( ProductionBatchCreate, ProductionBatchUpdate, ProductionBatchStatusUpdate, ProductionBatchResponse, ProductionBatchListResponse, DailyProductionRequirements, ProductionDashboardSummary, ProductionMetrics, ) from app.core.config import settings logger = structlog.get_logger() router = APIRouter(tags=["production"]) def get_production_service() -> ProductionService: """Dependency injection for production service""" from app.core.database import database_manager return ProductionService(database_manager, settings) # ================================================================ # DASHBOARD ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/dashboard-summary", response_model=ProductionDashboardSummary) async def get_dashboard_summary( tenant_id: UUID = Path(...), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Get production dashboard summary using shared auth""" try: # Verify tenant access using shared auth pattern if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") summary = await production_service.get_dashboard_summary(tenant_id) logger.info("Retrieved production dashboard summary", tenant_id=str(tenant_id), user_id=current_user.get("user_id")) return summary except Exception as e: logger.error("Error getting production dashboard summary", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get dashboard summary") @router.get("/tenants/{tenant_id}/production/daily-requirements", response_model=DailyProductionRequirements) async def get_daily_requirements( tenant_id: UUID = Path(...), date: Optional[date] = Query(None, description="Target date for production requirements"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Get daily production requirements""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") target_date = date or datetime.now().date() requirements = await production_service.calculate_daily_requirements(tenant_id, target_date) logger.info("Retrieved daily production requirements", tenant_id=str(tenant_id), date=target_date.isoformat()) return requirements except Exception as e: logger.error("Error getting daily production requirements", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get daily requirements") @router.get("/tenants/{tenant_id}/production/requirements", response_model=dict) async def get_production_requirements( tenant_id: UUID = Path(...), date: Optional[date] = Query(None, description="Target date for production requirements"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Get production requirements for procurement planning""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") target_date = date or datetime.now().date() requirements = await production_service.get_production_requirements(tenant_id, target_date) logger.info("Retrieved production requirements for procurement", tenant_id=str(tenant_id), date=target_date.isoformat()) return requirements except Exception as e: logger.error("Error getting production requirements", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get production requirements") # ================================================================ # PRODUCTION BATCH ENDPOINTS # ================================================================ @router.post("/tenants/{tenant_id}/production/batches", response_model=ProductionBatchResponse) async def create_production_batch( batch_data: ProductionBatchCreate, tenant_id: UUID = Path(...), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Create a new production batch""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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("/tenants/{tenant_id}/production/batches/active", response_model=ProductionBatchListResponse) async def get_active_batches( tenant_id: UUID = Path(...), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get currently active production batches""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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("/tenants/{tenant_id}/production/batches/{batch_id}", response_model=ProductionBatchResponse) async def get_batch_details( tenant_id: UUID = Path(...), batch_id: UUID = Path(...), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get detailed information about a production batch""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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("/tenants/{tenant_id}/production/batches/{batch_id}/status", response_model=ProductionBatchResponse) async def update_batch_status( status_update: ProductionBatchStatusUpdate, tenant_id: UUID = Path(...), batch_id: UUID = Path(...), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), production_service: ProductionService = Depends(get_production_service) ): """Update production batch status""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") 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") # ================================================================ # PRODUCTION SCHEDULE ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/schedule", response_model=dict) async def get_production_schedule( tenant_id: UUID = Path(...), start_date: Optional[date] = Query(None, description="Start date for schedule"), end_date: Optional[date] = Query(None, description="End date for schedule"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get production schedule for a date range""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") # Default to next 7 days if no dates provided if not start_date: start_date = datetime.now().date() if not end_date: end_date = start_date + timedelta(days=7) from app.repositories.production_schedule_repository import ProductionScheduleRepository schedule_repo = ProductionScheduleRepository(db) schedules = await schedule_repo.get_schedules_by_date_range( str(tenant_id), start_date, end_date ) schedule_data = { "start_date": start_date.isoformat(), "end_date": end_date.isoformat(), "schedules": [ { "id": str(schedule.id), "date": schedule.schedule_date.isoformat(), "shift_start": schedule.shift_start.isoformat(), "shift_end": schedule.shift_end.isoformat(), "capacity_utilization": schedule.utilization_percentage, "batches_planned": schedule.total_batches_planned, "is_finalized": schedule.is_finalized } for schedule in schedules ], "total_schedules": len(schedules) } logger.info("Retrieved production schedule", tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat(), schedules_count=len(schedules)) return schedule_data except Exception as e: logger.error("Error getting production schedule", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get production schedule") # ================================================================ # CAPACITY MANAGEMENT ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/capacity/status", response_model=dict) async def get_capacity_status( tenant_id: UUID = Path(...), date: Optional[date] = Query(None, description="Date for capacity status"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get production capacity status for a specific date""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") target_date = date or datetime.now().date() from app.repositories.production_capacity_repository import ProductionCapacityRepository capacity_repo = ProductionCapacityRepository(db) capacity_summary = await capacity_repo.get_capacity_utilization_summary( str(tenant_id), target_date, target_date ) logger.info("Retrieved capacity status", tenant_id=str(tenant_id), date=target_date.isoformat()) return capacity_summary 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") # ================================================================ # METRICS AND ANALYTICS ENDPOINTS # ================================================================ @router.get("/tenants/{tenant_id}/production/metrics/yield", response_model=dict) async def get_yield_metrics( tenant_id: UUID = Path(...), start_date: date = Query(..., description="Start date for metrics"), end_date: date = Query(..., description="End date for metrics"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db=Depends(get_db) ): """Get production yield metrics for analysis""" try: if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") from app.repositories.production_batch_repository import ProductionBatchRepository batch_repo = ProductionBatchRepository(db) metrics = await batch_repo.get_production_metrics(str(tenant_id), start_date, end_date) logger.info("Retrieved yield metrics", tenant_id=str(tenant_id), start_date=start_date.isoformat(), end_date=end_date.isoformat()) return metrics except Exception as e: logger.error("Error getting yield metrics", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to get yield metrics")