From c92f0d70207c3e7140c4f3b09dcb9065ccd9e249 Mon Sep 17 00:00:00 2001 From: YUAN Date: Mon, 7 Sep 2026 17:12:46 +0800 Subject: [PATCH] =?UTF-8?q?AL-04:=20=E5=BC=95=E5=85=A5=20model/suitability?= =?UTF-8?q?.py=20=E5=AF=B9=E9=BD=90=20main=20=E5=9F=BA=E5=87=86=E2=80=94?= =?UTF-8?q?=E2=80=94build=5Fsuitability=5Flog=5Frow=20=E5=8E=9F=E6=A0=B7?= =?UTF-8?q?=E5=BC=95=E5=85=A5(check=20=E7=BB=93=E6=9E=9C=E2=86=92risk=5Fsu?= =?UTF-8?q?itability=5Flog=20INSERT=20=E8=A1=8C=E6=98=A0=E5=B0=84;rule=5Fr?= =?UTF-8?q?efs=20=E7=BB=84=E5=90=88:JR-AST-012=20=E7=AD=89=E7=BA=A7?= =?UTF-8?q?=E4=B8=8D=E9=85=8D/FM-03=20=E9=A3=8E=E8=AF=84=E8=BF=87=E6=9C=9F?= =?UTF-8?q?/FM-01=20=E7=BD=91=E7=82=B9=E7=A1=AE=E8=AE=A4/JR-AST-PRO=20?= =?UTF-8?q?=E4=B8=93=E4=B8=9A=E8=B1=81=E5=85=8D;CheckSource=20=E5=9B=9B?= =?UTF-8?q?=E5=80=BC=20Literal)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/model/suitability.py | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/model/suitability.py diff --git a/app/model/suitability.py b/app/model/suitability.py new file mode 100644 index 0000000..91ba74d --- /dev/null +++ b/app/model/suitability.py @@ -0,0 +1,55 @@ +"""适当性判定 → 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 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: list[str] = [] + match_result = check.get("match_result") or "forbidden" + 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 { + "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, + }