"""RW-007 研判必须复核第十五条豁免额度(`docs/25` 第七节 #2)。 政策原文(`knowledge/policy/个人投资者适当性管理指南.md:334-341`): - C3 买 R4:签署《产品风险超越投资者风险承受能力揭示书》后**可买**,但单只 R4 持仓 不超过总资产 **20%**; - C4 买 R5:同理,上限 **10%**。 也就是说越级购买本身不是违规,**超出额度**才是。原先 `_assess_rw007` 只看留痕: 只要揭示书、二次确认、录音都齐,就判"疑似误报" —— 而这三样恰恰是豁免的**前提条件**, 把前提当成结论,于是"签了字但买超了额度"被静默放过。 数据缺失的情况单独处理:额度要靠总资产与持仓快照才能核算,本项目的画像与持仓由上游 流程写入(实测 `total_asset` 全为 0、`fin_holding` 为空),既不能默认"没超"判误报, 也不能默认"超了"判风险 —— 那会把豁免规则变成新的误报源。 """ from typing import Any from app.service.risk_judgement_service import ( VERDICT_CONTINUE_REVIEW, VERDICT_RISK_SUPPORTED, VERDICT_SUSPECTED_FALSE_POSITIVE, _assess_rw007, ) COMPLETE_WORK_ORDER: dict[str, Any] = { "risk_disclosure_ack_at": "2026-09-01T00:00:00", "second_confirmation_at": "2026-09-01T00:00:00", "recording_reference": "REC-1", } REQUIRING_PRODUCT: dict[str, Any] = { "risk_level": "R4", "risk_disclosure_required": True, "second_confirmation_required": True, "recording_required": True, } def _detail( snapshot: dict[str, Any], *, work_order: dict[str, Any] | None = None, investor_type: str = "C3", ) -> dict[str, Any]: """一条 C3→R4(相差 1 级、属豁免情形)的 RW-007 详情。""" return { "customer": {"investor_type": investor_type}, "product": dict(REQUIRING_PRODUCT), "work_order": dict(COMPLETE_WORK_ORDER if work_order is None else work_order), "evidence_snapshot": snapshot, } def test_limit_breach_is_risk_supported_even_with_complete_trace() -> None: """这是本次补上的判断:留痕齐全 ≠ 豁免成立,额度超了就是违规。""" result = _assess_rw007( _detail({"exemption_limit": "0.20", "exemption_ratio": "0.35"}) ) assert VERDICT_RISK_SUPPORTED in str(result) assert "35.00%" in str(result) assert "20%" in str(result) def test_within_limit_with_complete_trace_is_a_false_positive() -> None: result = _assess_rw007( _detail({"exemption_limit": "0.20", "exemption_ratio": "0.10"}) ) assert VERDICT_SUSPECTED_FALSE_POSITIVE in str(result) def test_exact_limit_is_not_a_breach() -> None: """政策写的是"不超过",恰好等于上限属于合规。""" result = _assess_rw007( _detail({"exemption_limit": "0.20", "exemption_ratio": "0.2"}) ) assert VERDICT_SUSPECTED_FALSE_POSITIVE in str(result) def test_missing_snapshot_data_asks_for_manual_review() -> None: """额度核算不出结果时,既不判误报也不判风险。""" result = _assess_rw007( _detail( { "exemption_limit": "0.20", "exemption_ratio": None, "exemption_data_missing": True, } ) ) assert VERDICT_CONTINUE_REVIEW in str(result) assert "总资产" in str(result) def test_missing_trace_still_outranks_the_exemption_path() -> None: """留痕不完整仍然直接判风险 —— 豁免的**前提**没满足,谈额度没有意义。""" result = _assess_rw007( _detail( {"exemption_limit": "0.20", "exemption_ratio": "0.01"}, work_order={}, ) ) assert VERDICT_RISK_SUPPORTED in str(result) assert "缺少交易留痕" in str(result) def test_pairs_outside_the_exemption_table_keep_the_old_behaviour() -> None: """C2→R5 不属豁免情形,快照里也不会有额度字段:行为必须与改动前一致。""" result = _assess_rw007(_detail({}, investor_type="C2")) assert VERDICT_SUSPECTED_FALSE_POSITIVE in str(result)