protocol_pay_mode_switch.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. """缴费模式切换校验(与前端 protocol-pay-mode-switch.ts 规则一致)"""
  2. from __future__ import annotations
  3. from decimal import Decimal
  4. from typing import Any, Dict, Optional
  5. LOCKED_STATUSES = frozenset({"jfqrz", "yjf", "tfqrz", "ytf"})
  6. BLOCKING_STATUSES = frozenset({"jfqrz", "tfqrz", "ytf"})
  7. def _dec(v: Any) -> float:
  8. if v is None:
  9. return 0.0
  10. return float(Decimal(str(v)))
  11. def validate_payment_details_for_save(
  12. pay_mode: str,
  13. contract_amount: Optional[Any],
  14. installment_count: Optional[int],
  15. details: Optional[list],
  16. ) -> None:
  17. """保存缴费明细前的 P0 校验。"""
  18. rows = [dict(d) for d in (details or [])]
  19. contract = _dec(contract_amount)
  20. for r in rows:
  21. status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
  22. if status in BLOCKING_STATUSES:
  23. raise ValueError("存在缴费确认中或退费处理中的明细,无法保存")
  24. paid_sum = sum(
  25. _dec(r.get("due_amount") if r.get("due_amount") is not None else r.get("dueAmount"))
  26. for r in rows
  27. if (r.get("pay_status") or r.get("payStatus")) == "yjf"
  28. )
  29. dfj_rows = [
  30. r
  31. for r in rows
  32. if (r.get("pay_status") or r.get("payStatus") or "dfj") == "dfj"
  33. ]
  34. yjf_rows = [r for r in rows if (r.get("pay_status") or r.get("payStatus")) == "yjf"]
  35. if pay_mode == "dj":
  36. if len(dfj_rows) > 1:
  37. raise ValueError("趸交模式仅允许一条待缴费明细")
  38. remaining = round(contract - paid_sum, 2)
  39. if dfj_rows:
  40. due = _dec(
  41. dfj_rows[0].get("due_amount")
  42. if dfj_rows[0].get("due_amount") is not None
  43. else dfj_rows[0].get("dueAmount")
  44. )
  45. expected = remaining if yjf_rows else contract
  46. if abs(due - expected) > 0.015:
  47. raise ValueError(
  48. f"趸交待缴金额应为 {expected:.2f},当前为 {due:.2f}"
  49. )
  50. elif pay_mode == "fq":
  51. count = int(installment_count or 0)
  52. if count < 1:
  53. raise ValueError("分期模式须填写分期期数")
  54. if len(rows) != count:
  55. raise ValueError(f"分期明细条数应等于分期期数({count})")
  56. fq_forbidden = frozenset({"jfqrz", "tfqrz", "ytf"})
  57. for r in rows:
  58. status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
  59. if status in fq_forbidden:
  60. raise ValueError("分期模式下存在不可保存的缴费状态")
  61. total_due = sum(
  62. _dec(r.get("due_amount") if r.get("due_amount") is not None else r.get("dueAmount"))
  63. for r in rows
  64. )
  65. if contract and abs(total_due - contract) > 0.015:
  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("分期期数不能少于已缴费的最大期次")