diff --git a/app/routes/workflows.py b/app/routes/workflows.py index 51d0e63..cfabf52 100644 --- a/app/routes/workflows.py +++ b/app/routes/workflows.py @@ -464,7 +464,14 @@ async def approve_workflow_step( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("workflows:write")), ): - """Approve the current approval step of a workflow instance.""" + """Approve the current approval step of a workflow instance. + + F12 (Astra P1): resolves the EXISTING pending approval request that + the workflow engine created when the decision guard paused the flow — + instead of creating a NEW request and self-approving it (old behaviour: + TypeError on approval["id"] — create returns an ORM object, and the + call used a nonexistent ``decided_by`` kwarg). + """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) @@ -476,7 +483,11 @@ async def approve_workflow_step( from sqlalchemy import select - from app.core.approval import create_approval_request, resolve_approval_request + from app.core.approval import ( + ApprovalDecisionError, + ApprovalRequest, + resolve_approval_request, + ) from app.models.workflow import WorkflowInstance result = await db.execute( @@ -493,22 +504,44 @@ async def approve_workflow_step( detail={"detail": "Instance not found", "code": "not_found"}, ) - approval = await create_approval_request( - db=db, - tenant_id=tenant_id, - entity_type="workflow_instance", - entity_id=uuid.UUID(instance_id), - action="workflow_step_approval", - requested_by=user_id, - requested_by_type="user", - ) - await resolve_approval_request( - db=db, - request_id=approval["id"], - decision="approved", - decided_by=user_id, - comment=comment, + # F12: find the EXISTING pending approval the engine created for this + # instance — do not create a new, instantly self-approved one. + pending_result = await db.execute( + select(ApprovalRequest) + .where( + ApprovalRequest.tenant_id == tenant_id, + ApprovalRequest.entity_type == "workflow_instance", + ApprovalRequest.entity_id == instance.id, + ApprovalRequest.status == "pending", + ) + .order_by(ApprovalRequest.created_at.desc()) + .limit(1) ) + pending = pending_result.scalar_one_or_none() + if pending is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "detail": "No pending approval request for this workflow instance", + "code": "no_pending_approval", + }, + ) + + try: + await resolve_approval_request( + db, + tenant_id, + pending.id, + decision="approved", + approver_id=user_id, + comment=comment, + is_system_admin=bool(current_user.get("is_system_admin", False)), + ) + except ApprovalDecisionError as exc: + raise HTTPException( + status_code=exc.http_status, + detail={"detail": str(exc), "code": exc.code}, + ) from exc return await workflow_service.advance_instance( db, @@ -528,7 +561,11 @@ async def reject_workflow_step( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("workflows:write")), ): - """Reject the current approval step of a workflow instance.""" + """Reject the current approval step of a workflow instance. + + F12 (Astra P1): resolves the EXISTING pending approval request that + the workflow engine created — same contract fix as approve. + """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) @@ -540,7 +577,11 @@ async def reject_workflow_step( from sqlalchemy import select - from app.core.approval import create_approval_request, resolve_approval_request + from app.core.approval import ( + ApprovalDecisionError, + ApprovalRequest, + resolve_approval_request, + ) from app.models.workflow import WorkflowInstance result = await db.execute( @@ -557,22 +598,43 @@ async def reject_workflow_step( detail={"detail": "Instance not found", "code": "not_found"}, ) - approval = await create_approval_request( - db=db, - tenant_id=tenant_id, - entity_type="workflow_instance", - entity_id=uuid.UUID(instance_id), - action="workflow_step_approval", - requested_by=user_id, - requested_by_type="user", - ) - await resolve_approval_request( - db=db, - request_id=approval["id"], - decision="rejected", - decided_by=user_id, - comment=comment, + # F12: resolve the EXISTING pending approval for this instance. + pending_result = await db.execute( + select(ApprovalRequest) + .where( + ApprovalRequest.tenant_id == tenant_id, + ApprovalRequest.entity_type == "workflow_instance", + ApprovalRequest.entity_id == instance.id, + ApprovalRequest.status == "pending", + ) + .order_by(ApprovalRequest.created_at.desc()) + .limit(1) ) + pending = pending_result.scalar_one_or_none() + if pending is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "detail": "No pending approval request for this workflow instance", + "code": "no_pending_approval", + }, + ) + + try: + await resolve_approval_request( + db, + tenant_id, + pending.id, + decision="rejected", + approver_id=user_id, + comment=comment, + is_system_admin=bool(current_user.get("is_system_admin", False)), + ) + except ApprovalDecisionError as exc: + raise HTTPException( + status_code=exc.http_status, + detail={"detail": str(exc), "code": exc.code}, + ) from exc return await workflow_service.cancel_instance( db,