| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192 |
- """缴费模式切换校验(与前端 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) -> Decimal:
- if v is None:
- return Decimal("0")
- return Decimal(str(v))
- def dj_due_amount_mismatch_hint(
- contract_amount: Optional[Any],
- details: Optional[list],
- ) -> Optional[str]:
- """趸交:应缴合计与合同金额不一致时返回提醒文案(不阻断保存)。"""
- rows = [dict(d) for d in (details or [])]
- contract = _dec(contract_amount)
- if not contract:
- return None
- 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 abs(total_due - contract) > Decimal("0.015"):
- return f"趸交各明细应缴金额之和应为 {contract:.2f},当前为 {total_due:.2f},请核对"
- return None
- 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,
- locked_rows_are_read_only: bool = False,
- ) -> None:
- """保存缴费明细前的 P0 校验。
- 趸交应缴合计与合同金额不一致仅作前端提醒,不在此阻断;按用户填写原样保存。
- """
- if pay_mode not in {'dj', 'fq', 'djfq', 'fgd'}:
- raise ValueError('不支持的缴费模式')
- rows = [dict(d) for d in (details or [])]
- contract = _dec(contract_amount)
- if not locked_rows_are_read_only:
- for r in rows:
- status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
- if status in BLOCKING_STATUSES:
- raise ValueError("存在付款确认中或退费处理中的明细,无法保存")
- yjf_rows = [r for r in rows if (r.get("pay_status") or r.get("payStatus")) == "yjf"]
- if pay_mode in ("fq", "djfq", "fgd"):
- 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 = [Decimal(str(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) - Decimal("100")) > Decimal("0.01"):
- raise ValueError("缴费比例之和须等于 100%")
- if len(rows) != count:
- raise ValueError(f"分期明细条数应等于分期期数({count})")
- if not locked_rows_are_read_only:
- 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("分期期数不能少于已缴费的最大期次")
|