from datetime import datetime from decimal import Decimal import pytest from app.core.contracts import RequestContext from app.model.audit import InteractionAudit from app.model.fund import FundCustomerProfile, FundRiskAlert from app.service.risk_action_service import RiskActionError, RiskActionService class FakeSession: def __init__(self, alerts=None, profiles=None): self.values = list(alerts or []) self.profiles = list(profiles or []) self.added = [] self.committed = False async def scalar(self, _statement): if self.values: return self.values.pop(0) if self.profiles: return self.profiles.pop(0) return None def add(self, value): self.added.append(value) async def commit(self): self.committed = True async def refresh(self, _value): return None def context() -> RequestContext: return RequestContext( user_id="990000002", trace_id="action-trace", permissions=("risk:alert:write",), data_scope="all", ) def alert( *, status: str = "待处理", ack_at: datetime | None = None, level: str = "高", ) -> FundRiskAlert: return FundRiskAlert( id=1, alert_no="ALERT-001", customer_id=9, alert_type="适当性错配", alert_level=level, trigger_rule_codes=["RW-007"], evidence_summary="证据", evidence_snapshot={}, priority_score=90, event_status="正在发生", status=status, ack_status="已确认" if ack_at else "未确认", ack_at=ack_at, is_escalated=0, created_at=datetime(2026, 9, 10), updated_at=datetime(2026, 9, 10), ) def profile(score: int = 20) -> FundCustomerProfile: return FundCustomerProfile( customer_id=9, trade_account="ACC-009", real_name="张三", investor_type="C2", total_asset=Decimal("100000.00"), behavior_score=score, updated_at=datetime(2026, 9, 10), ) @pytest.mark.asyncio async def test_acknowledge_updates_status_and_audit() -> None: session = FakeSession(alerts=[alert()]) result = await RiskActionService(session).acknowledge("ALERT-001", context()) assert result["ack_status"] == "已确认" assert session.committed is True assert session.added[0].action_type == "risk_alert_acknowledged" @pytest.mark.asyncio async def test_investigate_requires_acknowledgement() -> None: session = FakeSession(alerts=[alert()]) with pytest.raises(RiskActionError, match="确认接收"): await RiskActionService(session).investigate("ALERT-001", context()) @pytest.mark.asyncio async def test_exclude_requires_reason_and_closes_false_positive() -> None: session = FakeSession(alerts=[alert(ack_at=datetime(2026, 9, 10))]) result = await RiskActionService(session).exclude("ALERT-001", "客户本人确认", context()) assert result["status"] == "已排除" assert result["handle_result"] == "客户本人确认" @pytest.mark.parametrize( ("level", "before", "deduction", "after"), [("低", 20, 3, 17), ("中", 20, 5, 15), ("高", 20, 20, 0), ("高", 4, 20, 0)], ) @pytest.mark.asyncio async def test_resolve_applies_behavior_score_deduction( level: str, before: int, deduction: int, after: int, ) -> None: session = FakeSession( alerts=[alert(status="调查中", ack_at=datetime(2026, 9, 10), level=level)], profiles=[profile(before)], ) result = await RiskActionService(session).resolve("ALERT-001", "已核实", context()) assert result["status"] == "已结案" assert result["behavior_score_deduction"] == deduction assert result["behavior_score_after"] == after assert any(isinstance(item, InteractionAudit) for item in session.added) @pytest.mark.asyncio async def test_escalate_is_not_terminal_and_rejects_duplicate() -> None: session = FakeSession(alerts=[alert(status="调查中", ack_at=datetime(2026, 9, 10))]) result = await RiskActionService(session).escalate("ALERT-001", "需要高级复核", context()) assert result["is_escalated"] is True assert result["status"] == "调查中" assert result["escalation_reason"] == "需要高级复核" session = FakeSession(alerts=[alert(status="调查中", ack_at=datetime(2026, 9, 10))]) session.values[0].is_escalated = 1 with pytest.raises(RiskActionError, match="已经升级"): await RiskActionService(session).escalate("ALERT-001", "重复", context())