"""Minimale Operating-Transitions — AP1.0 mit Hook Dispatch.""" from __future__ import annotations from typing import Any, Optional from psycopg2.extras import RealDictCursor from db import get_connection from steering.hooks.dispatch import dispatch_hook RESOLVED_BLOCKER_STATUSES = frozenset({"resolved", "accepted_risk", "dismissed"}) def after_blocker_status_change( *, tenant_id: str, blocker_id: str, action_id: Optional[str], initiative_id: Optional[str] = None, old_status: str, new_status: str, user_id: Optional[str] = None, ) -> list[dict[str, Any]]: """Blocker geschlossen → verknüpfte blockierte Maßnahme entblocken.""" effects: list[dict[str, Any]] = [] if new_status == old_status: return effects if new_status not in RESOLVED_BLOCKER_STATUSES or not action_id: return effects conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT id, status FROM actions WHERE id = %s AND tenant_id = %s """, (action_id, tenant_id), ) action = cur.fetchone() if not action or action["status"] != "blocked": return effects cur.execute( """ SELECT COUNT(*) AS remaining FROM blockers WHERE tenant_id = %s AND action_id = %s AND id != %s AND status IN ('open', 'in_progress') """, (tenant_id, action_id, blocker_id), ) row = cur.fetchone() remaining = int(row["remaining"]) if row else 0 if remaining > 0: effects.append( { "kind": "action_still_blocked", "action_id": str(action_id), "reason": "other_open_blockers", } ) return effects cur.execute( """ UPDATE actions SET status = 'open', updated_at = NOW() WHERE id = %s AND tenant_id = %s AND status = 'blocked' RETURNING id """, (action_id, tenant_id), ) if cur.fetchone(): effects.append( { "kind": "action_unblocked", "action_id": str(action_id), "trigger": "blocker_resolved", "blocker_id": blocker_id, } ) conn.commit() finally: conn.close() if effects: dispatch_hook( "on_blocker_resolved", None, { "blocker_id": blocker_id, "action_id": action_id, "initiative_id": initiative_id, "effects": effects, }, tenant_id=tenant_id, user_id=user_id, ) if initiative_id and user_id: from steering.lifecycle.orchestrator import transition transition( None, initiative_id, "action_selection", reason="blocker_resolved", tenant_id=tenant_id, user_id=user_id, ) if effects and user_id: from services.audit import log_audit log_audit( "operating_transition.action_unblocked", user_id=user_id, tenant_id=tenant_id, details={"effects": effects, "blocker_id": blocker_id}, ) return effects def after_review_status_change( *, tenant_id: str, review_id: str, action_id: Optional[str], initiative_id: Optional[str] = None, old_status: str, new_status: str, user_id: Optional[str] = None, ) -> list[dict[str, Any]]: """Review abgeschlossen → Maßnahme aus review_required weiterführen.""" effects: list[dict[str, Any]] = [] if new_status == old_status or new_status != "completed" or not action_id: return effects conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ UPDATE actions SET status = 'done', updated_at = NOW() WHERE id = %s AND tenant_id = %s AND status = 'review_required' RETURNING id """, (action_id, tenant_id), ) if cur.fetchone(): effects.append( { "kind": "action_completed_after_review", "action_id": str(action_id), "trigger": "review_completed", "review_id": review_id, } ) conn.commit() finally: conn.close() if effects: dispatch_hook( "on_review_completed", None, { "review_id": review_id, "action_id": action_id, "initiative_id": initiative_id, "effects": effects, }, tenant_id=tenant_id, user_id=user_id, ) if effects and user_id: from services.audit import log_audit log_audit( "operating_transition.action_completed_after_review", user_id=user_id, tenant_id=tenant_id, details={"effects": effects, "review_id": review_id}, ) return effects