Files
group_xinghuo_jinrong/app/service/suitability.py
T

120 lines
5.8 KiB
Python
Raw Normal View History

"""适当性校验服务(R-02 · 全系统唯一阻断点 · main 矩阵契约)。
AL-05 对齐 main 基准(2026-09-07):
- 判定内核由「SUIT-001~008 纯函数矩阵」整体替换为 main 的
``CoreReadOnlyRepository.check_suitability``(C×R 数据驱动,L0 权威 =
jinrong_core.core_suitability_rule);check_core / cap_by_age /
match_by_matrix / is_assessment_valid 删除(SUIT-006 封顶被 FM-01 网点确认
替换;SUIT-008 被 FM-03 expires_at 数据驱动替换,settings.risk_assessment_valid_days
退役)。
- 返回 SuitabilityResult 扩展 main 新契约字段(match_result 五值 / mismatch_type
七值 / requires_disclosure / needs_branch_confirm / block_response_code /
rule_refs);同时保留旧字段供既有调用方零改动读取:
- ``rule_id`` = block_response_code(机器码别名,SUIT-00n 体系退役后的过渡口);
正式规则依据请读 ``rule_refs``(JR-AST-012 / FM-01 / FM-03 / JR-AST-PRO)。
- ``effective_level`` = customer_level(SUIT-006 封顶退役后二者恒等)。
- ``reasons`` = [reason](main 契约单条对人可读原因)。
- NotFound 语义变更:客户/产品缺失不再抛 NotFoundError,返回 forbidden/not_found
结构(main 契约)。
- 落库走 ``app/model/suitability.build_suitability_log_row`` 统一映射;
check_source 参数化(网关 r02_trade / 对话 Tool r02_chat / C-11 询问
c11_inquiry / 人工 manual),actor_id 由调用方透传。
- 数据归属校验(G-01)在 FastAPI 依赖层完成,本服务不做。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from app.model.suitability import CheckSource, build_suitability_log_row, compute_rule_refs
from app.repository.core_ro import CoreReadOnlyRepository
from app.repository.risk_repository import RiskRepository
from app.utils.trace import current_trace, new_trace
@dataclass(frozen=True)
class SuitabilityResult:
"""R-02 判定结果(main 契约字段 + 旧字段兼容层)。"""
# ---- main 新契约 ----
match_result: str # allowed / allowed_with_disclosure / forbidden / risk_expired / professional_exempt
mismatch_type: str # none / risk_level / risk_expired / age_branch_confirm / min_subscribe / not_found / professional_exempt
is_matched: bool
blocked: bool
requires_disclosure: bool = False
needs_branch_confirm: bool = False # FM-01 年龄≥70 买 R3+
block_reason: str = "" # 对人可读原因(= reason)
block_response_code: str = "" # API 机器码,如 SUIT_RISK_MISMATCH
rule_refs: list[str] = field(default_factory=list) # ["JR-AST-012","FM-03",...]
investor_category: str = "ordinary"
# ---- 兼容旧调用方(语义映射,见 module docstring) ----
customer_level: str = "" # 原测评等级(= check.customer_risk_code)
product_level: str = "" # 产品最低适配等级(= check.product_risk_code)
effective_level: str = "" # SUIT-006 退役后恒 = customer_level
reasons: list[str] = field(default_factory=list) # = [reason]
rule_id: str = "" # = block_response_code(机器码别名;SUIT-00n 退役)
@classmethod
def from_check(cls, check: dict[str, Any]) -> "SuitabilityResult":
"""core_ro.check_suitability 结果 → SuitabilityResult(含兼容映射)。"""
reason = check.get("reason") or ""
code = check.get("block_response_code") or ""
customer_level = check.get("customer_risk_code") or ""
return cls(
match_result=check.get("match_result") or "forbidden",
mismatch_type=check.get("mismatch_type") or "not_found",
is_matched=bool(check.get("matched")),
blocked=bool(check.get("blocked")),
requires_disclosure=bool(check.get("requires_disclosure")),
needs_branch_confirm=bool(check.get("needs_branch_confirm")),
block_reason=reason,
block_response_code=code,
rule_refs=compute_rule_refs(check),
investor_category=check.get("investor_category") or "ordinary",
customer_level=customer_level,
product_level=check.get("product_risk_code") or "",
effective_level=customer_level,
reasons=[reason] if reason else [],
rule_id=code,
)
def suitability_check(
customer_id: str,
product_id: str,
core_ro: CoreReadOnlyRepository | None = None,
risk_repo: RiskRepository | None = None,
check_source: CheckSource = "r02_trade",
actor_id: str | None = None,
request_ref: str | None = None,
) -> SuitabilityResult:
"""服务入口:core_ro.check_suitability 矩阵判定 → 落 risk_suitability_log(每次校验必落)。
- 签名兼容:前五参位置调用(trade_gateway / risk.py / chat_tools)不受影响;
valid_days / today 参数随 SUIT-008 退役删除(有效期改由
core_customer_risk.expires_at 数据驱动)。
- NotFound:客户/产品缺失返回 forbidden/not_found 结构,不再抛异常。
- check_source:网关 r02_trade / 对话 Tool r02_chat / C-11 c11_inquiry /
manual;actor_id 由调用方透传(网关 "svc-trade-suitability",直调 API 传
auth.actor_id),缺省 "SYSTEM"。
- 阻断时的预警单由调用方(交易网关 FR-1)生成,本函数只落校验日志。
"""
core = core_ro or CoreReadOnlyRepository()
repo = risk_repo or RiskRepository()
check = core.check_suitability(customer_id, product_id)
result = SuitabilityResult.from_check(check)
row = build_suitability_log_row(
check,
trace_id=current_trace() or new_trace(),
actor_id=actor_id or "SYSTEM",
check_source=check_source,
request_ref=request_ref,
profile_l1_version=None,
)
repo.insert_suitability_log(row)
return result