"""Query helpers (part B) for AI PMO cadence service.""" from __future__ import annotations from .shared import ( Any, Budget, Notification, Optional, SignalSourceRegistry, UserProject, UserWorkload, and_, select, ) class CadenceQueriesBMixin: """Budget/capacity/signal-source/notification query helpers.""" async def _list_budget_hotspots(self, *, project_id: Optional[int]) -> list[dict[str, Any]]: stmt = select(Budget).where(Budget.tenant_id == self.tenant_id) if project_id is not None: stmt = stmt.where(Budget.project_id == int(project_id)) rows = (await self.db.execute(stmt)).scalars().all() hotspots = [] for row in rows: amount = float(row.budget_amount_usd or 0.0) usage = float(row.current_usage_usd or 0.0) ratio = (usage / amount) if amount > 0 else 0.0 if ratio >= 0.8: hotspots.append( { "budget_id": int(row.id), "project_id": row.project_id, "usage_ratio": round(ratio, 4), "hard_limit_enabled": bool(row.hard_limit_enabled), } ) return sorted(hotspots, key=lambda item: item["usage_ratio"], reverse=True) async def _list_capacity_hotspots(self, *, project_id: Optional[int]) -> list[dict[str, Any]]: stmt = select(UserWorkload).where( and_( UserWorkload.tenant_id == self.tenant_id, UserWorkload.workload_ratio >= 0.85, ) ) if project_id is not None: stmt = stmt.where( UserWorkload.user_id.in_( select(UserProject.user_id).where( and_( UserProject.tenant_id == self.tenant_id, UserProject.project_id == int(project_id), ) ) ) ) rows = (await self.db.execute(stmt.limit(50))).scalars().all() return [ { "user_id": int(row.user_id), "workload_ratio": float(row.workload_ratio or 0.0), "current_assigned_hours": float(row.current_assigned_hours or 0.0), } for row in rows ] async def _list_pending_signal_sources(self, *, project_id: Optional[int]) -> list[dict[str, Any]]: stmt = select(SignalSourceRegistry).where( and_( SignalSourceRegistry.tenant_id == self.tenant_id, SignalSourceRegistry.onboarding_status == "pending_manual", ) ) if project_id is not None: stmt = stmt.where(SignalSourceRegistry.project_id == int(project_id)) rows = ( await self.db.execute( stmt.order_by(SignalSourceRegistry.updated_at.desc()).limit(50) ) ).scalars().all() return [ { "registry_id": int(row.id), "project_id": int(row.project_id), "source_kind": str(row.source_kind or ""), "source_name": str(row.source_name or ""), "status": str(row.status or ""), "onboarding_status": str(row.onboarding_status or ""), } for row in rows ] async def _list_unread_risk_notifications(self, *, project_id: Optional[int]) -> list[dict[str, Any]]: stmt = select(Notification).where( and_( Notification.tenant_id == self.tenant_id, Notification.is_read.is_(False), Notification.kind.in_(["risk", "alert", "action_required"]), ) ) if project_id is not None: stmt = stmt.where( and_( Notification.entity_type == "project", Notification.entity_id == int(project_id), ) ) rows = ( await self.db.execute( stmt.order_by(Notification.updated_at.desc()).limit(50) ) ).scalars().all() return [ { "id": int(row.id), "title": str(row.title or ""), "status": str(row.status or ""), "priority": str(row.priority or ""), } for row in rows ]