"""Workflow routes — CRUD, instance lifecycle, advance/cancel.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.auth import check_permission from app.core.db import get_db from app.deps import get_current_user from app.schemas.workflow import WorkflowCreate, WorkflowUpdate, InstanceCreate, AdvanceRequest from app.services import workflow_service router = APIRouter(prefix="/api/v1/workflows", tags=["workflows"]) # ─── Workflow CRUD ─── @router.get("") async def list_workflows( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), is_active: bool | None = Query(None), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List workflows with pagination.""" tenant_id = uuid.UUID(current_user["tenant_id"]) return await workflow_service.list_workflows( db, tenant_id, page=page, page_size=page_size, is_active=is_active, ) @router.post("", status_code=status.HTTP_201_CREATED) async def create_workflow( body: WorkflowCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Create a new workflow definition. Requires write permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "workflows", "create"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Insufficient permissions", "code": "forbidden"}, ) data = body.model_dump() return await workflow_service.create_workflow(db, tenant_id, user_id, data) @router.get("/instances") async def list_instances( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), status: str | None = Query(None), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List workflow instances with optional status filter.""" tenant_id = uuid.UUID(current_user["tenant_id"]) return await workflow_service.list_instances( db, tenant_id, page=page, page_size=page_size, status_filter=status, ) @router.get("/{workflow_id}") async def get_workflow( workflow_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Get a single workflow by ID.""" tenant_id = uuid.UUID(current_user["tenant_id"]) result = await workflow_service.get_workflow(db, tenant_id, workflow_id) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": "Workflow not found", "code": "not_found"}, ) return result @router.patch("/{workflow_id}") async def update_workflow( workflow_id: str, body: WorkflowUpdate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Update a workflow definition.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "workflows", "update"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Insufficient permissions", "code": "forbidden"}, ) data = body.model_dump(exclude_unset=True) result = await workflow_service.update_workflow(db, tenant_id, user_id, workflow_id, data) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": "Workflow not found", "code": "not_found"}, ) return result @router.delete("/{workflow_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_workflow( workflow_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Delete a workflow definition.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "workflows", "delete"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Insufficient permissions", "code": "forbidden"}, ) deleted = await workflow_service.delete_workflow(db, tenant_id, user_id, workflow_id) if not deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": "Workflow not found", "code": "not_found"}, ) return Response(status_code=status.HTTP_204_NO_CONTENT) # ─── Instance endpoints ─── @router.post("/{workflow_id}/instances", status_code=status.HTTP_201_CREATED) async def create_instance( workflow_id: str, body: InstanceCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Create a new workflow instance.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) result = await workflow_service.create_instance( db, tenant_id, user_id, workflow_id=workflow_id, context=body.context, timeout_hours=body.timeout_hours, ) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": "Workflow not found", "code": "not_found"}, ) return result @router.get("/instances/{instance_id}") async def get_instance( instance_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Get a workflow instance with step history.""" tenant_id = uuid.UUID(current_user["tenant_id"]) result = await workflow_service.get_instance(db, tenant_id, instance_id) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": "Instance not found", "code": "not_found"}, ) return result @router.post("/instances/{instance_id}/advance") async def advance_instance( instance_id: str, body: AdvanceRequest, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Advance or reject a workflow instance step. Body decision: "approve" or "reject". Returns 200 with updated instance. """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) result = await workflow_service.advance_instance( db, tenant_id, user_id, instance_id=instance_id, decision=body.decision, comment=body.comment, ) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": "Instance not found", "code": "not_found"}, ) if "error" in result: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"detail": result["error"], "code": "invalid_state"}, ) return result @router.post("/instances/{instance_id}/cancel") async def cancel_instance( instance_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Cancel a workflow instance. Returns 200 with cancelled instance.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) result = await workflow_service.cancel_instance( db, tenant_id, user_id, instance_id=instance_id, ) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"detail": "Instance not found", "code": "not_found"}, ) if "error" in result: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"detail": result["error"], "code": "invalid_state"}, ) return result