Files
group_fqcd_jr/tests/unit/service/test_risk_action_service.py
T

150 lines
4.5 KiB
Python

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())