# backend/app/approvals/service.py # 큐 분기(derive_queue = 원본 approve.jsx initStatus) + 상태 전이. from __future__ import annotations from datetime import UTC, datetime from sqlmodel import Session, select from ..automation.event_bus import bus from ..automation.events import APPROVAL_EXECUTED, APPROVAL_UNDONE from ..models import ( Approval, ApprovalLog, ApprovalStatus, AutonomyLevel, AutonomySetting, RiskLevel, ) _LEVELS = ( AutonomyLevel.approval_first.value, AutonomyLevel.mixed.value, AutonomyLevel.full_auto.value, ) def get_autonomy(s: Session) -> str: row = s.get(AutonomySetting, "default") if not row: row = AutonomySetting(id="default", level=AutonomyLevel.mixed.value) s.add(row) s.commit() s.refresh(row) return row.level def set_autonomy(s: Session, level: str) -> str: if level not in _LEVELS: raise ValueError("invalid autonomy level") row = s.get(AutonomySetting, "default") or AutonomySetting(id="default") row.level = level row.updated_at = datetime.now(UTC) s.add(row) s.commit() return level def derive_status(level: str, risk: str) -> str: """원본 approve.jsx initStatus(autonomy) 이식. full_auto: 전부 executed. mixed: low=executed, high=pending. approval_first: 전부 pending.""" if level == AutonomyLevel.full_auto.value: return ApprovalStatus.executed.value if level == AutonomyLevel.mixed.value: return ( ApprovalStatus.executed.value if risk == RiskLevel.low.value else ApprovalStatus.pending.value ) return ApprovalStatus.pending.value # approval_first def derive_queue(s: Session, user_id: str | None = None) -> dict: """자율성 레벨로 모든 approval 의 표시 상태를 파생. 단, 사용자가 명시적으로 undone 한 것은 pending, 엔진이 실제 실행한 건은 executed 로 고정. user_id 가 주어지면 소유자 스코프(phase-15).""" level = get_autonomy(s) stmt = select(Approval).order_by(Approval.sort_order) if user_id is not None: stmt = stmt.where(Approval.user_id == user_id) rows = s.exec(stmt).all() pending, done = [], [] for a in rows: if a.status == ApprovalStatus.undone.value: # 사용자가 되돌림 → 대기로 eff = ApprovalStatus.pending.value elif a.status == ApprovalStatus.executed.value and a.executed_at: eff = ApprovalStatus.executed.value # 사용자/엔진이 실제 실행한 건은 고정 else: eff = derive_status(level, a.risk) # 시드/표시용은 레벨로 파생 (done if eff == ApprovalStatus.executed.value else pending).append(a) return {"level": level, "pending": pending, "done": done} def approve(s: Session, aid: str) -> Approval: a = s.get(Approval, aid) a.status = ApprovalStatus.executed.value a.executed_at = datetime.now(UTC) s.add(a) s.commit() s.refresh(a) s.add(ApprovalLog(id="alog-" + aid + "-x", time="방금", text=a.title, approval_id=aid)) s.commit() bus.publish(APPROVAL_EXECUTED, {"approval_id": aid, "risk": a.risk, "title": a.title}) return a def undo(s: Session, aid: str) -> Approval: a = s.get(Approval, aid) a.status = ApprovalStatus.undone.value a.undone_at = datetime.now(UTC) s.add(a) s.commit() s.refresh(a) bus.publish(APPROVAL_UNDONE, {"approval_id": aid, "title": a.title}) return a def approve_all(s: Session) -> int: q = derive_queue(s) n = 0 for a in q["pending"]: approve(s, a.id) n += 1 return n