100 lines
4.0 KiB
Python
100 lines
4.0 KiB
Python
"""缴费模式切换校验(与前端 protocol-pay-mode-switch.ts 规则一致)"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from app.services.protocol_payment_installment import is_legacy_installment_ratios
|
||
|
||
LOCKED_STATUSES = frozenset({"jfqrz", "yjf", "tfqrz", "ytf"})
|
||
BLOCKING_STATUSES = frozenset({"jfqrz", "tfqrz", "ytf"})
|
||
|
||
|
||
def _dec(v: Any) -> float:
|
||
if v is None:
|
||
return 0.0
|
||
return float(Decimal(str(v)))
|
||
|
||
|
||
def validate_payment_details_for_save(
|
||
pay_mode: str,
|
||
contract_amount: Optional[Any],
|
||
installment_count: Optional[int],
|
||
details: Optional[list],
|
||
installment_ratios: Optional[List[float]] = None,
|
||
installment_advance_days: Optional[int] = None,
|
||
) -> None:
|
||
"""保存缴费明细前的 P0 校验。"""
|
||
rows = [dict(d) for d in (details or [])]
|
||
contract = _dec(contract_amount)
|
||
|
||
for r in rows:
|
||
status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
|
||
if status in BLOCKING_STATUSES:
|
||
raise ValueError("存在缴费确认中或退费处理中的明细,无法保存")
|
||
|
||
paid_sum = sum(
|
||
_dec(r.get("due_amount") if r.get("due_amount") is not None else r.get("dueAmount"))
|
||
for r in rows
|
||
if (r.get("pay_status") or r.get("payStatus")) == "yjf"
|
||
)
|
||
dfj_rows = [
|
||
r
|
||
for r in rows
|
||
if (r.get("pay_status") or r.get("payStatus") or "dfj") == "dfj"
|
||
]
|
||
yjf_rows = [r for r in rows if (r.get("pay_status") or r.get("payStatus")) == "yjf"]
|
||
|
||
if pay_mode == "dj":
|
||
if len(dfj_rows) > 1:
|
||
raise ValueError("趸交模式仅允许一条待缴费明细")
|
||
remaining = round(contract - paid_sum, 2)
|
||
if dfj_rows:
|
||
due = _dec(
|
||
dfj_rows[0].get("due_amount")
|
||
if dfj_rows[0].get("due_amount") is not None
|
||
else dfj_rows[0].get("dueAmount")
|
||
)
|
||
expected = remaining if yjf_rows else contract
|
||
if abs(due - expected) > 0.015:
|
||
raise ValueError(
|
||
f"趸交待缴金额应为 {expected:.2f},当前为 {due:.2f}"
|
||
)
|
||
|
||
elif pay_mode == "fq":
|
||
count = int(installment_count or 0)
|
||
if count < 1:
|
||
raise ValueError("分期模式须填写分期期数")
|
||
if installment_advance_days is not None and int(installment_advance_days) < 0:
|
||
raise ValueError("应缴费提前天数不能为负数")
|
||
if installment_ratios is not None:
|
||
values = [float(x) for x in installment_ratios]
|
||
if len(values) != count:
|
||
raise ValueError("分期金额数量须与分期期数一致")
|
||
if is_legacy_installment_ratios(values, Decimal(str(contract)) if contract else None):
|
||
if abs(sum(values) - 100.0) > 0.01:
|
||
raise ValueError("缴费比例之和须等于 100%")
|
||
elif contract and abs(sum(values) - contract) > 0.015:
|
||
raise ValueError("分期金额合计须等于合同金额")
|
||
if len(rows) != count:
|
||
raise ValueError(f"分期明细条数应等于分期期数({count})")
|
||
fq_forbidden = frozenset({"jfqrz", "tfqrz", "ytf"})
|
||
for r in rows:
|
||
status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
|
||
if status in fq_forbidden:
|
||
raise ValueError("分期模式下存在不可保存的缴费状态")
|
||
total_due = sum(
|
||
_dec(r.get("due_amount") if r.get("due_amount") is not None else r.get("dueAmount"))
|
||
for r in rows
|
||
)
|
||
if contract and abs(total_due - contract) > 0.015:
|
||
raise ValueError("分期各期应缴金额之和应等于合同金额")
|
||
if yjf_rows:
|
||
max_paid_no = max(
|
||
int(r.get("installment_no") or r.get("installmentNo") or 0)
|
||
for r in yjf_rows
|
||
)
|
||
if max_paid_no > count:
|
||
raise ValueError("分期期数不能少于已缴费的最大期次")
|