protocol_pay_mode_switch.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. """缴费模式切换校验(与前端 protocol-pay-mode-switch.ts 规则一致)"""
  2. from __future__ import annotations
  3. from decimal import Decimal
  4. from typing import Any, Dict, List, Optional
  5. from app.services.protocol_payment_installment import is_legacy_installment_ratios
  6. LOCKED_STATUSES = frozenset({"jfqrz", "yjf", "tfqrz", "ytf"})
  7. # 仅阻断进行中的缴费/退费确认;已退费(ytf)为终态,允许随协议单保存(明细只读)
  8. BLOCKING_STATUSES = frozenset({"jfqrz", "tfqrz"})
  9. def _dec(v: Any) -> float:
  10. if v is None:
  11. return 0.0
  12. return float(Decimal(str(v)))
  13. def dj_due_amount_mismatch_hint(
  14. contract_amount: Optional[Any],
  15. details: Optional[list],
  16. ) -> Optional[str]:
  17. """趸交:应缴合计与合同金额不一致时返回提醒文案(不阻断保存)。"""
  18. rows = [dict(d) for d in (details or [])]
  19. contract = _dec(contract_amount)
  20. if not contract:
  21. return None
  22. total_due = sum(
  23. _dec(r.get("due_amount") if r.get("due_amount") is not None else r.get("dueAmount"))
  24. for r in rows
  25. )
  26. if abs(total_due - contract) > 0.015:
  27. return f"趸交各明细应缴金额之和应为 {contract:.2f},当前为 {total_due:.2f},请核对"
  28. return None
  29. def validate_payment_details_for_save(
  30. pay_mode: str,
  31. contract_amount: Optional[Any],
  32. installment_count: Optional[int],
  33. details: Optional[list],
  34. installment_ratios: Optional[List[float]] = None,
  35. installment_advance_days: Optional[int] = None,
  36. ) -> None:
  37. """保存缴费明细前的 P0 校验。
  38. 趸交应缴合计与合同金额不一致仅作前端提醒,不在此阻断;按用户填写原样保存。
  39. """
  40. rows = [dict(d) for d in (details or [])]
  41. contract = _dec(contract_amount)
  42. for r in rows:
  43. status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
  44. if status in BLOCKING_STATUSES:
  45. raise ValueError("存在缴费确认中或退费处理中的明细,无法保存")
  46. yjf_rows = [r for r in rows if (r.get("pay_status") or r.get("payStatus")) == "yjf"]
  47. if pay_mode in ("fq", "djfq"):
  48. count = int(installment_count or 0)
  49. if count < 1:
  50. raise ValueError("分期模式须填写分期期数")
  51. if installment_advance_days is not None and int(installment_advance_days) < 0:
  52. raise ValueError("应缴费提前天数不能为负数")
  53. if installment_ratios is not None:
  54. values = [float(x) for x in installment_ratios]
  55. if len(values) != count:
  56. raise ValueError("分期金额数量须与分期期数一致")
  57. if is_legacy_installment_ratios(values, Decimal(str(contract)) if contract else None):
  58. if abs(sum(values) - 100.0) > 0.01:
  59. raise ValueError("缴费比例之和须等于 100%")
  60. if len(rows) != count:
  61. raise ValueError(f"分期明细条数应等于分期期数({count})")
  62. fq_forbidden = frozenset({"jfqrz", "tfqrz"})
  63. for r in rows:
  64. status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
  65. if status in fq_forbidden:
  66. raise ValueError("分期模式下存在不可保存的缴费状态")
  67. if yjf_rows:
  68. max_paid_no = max(
  69. int(r.get("installment_no") or r.get("installmentNo") or 0)
  70. for r in yjf_rows
  71. )
  72. if max_paid_no > count:
  73. raise ValueError("分期期数不能少于已缴费的最大期次")