按 docs/v2/05-implementation-plan.md §1 候选迁移表与 claude_use/T00 白名单, 从 55e2cdb 抽取 224 个文件(2.48 MB)到独立目录 F:/mini-OpenClaw-v2。 - 排除:凭据(.env/server-deploy.conf/旧DELIVERY.md)、运行数据(xingbang_data 等 4 个 SQLite)、依赖(venv/node_modules 符号链接)、归档(_backup*/test-evidence/ ori_file)、Office 临时文件 - 目标目录独立 git 仓库,初始恢复点;Gitea 远程与分支待 OQ-06 Q2 定案 - 未部署、未启动服务、未调用模型 Co-Authored-By: Claude Code <noreply@anthropic.com>
118 lines
8.1 KiB
Python
118 lines
8.1 KiB
Python
"""Deterministic evidence checks and conservative recovery scenarios."""
|
||
from datetime import date
|
||
from decimal import Decimal, ROUND_HALF_UP
|
||
|
||
KINDS = {"asset": "租赁物", "insurance": "保险", "deposit": "保证金", "guarantee": "保证", "mortgage": "抵押", "pledge": "质押"}
|
||
|
||
|
||
def money(value):
|
||
return Decimal(str(value or 0))
|
||
|
||
|
||
def number(value):
|
||
return str(value.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP))
|
||
|
||
|
||
def calculate(case, rules, haircut, today=None):
|
||
today = today or date.today()
|
||
checks = []
|
||
assets = {}
|
||
deposits = {}
|
||
identifiers = {m["resource_key"] for m in case["measures"] if m["kind"] == "asset" and m["resource_key"]}
|
||
|
||
def add(key, title, status, detail, measure=None):
|
||
checks.append({"id": f"{measure['id'] if measure else 'project'}:{key}", "measure_id": measure["id"] if measure else None,
|
||
"title": title, "status": status, "detail": detail, "evidence": measure.get("evidence") if measure else None})
|
||
|
||
for kind in rules["required_kinds"]:
|
||
if not any(m["kind"] == kind for m in case["measures"]):
|
||
add("required-"+kind, "缺少"+KINDS[kind], "missing", "尚未接入,不代表不存在风险")
|
||
duplicate_keys = {}
|
||
for m in case["measures"]:
|
||
if m["resource_key"] and m["kind"] != "insurance":
|
||
duplicate_keys.setdefault(m["resource_key"], []).append(m["id"])
|
||
for key, ids in duplicate_keys.items():
|
||
if len(ids) > 1:
|
||
add("duplicate-"+key, "同一风险来源存在重复记录", "review", "关联标识 "+key+";重复来源不叠加计入可回收金额")
|
||
|
||
for m in case["measures"]:
|
||
start = len(checks)
|
||
label = m["name"]
|
||
if not m.get("confirmed_by"):
|
||
add("confirmed", label+":事实待人工确认", "review", "录入或提取结果尚未经审核员核对", m)
|
||
evidence = m.get("evidence", {})
|
||
if not evidence.get("reference") and not evidence.get("document_id"):
|
||
add("evidence", label+":来源缺失", "missing", "需要原始文件页码或 ERP 材料编号", m)
|
||
as_of = evidence.get("as_of")
|
||
if not as_of:
|
||
add("date", label+":证据日期缺失", "missing", "无法判断数据新鲜度", m)
|
||
else:
|
||
delta = (today-date.fromisoformat(as_of)).days
|
||
if delta < 0 or delta > rules["max_age_days"]:
|
||
add("date", label+":证据日期需复核", "review", f"证据相差 {delta} 天;允许 {rules['max_age_days']} 天", m)
|
||
if m.get("amount") is None:
|
||
add("amount", label+":金额缺失", "missing", "金额统一采用人民币万元", m)
|
||
if not m["resource_key"]:
|
||
add("identity", label+":唯一标识缺失", "missing", "补充序列号、登记号、保单号或账户标识", m)
|
||
kind = m["kind"]
|
||
bools = {"asset": ["ownership_clear", "enforceable"], "mortgage": ["ownership_clear", "registered", "enforceable"],
|
||
"pledge": ["ownership_clear", "registered", "enforceable"], "deposit": ["paid", "enforceable"], "guarantee": ["enforceable"], "insurance": []}[kind]
|
||
names = {"ownership_clear": "权属及权利限制", "registered": "登记状态", "enforceable": "可执行性", "paid": "实际到账"}
|
||
for key in bools:
|
||
if m.get(key) is not True:
|
||
add(key, label+":"+names[key], "missing" if m.get(key) is None else "fail", "待核实" if m.get(key) is None else "未满足条件", m)
|
||
if kind == "asset":
|
||
if not m.get("asset_type"):
|
||
add("asset_type", label+":租赁物类型缺失", "missing", "需核对适格性", m)
|
||
elif not rules["allowed_asset_types"]:
|
||
add("asset_type", label+":适格性待复核", "review", "规则尚未配置允许的租赁物类型", m)
|
||
elif m["asset_type"] not in rules["allowed_asset_types"]:
|
||
add("asset_type", label+":不在允许类型中", "fail", "依据所选规则版本", m)
|
||
if kind in ("insurance", "guarantee", "mortgage", "pledge"):
|
||
if not m.get("start_date") or not m.get("end_date"):
|
||
add("period", label+":有效期间缺失", "missing", "需核对起止日期", m)
|
||
elif date.fromisoformat(m["start_date"]) > today or date.fromisoformat(m["end_date"]) < date.fromisoformat(case["lease_end"]):
|
||
add("period", label+":期限未覆盖", "fail", "尚未生效或未覆盖方案到期日 "+case["lease_end"], m)
|
||
if kind == "insurance":
|
||
absent = set(rules["required_coverages"]) - set(m["coverage_types"])
|
||
if absent:
|
||
add("coverages", label+":缺少必需险种", "fail", "、".join(sorted(absent)), m)
|
||
if not rules["beneficiary"]:
|
||
add("beneficiary", label+":受益人规则未配置", "review", "需要配置并审核正式受益人要求", m)
|
||
elif m.get("beneficiary") != rules["beneficiary"]:
|
||
add("beneficiary", label+":受益人不一致", "fail", "应为 "+rules["beneficiary"], m)
|
||
if not m["insured_keys"] or not set(m["insured_keys"]).issubset(identifiers):
|
||
add("insured", label+":保险标的无法匹配", "review", "核对租赁物唯一标识与保单清单", m)
|
||
effective = min(money(m.get("amount")), money(m.get("payout_limit"))) if m.get("payout_limit") is not None else Decimal(0)
|
||
if m.get("payout_limit") is None or m.get("deductible") is None:
|
||
add("payout", label+":赔偿上限或免赔额缺失", "missing", "不能以保额直接推定有效赔偿", m)
|
||
else:
|
||
effective = max(Decimal(0), effective-money(m["deductible"]))
|
||
ratio = effective / money(case["amount"])
|
||
add("insurance_ratio", label+":单张保单名义覆盖", "pass" if ratio >= money(rules["min_insurance_coverage"]) else "fail",
|
||
f"(min(保额,赔偿上限)-免赔额)/授信 = {number(ratio)};不与资产回收价值相加", m)
|
||
if not m.get("exclusions") or not m.get("clauses_reviewed"):
|
||
add("exclusions", label+":除外责任待复核", "review", "需记录除外条款并由审核员确认其适用性", m)
|
||
if kind == "guarantee":
|
||
add("capacity", label+":代偿能力需单独判断", "review", "保证额度不直接计入可回收现金;需结合保证人财务和合同证据", m)
|
||
if kind in ("asset", "mortgage", "pledge") and m.get("prior_claims") is None:
|
||
add("prior_claims", label+":优先权金额缺失", "missing", "未查明优先受偿权时不计入回收价值", m)
|
||
issues = [x for x in checks[start:] if x["status"] != "pass"]
|
||
if not issues:
|
||
add("ready", label+":已配置检查项满足", "pass", "仅表示所选规则及已确认材料检查结果,不构成投放许可", m)
|
||
key = m["resource_key"]
|
||
if len(duplicate_keys.get(key, [])) <= 1:
|
||
if kind in ("asset", "mortgage", "pledge"):
|
||
assets[key] = max(Decimal(0), money(m["amount"]) * (1-money(haircut)/100)-money(m["prior_claims"]))
|
||
elif kind == "deposit":
|
||
deposits[key] = money(m["amount"])
|
||
recovery = sum(assets.values(), Decimal(0)) + sum(deposits.values(), Decimal(0))
|
||
exposure = money(case["amount"])
|
||
ratio = recovery / exposure
|
||
add("coverage", "折价后直接回收覆盖", "pass" if ratio >= money(rules["min_asset_coverage"]) else "fail",
|
||
f"(已确认资产折价净值+已确认到账保证金)/授信 = {number(ratio)};要求 {rules['min_asset_coverage']}")
|
||
return {"checks": checks, "scenario": {"haircut": str(haircut), "asset_recovery": number(sum(assets.values(), Decimal(0))),
|
||
"deposit_recovery": number(sum(deposits.values(), Decimal(0))), "total_recovery": number(recovery),
|
||
"coverage": number(ratio), "uncovered": number(max(Decimal(0), exposure-recovery)),
|
||
"formula": "sum(max(0,资产价值*(1-折价率)-优先权金额))+已到账保证金;重复来源排除;保险及保证不叠加"}}
|