Files
zkxw/backend/app/services/protocol_pay_mode_switch.py
T
2026-07-06 19:41:24 +08:00

87 lines
3.4 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.
"""缴费模式切换校验(与前端 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"})
# 仅阻断进行中的缴费/退费确认;已退费(ytf)为终态,允许随协议单保存(明细只读)
BLOCKING_STATUSES = frozenset({"jfqrz", "tfqrz"})
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":
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(
f"趸交各明细应缴金额之和应为 {contract:.2f},当前为 {total_due:.2f}"
)
elif pay_mode in ("fq", "djfq"):
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%")
if len(rows) != count:
raise ValueError(f"分期明细条数应等于分期期数({count})")
fq_forbidden = frozenset({"jfqrz", "tfqrz"})
for r in rows:
status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
if status in fq_forbidden:
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("分期期数不能少于已缴费的最大期次")