fix(workflows): F12 (Astra P1) — approve/reject an zentralen Approval-Vertrag anpassen
Vorher: Beide Routen behandelten die Rueckgabe von create_approval_request als Dictionary (approval["id"] -> TypeError: ApprovalRequest object is not subscriptable, Astra-Repro), riefen resolve_approval_request mit nicht existierendem decided_by statt approver_id und ohne tenant_id auf — und erzeugten bei JEDEM Aufruf eine NEUE Anfrage, die sie sofort selbst genehmigten, statt die wartende Engine-Anfrage aufzuloesen. Fix (beide Routen, approve + reject): - Suchen die BESTEHENDE pending ApprovalRequest der Engine (entity_type=workflow_instance, entity_id, status=pending, neueste zuerst) und loesen genau diese auf — keine Selbst-Genehmigung mehr - Korrekte F11-Signatur: (db, tenant_id, request_id, decision=, approver_id=, comment=, is_system_admin=) + ApprovalDecisionError- Behandlung (403/409/410) Keine wartende Anfrage -> 409 no_pending_approval (kla rer Zustand statt stiller Neubau) - advance_instance/cancel_instance laufen wie gehabt NACH erfolgreicher Aufloesung Abnahme (Astra): Beide URLs funktionieren; Zustandswechsel, Audit und Freigabe stimmen; Wiederholung erzeugt keinen zweiten Fortschritt — erfuellt (resolve wirft 409 not_pending bei Zweitentscheid). Tests: test_phase_g_workflows + test_s1_security_guards 60/60. ruff clean.
This commit is contained in:
+96
-34
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user