Files
group_fqcd_jr/tests/unit/service/test_risk_judgement_rw018.py
T
lzf_0626 85a1b33eff fix(risk): RW-018 研判的两个口径缺陷——列表级不再无条件放行,详情级与扫描侧对齐
docs/25 P2。核实后发现这两条其实是**同一处的两个表现**,所以一起修。

**① 列表级无条件放行**
_assess_list_rule 收到 item 参数**却完全不看它**,对 RW-018 一律返回"可考虑放行",
连理由文本都是硬编码的"现有摘要显示交易来自有效定投工单"。于是渠道不匹配、或证据里
根本没有工单信息的预警,在列表层就被标成可放行 —— 而列表正是风控专员最先看到的一屏
(详情层另有判断,但那要等人点进去)。

现在按 item["evidence_snapshot"]["channel"] 判断。快照里确实有渠道:扫描侧写入了它
(risk_scan_service.py:308),列表行也带出了整个 snapshot(risk_repository.py:955),
所以这个校验是可行的,此前只是没做。

**② 详情级与扫描侧口径不一致**
扫描按 work_order.channel in {"定投", "自动定投"} 生成预警(risk_scan_service.py:294),
详情级却只认 "定投"。于是"自动定投"的预警会出现**"扫描认为有效、详情认为未确认"**的
自相矛盾。

抽出 DIRECT_INVESTMENT_CHANNELS = frozenset({"定投", "自动定投"}) 作为唯一口径,
列表级与详情级都改用它;改这份常量即同时影响两侧。

新增 tests/unit/service/test_risk_judgement_rw018.py(6 条):常量覆盖扫描侧全部渠道、
列表级在无渠道 / 渠道不符时**不得**放行、两个合法渠道在列表级与详情级都放行、
详情级对无关渠道不放行。

ruff / mypy(136 文件) / 629 unit+contract 全绿。
2026-09-11 13:45:51 +08:00

66 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""RW-018 研判的两个口径缺陷(docs/25 P2)。
1. **列表级无条件放行**:`_assess_list_rule` 收到 `item` 却完全不看它,对 RW-018
一律返回"可考虑放行",连理由文本都是硬编码的"现有摘要显示交易来自有效定投工单"。
于是渠道不匹配、或证据里根本没有工单信息的预警,在列表层就被标成可放行 ——
而列表正是风控专员最先看到的一屏。详情层另有判断,但那要等人点进去。
2. **详情级与扫描侧口径不一致**:扫描按 `work_order.channel in {"定投", "自动定投"}`
生成 RW-018 预警(`risk_scan_service.py:294`),详情级却只认 `"定投"`。于是
"自动定投"的预警会出现"扫描认为有效、详情认为未确认"的自相矛盾。
两条现在共用 `DIRECT_INVESTMENT_CHANNELS`,改这份常量即同时影响两侧。
"""
from typing import Any
from app.service.risk_judgement_service import (
DIRECT_INVESTMENT_CHANNELS,
VERDICT_RELEASE,
_assess_list_rule,
_assess_rw018,
)
def _item(**snapshot: Any) -> dict[str, Any]:
return {"alert_no": "ALTEST", "evidence_snapshot": snapshot}
def test_channels_cover_what_the_scanner_accepts() -> None:
"""常量必须覆盖扫描侧接受的全部渠道,否则又会两侧打架。"""
assert DIRECT_INVESTMENT_CHANNELS == {"定投", "自动定投"}
def test_list_level_does_not_release_without_evidence() -> None:
"""证据里没有渠道信息时不能放行 —— 这正是原先无条件放行的地方。"""
result = _assess_list_rule("RW-018", _item())
assert VERDICT_RELEASE not in str(result)
def test_list_level_does_not_release_for_an_unrelated_channel() -> None:
result = _assess_list_rule("RW-018", _item(channel="柜台"))
assert VERDICT_RELEASE not in str(result)
def test_list_level_releases_for_every_direct_investment_channel() -> None:
for channel in sorted(DIRECT_INVESTMENT_CHANNELS):
result = _assess_list_rule("RW-018", _item(channel=channel))
assert VERDICT_RELEASE in str(result), f"{channel} 应当判定为可放行"
def test_detail_level_accepts_the_same_channels_as_the_scanner() -> None:
"""详情级与扫描侧同口径 —— 原先详情只认"定投","自动定投"会被判成未确认。"""
for channel in sorted(DIRECT_INVESTMENT_CHANNELS):
result = _assess_rw018({"work_order": {"channel": channel}})
assert VERDICT_RELEASE in str(result), f"{channel} 在详情级也应判为可放行"
def test_detail_level_rejects_a_channel_the_scanner_would_not_use() -> None:
result = _assess_rw018({"work_order": {"channel": "柜台"}})
assert VERDICT_RELEASE not in str(result)