Files
zkxw/backend/app/services/protocol_pay_mode_switch.py
T
2026-06-15 11:43:16 +08:00

100 lines
4.0 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"})
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("分期期数不能少于已缴费的最大期次")