from typing import Any from app.core.constants import ActorValue from app.core.utils.time import utc_now from app.modules.audit.constants import ( AuditAction, AuditRiskLevel, AuditSource, ) from app.modules.audit.schemas import AuditLogCreate from app.modules.audit.service import AuditService from app.modules.business.constants import ( BusinessDomain, StatusValue, ) from app.modules.business.models import ( RiskEvent, RiskEventAction, ) from app.modules.business.service import serialize_model from app.modules.events.constants import ( EventAggregateType, EventPayloadKey, EventSource, EventType, ) from app.modules.events.services import EventService from app.modules.risk.constants import ( RiskEventActionKey, RiskEventActionValue, RiskErrorDetail, ) class RiskActionMixin: def assign_event( self, risk_event_id: int, assigned_to: str, comment: str | None = None, actor: str = ActorValue.API, ) -> dict[str, Any]: record = self._get_event(risk_event_id) from_status = record.status record.assigned_to = assigned_to action = self._record_action( record, RiskEventActionValue.ASSIGN, actor, from_status, record.status, comment, {RiskEventActionKey.ASSIGNED_TO: assigned_to}, ) self.db.commit() self.db.refresh(record) self.db.refresh(action) return self._action_response(record, action) def comment_event( self, risk_event_id: int, comment: str, payload: dict[str, Any] | None = None, actor: str = ActorValue.API, ) -> dict[str, Any]: record = self._get_event(risk_event_id) action = self._record_action( record, RiskEventActionValue.COMMENT, actor, record.status, record.status, comment, payload or {}, ) self.db.commit() self.db.refresh(record) self.db.refresh(action) return self._action_response(record, action) def resolve_event( self, risk_event_id: int, comment: str | None = None, payload: dict[str, Any] | None = None, actor: str = ActorValue.API, ) -> dict[str, Any]: record = self._get_event(risk_event_id) from_status = record.status record.status = StatusValue.RESOLVED record.resolved_at = utc_now() action = self._record_action( record, RiskEventActionValue.RESOLVE, actor, from_status, record.status, comment, payload or {}, ) self.db.commit() self.db.refresh(record) self.db.refresh(action) return self._action_response(record, action) def close_event( self, risk_event_id: int, closed_reason: str, review_summary: str | None = None, actor: str = ActorValue.API, ) -> dict[str, Any]: record = self._get_event(risk_event_id) from_status = record.status now = utc_now() record.status = StatusValue.CLOSED record.closed_reason = closed_reason record.review_summary = review_summary record.closed_at = now if record.resolved_at is None: record.resolved_at = now action = self._record_action( record, RiskEventActionValue.CLOSE, actor, from_status, record.status, closed_reason, {RiskEventActionKey.REVIEW_SUMMARY: review_summary}, ) self.db.commit() self.db.refresh(record) self.db.refresh(action) return self._action_response(record, action) def reopen_event( self, risk_event_id: int, comment: str | None = None, actor: str = ActorValue.API, ) -> dict[str, Any]: record = self._get_event(risk_event_id) from_status = record.status record.status = StatusValue.OPEN record.resolved_at = None record.closed_at = None action = self._record_action( record, RiskEventActionValue.REOPEN, actor, from_status, record.status, comment, {}, ) self.db.commit() self.db.refresh(record) self.db.refresh(action) return self._action_response(record, action) def _get_event(self, risk_event_id: int) -> RiskEvent: record = self.db.get(RiskEvent, risk_event_id) if record is None: from fastapi import HTTPException, status raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=RiskErrorDetail.RISK_EVENT_NOT_FOUND, ) return record def _record_action( self, record: RiskEvent, action: str, actor: str, from_status: str | None, to_status: str | None, comment: str | None, payload: dict[str, Any], ) -> RiskEventAction: action_record = RiskEventAction( code=f"RISK-ACTION-{utc_now():%Y%m%d%H%M%S%f}", risk_event_id=record.id, action=action, actor=actor, from_status=from_status, to_status=to_status, assigned_to=record.assigned_to, comment=comment, payload=payload, ) self.db.add(action_record) AuditService(self.db).log( AuditLogCreate( actor=actor, source=AuditSource.RISK, action=AuditAction.RISK_EVENT_ACTION, target_type=BusinessDomain.RISK_EVENTS, target_id=str(record.id), risk_level=AuditRiskLevel.MEDIUM, request_payload={ RiskEventActionKey.ACTION: action, RiskEventActionKey.FROM_STATUS: from_status, RiskEventActionKey.TO_STATUS: to_status, RiskEventActionKey.COMMENT: comment, RiskEventActionKey.PAYLOAD: payload, }, ) ) EventService(self.db).emit( event_type=EventType.RISK_ACTION_RECORDED, source=EventSource.RISK, aggregate_type=EventAggregateType.RISK_EVENT, aggregate_id=record.id, actor=actor, payload={ EventPayloadKey.ACTION: action, EventPayloadKey.STATUS: to_status, EventPayloadKey.RECORD_ID: str(record.id), RiskEventActionKey.FROM_STATUS: from_status, RiskEventActionKey.TO_STATUS: to_status, RiskEventActionKey.COMMENT: comment, RiskEventActionKey.PAYLOAD: payload, }, idempotency_key=f"risk:{record.id}:{action_record.code}", dispatch=True, ) return action_record @staticmethod def _action_response(record: RiskEvent, action: RiskEventAction) -> dict[str, Any]: return { RiskEventActionKey.RISK_EVENT: serialize_model(record), RiskEventActionKey.ACTION_RECORD: serialize_model(action), }