Files
group_xinghuo_jinrong/app/service/suitability.py
T

120 lines
5.8 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.
"""适当性校验服务(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