66 lines
2.6 KiB
Python
66 lines
2.6 KiB
Python
"""适当性判定 → risk_suitability_log 行映射(R-02 P0 契约)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Literal
|
||
|
||
CheckSource = Literal["r02_trade", "r02_chat", "c11_inquiry", "manual"]
|
||
|
||
|
||
def compute_rule_refs(check: dict[str, Any]) -> list[str]:
|
||
"""由 check 结果计算 rule_refs(JR-AST/FM 规则编号体系)。
|
||
|
||
独立公共函数(AL-05 抽取,main 逻辑不变):供 build_suitability_log_row
|
||
落库与 service 层 SuitabilityResult.rule_refs 共用,避免两处漂移。
|
||
"""
|
||
rule_refs: list[str] = []
|
||
mismatch_type = check.get("mismatch_type") or "not_found"
|
||
if mismatch_type == "risk_level":
|
||
rule_refs.append("JR-AST-012")
|
||
if check.get("risk_is_expired") or mismatch_type == "risk_expired":
|
||
rule_refs.append("FM-03")
|
||
if check.get("needs_branch_confirm") or mismatch_type == "age_branch_confirm":
|
||
rule_refs.append("FM-01")
|
||
if mismatch_type == "professional_exempt":
|
||
rule_refs.append("JR-AST-PRO")
|
||
return rule_refs
|
||
|
||
|
||
def build_suitability_log_row(
|
||
check: dict[str, Any],
|
||
*,
|
||
trace_id: str,
|
||
actor_id: str,
|
||
check_source: CheckSource = "r02_trade",
|
||
request_ref: str | None = None,
|
||
profile_l1_version: int | None = None,
|
||
) -> dict[str, Any]:
|
||
"""将 CoreReadOnlyRepository.check_suitability 结果转为 INSERT 用 dict。"""
|
||
rule_refs = compute_rule_refs(check)
|
||
match_result = check.get("match_result") or "forbidden"
|
||
mismatch_type = check.get("mismatch_type") or "not_found"
|
||
|
||
return {
|
||
"trace_id": trace_id,
|
||
"customer_id": check.get("customer_id") or "",
|
||
"product_id": check.get("product_id") or "",
|
||
"product_name": check.get("product_name"),
|
||
"customer_risk_level": check.get("customer_risk_code") or "C1",
|
||
"product_risk_level": check.get("product_risk_code") or "R1",
|
||
"investor_category": check.get("investor_category") or "ordinary",
|
||
"match_result": match_result,
|
||
"mismatch_type": mismatch_type,
|
||
"is_matched": 1 if check.get("matched") else 0,
|
||
"is_blocked": 1 if check.get("blocked") else 0,
|
||
"requires_disclosure": 1 if check.get("requires_disclosure") else 0,
|
||
"needs_branch_confirm": 1 if check.get("needs_branch_confirm") else 0,
|
||
"risk_was_expired": 1 if check.get("risk_is_expired") else 0,
|
||
"block_reason": check.get("reason"),
|
||
"block_response_code": check.get("block_response_code"),
|
||
"check_source": check_source,
|
||
"actor_id": actor_id,
|
||
"request_ref": request_ref,
|
||
"profile_l1_version": profile_l1_version,
|
||
"rule_refs": rule_refs or None,
|
||
}
|