PMFlow/backend/app/services/ai_pmo_cadence/mixin_queries_b.py

125 lines
4.4 KiB
Python

"""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
]