# ================================================================ # services/orchestrator/app/api/orchestration.py # ================================================================ """ Orchestration API Endpoints Testing and manual trigger endpoints for orchestration """ import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel, Field import structlog from app.core.database import get_db from app.repositories.orchestration_run_repository import OrchestrationRunRepository from sqlalchemy.ext.asyncio import AsyncSession logger = structlog.get_logger() router = APIRouter(prefix="/api/v1/tenants/{tenant_id}/orchestrator", tags=["Orchestration"]) # ================================================================ # REQUEST/RESPONSE SCHEMAS # ================================================================ class OrchestratorTestRequest(BaseModel): """Request schema for testing orchestrator""" test_scenario: Optional[str] = Field(None, description="Test scenario: full, production_only, procurement_only") dry_run: bool = Field(False, description="Dry run mode (no actual changes)") class OrchestratorTestResponse(BaseModel): """Response schema for orchestrator test""" success: bool message: str tenant_id: str forecasting_completed: bool = False production_completed: bool = False procurement_completed: bool = False notifications_sent: bool = False summary: dict = {} # ================================================================ # API ENDPOINTS # ================================================================ @router.post("/test", response_model=OrchestratorTestResponse) async def trigger_orchestrator_test( tenant_id: str, request_data: OrchestratorTestRequest, request: Request, db: AsyncSession = Depends(get_db) ): """ Trigger orchestrator for testing purposes This endpoint allows manual triggering of the orchestration workflow for a specific tenant, useful for testing during development. Args: tenant_id: Tenant ID to orchestrate request_data: Test request with scenario and dry_run options request: FastAPI request object db: Database session Returns: OrchestratorTestResponse with results """ logger.info("Orchestrator test trigger requested", tenant_id=tenant_id, test_scenario=request_data.test_scenario, dry_run=request_data.dry_run) try: # Get scheduler service from app state if not hasattr(request.app.state, 'scheduler_service'): raise HTTPException( status_code=503, detail="Orchestrator scheduler service not available" ) scheduler_service = request.app.state.scheduler_service # Trigger orchestration tenant_uuid = uuid.UUID(tenant_id) result = await scheduler_service.trigger_orchestration_for_tenant( tenant_id=tenant_uuid, test_scenario=request_data.test_scenario ) # Get the latest run for this tenant repo = OrchestrationRunRepository(db) latest_run = await repo.get_latest_run_for_tenant(tenant_uuid) # Build response response = OrchestratorTestResponse( success=result.get('success', False), message=result.get('message', 'Orchestration completed'), tenant_id=tenant_id, forecasting_completed=latest_run.forecasting_status == 'success' if latest_run else False, production_completed=latest_run.production_status == 'success' if latest_run else False, procurement_completed=latest_run.procurement_status == 'success' if latest_run else False, notifications_sent=latest_run.notification_status == 'success' if latest_run else False, summary={ 'forecasts_generated': latest_run.forecasts_generated if latest_run else 0, 'batches_created': latest_run.production_batches_created if latest_run else 0, 'pos_created': latest_run.purchase_orders_created if latest_run else 0, 'notifications_sent': latest_run.notifications_sent if latest_run else 0 } ) logger.info("Orchestrator test completed", tenant_id=tenant_id, success=response.success) return response except ValueError as e: raise HTTPException(status_code=400, detail=f"Invalid tenant ID: {str(e)}") except Exception as e: logger.error("Orchestrator test failed", tenant_id=tenant_id, error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Orchestrator test failed: {str(e)}") @router.get("/health") async def orchestrator_health(): """Check orchestrator health""" return { "status": "healthy", "service": "orchestrator", "message": "Orchestrator service is running" } @router.get("/runs", response_model=dict) async def list_orchestration_runs( tenant_id: str, limit: int = 10, offset: int = 0, db: AsyncSession = Depends(get_db) ): """ List orchestration runs for a tenant Args: tenant_id: Tenant ID limit: Maximum number of runs to return offset: Number of runs to skip db: Database session Returns: List of orchestration runs """ try: tenant_uuid = uuid.UUID(tenant_id) repo = OrchestrationRunRepository(db) runs = await repo.list_runs( tenant_id=tenant_uuid, limit=limit, offset=offset ) return { "runs": [ { "id": str(run.id), "run_number": run.run_number, "status": run.status.value, "started_at": run.started_at.isoformat() if run.started_at else None, "completed_at": run.completed_at.isoformat() if run.completed_at else None, "duration_seconds": run.duration_seconds, "forecasts_generated": run.forecasts_generated, "batches_created": run.production_batches_created, "pos_created": run.purchase_orders_created } for run in runs ], "total": len(runs), "limit": limit, "offset": offset } except ValueError as e: raise HTTPException(status_code=400, detail=f"Invalid tenant ID: {str(e)}") except Exception as e: logger.error("Error listing orchestration runs", tenant_id=tenant_id, error=str(e)) raise HTTPException(status_code=500, detail=str(e))