119 lines
4.8 KiB
Python
119 lines
4.8 KiB
Python
"""PlanData v1 的结构、冲突和证据门禁。"""
|
|
from __future__ import annotations
|
|
|
|
from insurance.plan_data.field_values import validate_field_value
|
|
|
|
|
|
AMOUNT_TOKENS = (
|
|
"premium", "sumassured", "cashvalue", "surrendervalue",
|
|
"deathbenefit", "accountvalue", "amount", "withdrawal",
|
|
)
|
|
|
|
# 只有核心保单金额需要在确认时提示证据缺失。利益演示、退保价值和
|
|
# 提领方案属于可选展示数据,不参与确认门禁。
|
|
CRITICAL_AMOUNT_PATHS = {
|
|
"policy.annualPremium",
|
|
"policy.sumAssured",
|
|
}
|
|
|
|
|
|
def validate_plan_data(plan_data: dict, *, for_confirmation: bool = False) -> dict:
|
|
issues: list[dict] = []
|
|
if not isinstance(plan_data, dict):
|
|
issues.append(_issue("PLAN_DATA_INVALID", "PlanData 必须是对象", "planData"))
|
|
return _result(issues)
|
|
|
|
for section in ("identity", "policy", "benefitScenarios", "assumptions"):
|
|
if section not in plan_data:
|
|
issues.append(_issue("PLAN_SECTION_MISSING", "PlanData 分区缺失", f"planData.{section}"))
|
|
if not isinstance(plan_data.get("identity"), dict):
|
|
issues.append(_issue("PLAN_SECTION_INVALID", "身份信息格式无效", "planData.identity"))
|
|
if not isinstance(plan_data.get("policy"), dict):
|
|
issues.append(_issue("PLAN_SECTION_INVALID", "保单信息格式无效", "planData.policy"))
|
|
if not isinstance(plan_data.get("benefitScenarios"), list):
|
|
issues.append(_issue("PLAN_SECTION_INVALID", "利益场景格式无效", "planData.benefitScenarios"))
|
|
|
|
field_values = list(iter_field_values(plan_data))
|
|
for path, value in field_values:
|
|
issues.extend(validate_field_value(value, f"planData.{path}"))
|
|
|
|
if for_confirmation:
|
|
issues.extend(_required_confirmation_fields(plan_data))
|
|
for path, value in field_values:
|
|
if value.get("status") == "conflict":
|
|
issues.append(_issue("UNRESOLVED_CONFLICT", "字段冲突尚未解决", f"planData.{path}"))
|
|
if _is_exported_amount(path, value) and not _has_traceable_source(value):
|
|
issues.append(_issue(
|
|
"CRITICAL_EVIDENCE_MISSING",
|
|
"导出金额缺少 PDF 证据或人工覆盖原因",
|
|
f"planData.{path}",
|
|
severity="warning",
|
|
))
|
|
return _result(issues)
|
|
|
|
|
|
def iter_field_values(value, prefix: str = ""):
|
|
if isinstance(value, dict):
|
|
if "status" in value and "value" in value and "evidence" in value:
|
|
yield prefix, value
|
|
return
|
|
for key, item in value.items():
|
|
child = f"{prefix}.{key}" if prefix else str(key)
|
|
yield from iter_field_values(item, child)
|
|
elif isinstance(value, list):
|
|
for index, item in enumerate(value):
|
|
child = f"{prefix}.{index}" if prefix else str(index)
|
|
yield from iter_field_values(item, child)
|
|
|
|
|
|
def _required_confirmation_fields(plan_data: dict) -> list[dict]:
|
|
required = (
|
|
("identity.insuredAge", (plan_data.get("identity") or {}).get("insuredAge")),
|
|
("identity.insuredGender", (plan_data.get("identity") or {}).get("insuredGender")),
|
|
("policy.currency", (plan_data.get("policy") or {}).get("currency")),
|
|
)
|
|
issues = []
|
|
for path, field in required:
|
|
if not isinstance(field, dict) or field.get("status") == "missing" or field.get("value") in (None, ""):
|
|
issues.append(_issue("REQUIRED_FIELD_MISSING", "确认所需字段缺失", f"planData.{path}"))
|
|
return issues
|
|
|
|
|
|
def _is_exported_amount(path: str, field: dict) -> bool:
|
|
if field.get("status") in {"missing", "derived"} or field.get("value") is None:
|
|
return False
|
|
if path not in CRITICAL_AMOUNT_PATHS:
|
|
return False
|
|
token = path.replace("_", "").lower()
|
|
if any(non_amount in token for non_amount in ("period", "year", "age", "label", "source")):
|
|
return False
|
|
return any(item in token for item in AMOUNT_TOKENS)
|
|
|
|
|
|
def _has_traceable_source(field: dict) -> bool:
|
|
if field.get("status") == "confirmed" and str(field.get("overrideReason") or "").strip():
|
|
return True
|
|
return any(
|
|
isinstance(item, dict)
|
|
and item.get("documentId")
|
|
and item.get("pageNumber")
|
|
and item.get("bbox")
|
|
for item in field.get("evidence") or []
|
|
)
|
|
|
|
|
|
def _result(issues: list[dict]) -> dict:
|
|
blocking = sum(1 for item in issues if item.get("severity") == "error")
|
|
warnings = sum(1 for item in issues if item.get("severity") == "warning")
|
|
conflicts = sum(1 for item in issues if item.get("code") == "UNRESOLVED_CONFLICT")
|
|
return {
|
|
"blockingCount": blocking,
|
|
"warningCount": warnings,
|
|
"conflictCount": conflicts,
|
|
"issues": issues,
|
|
}
|
|
|
|
|
|
def _issue(code: str, message: str, path: str, severity: str = "error") -> dict:
|
|
return {"code": code, "message": message, "path": path, "severity": severity}
|