from datetime import date from decimal import Decimal from typing import Any from sqlalchemy import select from app.core.http.pagination import bounded_limit from app.modules.business.constants import ( DONE_STATUSES, GENERATED_RISK_EVENT_TYPES, PROJECT_CLOSED_STATUSES, SUPPLIER_RISK_LEVELS, StatusValue, ) from app.modules.business.models import ( FundAccount, Project, RiskEvent, RiskEventAction, Supplier, WorkTask, ) from app.modules.business.service import serialize_model from app.modules.risk.constants import ( RISK_SCORE_WEIGHTS, RiskEventPayloadKey, RiskSummaryKey, risk_level_for_score, ) class RiskQueryMixin: def overdue_tasks(self) -> list[dict[str, Any]]: stmt = select(WorkTask).where( WorkTask.due_date.is_not(None), WorkTask.due_date < date.today(), WorkTask.status.notin_(DONE_STATUSES), ) return [serialize_model(item) for item in self.db.execute(stmt).scalars()] def delayed_projects(self) -> list[dict[str, Any]]: stmt = select(Project).where( Project.due_date.is_not(None), Project.due_date < date.today(), Project.status.notin_(PROJECT_CLOSED_STATUSES), ) return [serialize_model(item) for item in self.db.execute(stmt).scalars()] def over_budget_projects(self) -> list[dict[str, Any]]: stmt = select(Project).where( Project.budget_amount > 0, Project.actual_amount > Project.budget_amount, ) return [serialize_model(item) for item in self.db.execute(stmt).scalars()] def fund_risks(self) -> list[dict[str, Any]]: stmt = select(FundAccount).where(FundAccount.current_balance < FundAccount.safety_line) return [serialize_model(item) for item in self.db.execute(stmt).scalars()] def supplier_risks(self) -> list[dict[str, Any]]: stmt = select(Supplier).where( (Supplier.blacklist_status != StatusValue.NORMAL) | Supplier.risk_level.in_(SUPPLIER_RISK_LEVELS) ) return [serialize_model(item) for item in self.db.execute(stmt).scalars()] def list_events( self, limit: int = 100, status_filter: str | None = None, ) -> list[dict[str, Any]]: limit_value = bounded_limit(limit) stmt = select(RiskEvent).order_by(RiskEvent.id.desc()).limit(limit_value) if status_filter: stmt = ( select(RiskEvent) .where(RiskEvent.status == status_filter) .order_by(RiskEvent.id.desc()) .limit(limit_value) ) return [serialize_model(item) for item in self.db.execute(stmt).scalars()] def list_actions(self, risk_event_id: int, limit: int = 100) -> list[dict[str, Any]]: self._get_event(risk_event_id) stmt = ( select(RiskEventAction) .where(RiskEventAction.risk_event_id == risk_event_id) .order_by(RiskEventAction.id.desc()) .limit(bounded_limit(limit)) ) return [serialize_model(item) for item in self.db.execute(stmt).scalars()] def summary(self) -> dict[str, Any]: overdue_tasks = self.overdue_tasks() delayed_projects = self.delayed_projects() over_budget_projects = self.over_budget_projects() fund_risks = self.fund_risks() supplier_risks = self.supplier_risks() open_events = self.list_events(status_filter=StatusValue.OPEN) external_open_events = [ item for item in open_events if item.get(RiskEventPayloadKey.RISK_TYPE) not in GENERATED_RISK_EVENT_TYPES ] risk_score = ( len(overdue_tasks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.OVERDUE_TASKS] + len(delayed_projects) * RISK_SCORE_WEIGHTS[RiskSummaryKey.DELAYED_PROJECTS] + len(over_budget_projects) * RISK_SCORE_WEIGHTS[RiskSummaryKey.OVER_BUDGET_PROJECTS] + len(fund_risks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.FUND_RISKS] + len(supplier_risks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.SUPPLIER_RISKS] + len(external_open_events) * RISK_SCORE_WEIGHTS[RiskSummaryKey.OPEN_EVENTS] ) return { RiskSummaryKey.RISK_LEVEL: risk_level_for_score(risk_score), RiskSummaryKey.RISK_SCORE: Decimal(risk_score), RiskSummaryKey.OVERDUE_TASKS: overdue_tasks, RiskSummaryKey.DELAYED_PROJECTS: delayed_projects, RiskSummaryKey.OVER_BUDGET_PROJECTS: over_budget_projects, RiskSummaryKey.FUND_RISKS: fund_risks, RiskSummaryKey.SUPPLIER_RISKS: supplier_risks, RiskSummaryKey.OPEN_EVENTS: open_events, }