test_pl_vehicle_recharge_workflow.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. """车辆充值审批工作流:状态常量与核心流转单元测试。"""
  2. from __future__ import annotations
  3. from datetime import date, datetime
  4. from decimal import Decimal
  5. from types import SimpleNamespace
  6. import pytest
  7. from sqlalchemy import create_engine
  8. from sqlalchemy.orm import sessionmaker
  9. from app.models.bx_models import BxWorkflowDef, BxWorkflowNode
  10. from app.models.pl_models import (
  11. PlVehicle,
  12. PlVehicleRechargeApply,
  13. PlVehicleRechargeApproval,
  14. PlVehicleRechargeRecord,
  15. )
  16. from app.schemas.pl.vehicle_recharge import APPLY_STATUSES, NODE_ACTION_TYPES, RECHARGE_TYPES
  17. def test_apply_statuses_include_workflow_states():
  18. assert {"draft", "in_approval", "rejected", "disbursed", "cancelled"} <= APPLY_STATUSES
  19. assert {"approve", "disburse"} <= NODE_ACTION_TYPES
  20. assert {"etc", "fuel"} <= RECHARGE_TYPES
  21. def _make_session():
  22. engine = create_engine("sqlite://")
  23. for table in (
  24. PlVehicle.__table__,
  25. PlVehicleRechargeApply.__table__,
  26. PlVehicleRechargeApproval.__table__,
  27. PlVehicleRechargeRecord.__table__,
  28. BxWorkflowDef.__table__,
  29. BxWorkflowNode.__table__,
  30. ):
  31. table.create(engine)
  32. return sessionmaker(bind=engine)()
  33. def _user(uid: str = "u-applicant", name: str = "申请人") -> SimpleNamespace:
  34. return SimpleNamespace(id=uid, name=name, dept_id="d1", role="admin", tenant_no="t1")
  35. def _seed_vehicle(db, plate: str = "鄂A00001") -> None:
  36. db.add(
  37. PlVehicle(
  38. id="veh-1",
  39. vehicle_type="self",
  40. license_plate=plate,
  41. tenant_no="t1",
  42. create_dept_id="d1",
  43. )
  44. )
  45. def _seed_workflow(db) -> tuple[str, str, str]:
  46. wf_id = "wf-1"
  47. node_gm = "node-gm"
  48. node_fin = "node-fin"
  49. db.add(
  50. BxWorkflowDef(
  51. id=wf_id,
  52. biz_type="vehicle_recharge",
  53. wf_name="充值流程",
  54. status=1,
  55. tenant_no="t1",
  56. create_dept_id="d1",
  57. )
  58. )
  59. db.add(
  60. BxWorkflowNode(
  61. id=node_gm,
  62. wf_id=wf_id,
  63. node_name="总经理审批",
  64. node_order=1,
  65. approver_id="u-gm",
  66. approver_name="总经理",
  67. action_type="approve",
  68. tenant_no="t1",
  69. create_dept_id="d1",
  70. )
  71. )
  72. db.add(
  73. BxWorkflowNode(
  74. id=node_fin,
  75. wf_id=wf_id,
  76. node_name="财务发放",
  77. node_order=2,
  78. approver_id="u-fin",
  79. approver_name="财务",
  80. action_type="disburse",
  81. tenant_no="t1",
  82. create_dept_id="d1",
  83. )
  84. )
  85. return wf_id, node_gm, node_fin
  86. def _patch_scope(monkeypatch, *modules) -> None:
  87. for mod in modules:
  88. monkeypatch.setattr(mod, "apply_pl_scope", lambda query, *_a, **_k: query)
  89. monkeypatch.setattr(mod, "assert_pl_row_in_scope", lambda *_a, **_k: None)
  90. from app.services import pl_vehicle_recharge_service as svc
  91. monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "测试用户")
  92. monkeypatch.setattr(
  93. svc, "_resolve_applicant_id", lambda *_a, **_k: "u-applicant"
  94. )
  95. def test_submit_sets_in_approval_and_first_node(monkeypatch):
  96. from app.services import pl_vehicle_recharge_service as svc
  97. from app.schemas.pl.vehicle_recharge import VehicleRechargeApplyCreate
  98. db = _make_session()
  99. _seed_vehicle(db)
  100. wf_id, node_gm, _node_fin = _seed_workflow(db)
  101. db.commit()
  102. _patch_scope(monkeypatch, svc)
  103. user = _user()
  104. created = svc.create_apply(
  105. db,
  106. "t1",
  107. user,
  108. VehicleRechargeApplyCreate(
  109. licensePlate="鄂A00001",
  110. rechargeType="etc",
  111. applyDate=date(2026, 9, 26),
  112. ),
  113. )
  114. assert created.status == "draft"
  115. assert created.approval_round == 0
  116. result = svc.submit_apply(db, "t1", user, created.id)
  117. assert result.status == "in_approval"
  118. assert result.approval_round == 1
  119. assert result.wf_id == wf_id
  120. assert result.current_node == node_gm
  121. def test_resubmit_increments_round_keeps_history(monkeypatch):
  122. from app.services import pl_vehicle_recharge_service as svc
  123. db = _make_session()
  124. _seed_vehicle(db)
  125. wf_id, node_gm, _node_fin = _seed_workflow(db)
  126. apply = PlVehicleRechargeApply(
  127. id="apply-1",
  128. apply_no="ETC2026092601",
  129. license_plate="鄂A00001",
  130. recharge_type="etc",
  131. apply_date=date(2026, 9, 26),
  132. applicant_id="u-applicant",
  133. status="rejected",
  134. wf_id=wf_id,
  135. current_node=None,
  136. approval_round=1,
  137. tenant_no="t1",
  138. create_dept_id="d1",
  139. )
  140. db.add(apply)
  141. db.add(
  142. PlVehicleRechargeApproval(
  143. id="appr-1",
  144. apply_id="apply-1",
  145. approval_round=1,
  146. node_id=node_gm,
  147. node_name="总经理审批",
  148. approver_id="u-gm",
  149. approver_name="总经理",
  150. approval_status="rejected",
  151. approval_opinion="驳回",
  152. approval_time=datetime(2026, 9, 26, 10, 0, 0),
  153. tenant_no="t1",
  154. create_dept_id="d1",
  155. )
  156. )
  157. db.commit()
  158. _patch_scope(monkeypatch, svc)
  159. result = svc.submit_apply(db, "t1", _user(), "apply-1")
  160. assert result.approval_round == 2
  161. assert result.status == "in_approval"
  162. assert result.current_node == node_gm
  163. assert len(result.approvals) == 1
  164. assert result.approvals[0].approval_status == "rejected"
  165. def test_approve_advances_to_next_node(monkeypatch):
  166. from app.services import pl_vehicle_recharge_approval_service as approval_svc
  167. from app.services import pl_vehicle_recharge_service as svc
  168. db = _make_session()
  169. _seed_vehicle(db)
  170. wf_id, node_gm, node_fin = _seed_workflow(db)
  171. db.add(
  172. PlVehicleRechargeApply(
  173. id="apply-1",
  174. apply_no="ETC2026092601",
  175. license_plate="鄂A00001",
  176. recharge_type="etc",
  177. apply_date=date(2026, 9, 26),
  178. applicant_id="u-applicant",
  179. status="in_approval",
  180. wf_id=wf_id,
  181. current_node=node_gm,
  182. approval_round=1,
  183. tenant_no="t1",
  184. create_dept_id="d1",
  185. )
  186. )
  187. db.commit()
  188. _patch_scope(monkeypatch, svc)
  189. result = approval_svc.approve_apply(
  190. db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="同意"
  191. )
  192. assert result.status == "in_approval"
  193. assert result.current_node == node_fin
  194. assert any(a.approval_status == "approved" for a in result.approvals)
  195. def test_reject_sets_rejected_and_appends_history(monkeypatch):
  196. from app.services import pl_vehicle_recharge_approval_service as approval_svc
  197. from app.services import pl_vehicle_recharge_service as svc
  198. db = _make_session()
  199. _seed_vehicle(db)
  200. wf_id, node_gm, _node_fin = _seed_workflow(db)
  201. db.add(
  202. PlVehicleRechargeApply(
  203. id="apply-1",
  204. apply_no="ETC2026092601",
  205. license_plate="鄂A00001",
  206. recharge_type="etc",
  207. apply_date=date(2026, 9, 26),
  208. applicant_id="u-applicant",
  209. status="in_approval",
  210. wf_id=wf_id,
  211. current_node=node_gm,
  212. approval_round=1,
  213. tenant_no="t1",
  214. create_dept_id="d1",
  215. )
  216. )
  217. db.commit()
  218. _patch_scope(monkeypatch, svc)
  219. result = approval_svc.reject_apply(
  220. db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="材料不全"
  221. )
  222. assert result.status == "rejected"
  223. assert result.current_node is None
  224. assert result.approvals[-1].approval_status == "rejected"
  225. def test_disburse_writes_record_and_disbursed(monkeypatch):
  226. from app.services import pl_vehicle_recharge_approval_service as approval_svc
  227. from app.services import pl_vehicle_recharge_service as svc
  228. from app.schemas.pl.vehicle_recharge import VehicleRechargeRecordCreate
  229. db = _make_session()
  230. _seed_vehicle(db)
  231. wf_id, _node_gm, node_fin = _seed_workflow(db)
  232. db.add(
  233. PlVehicleRechargeApply(
  234. id="apply-1",
  235. apply_no="ETC2026092601",
  236. license_plate="鄂A00001",
  237. recharge_type="etc",
  238. apply_date=date(2026, 9, 26),
  239. applicant_id="u-applicant",
  240. status="in_approval",
  241. wf_id=wf_id,
  242. current_node=node_fin,
  243. approval_round=1,
  244. tenant_no="t1",
  245. create_dept_id="d1",
  246. )
  247. )
  248. db.commit()
  249. _patch_scope(monkeypatch, svc)
  250. payload = VehicleRechargeRecordCreate(
  251. rechargeDate=date(2026, 9, 26),
  252. rechargeType="etc",
  253. amount=Decimal("100.00"),
  254. applyId="apply-1",
  255. )
  256. result = approval_svc.disburse_apply(
  257. db, "t1", _user("u-fin", "财务"), "apply-1", payload
  258. )
  259. assert result.status == "disbursed"
  260. assert result.record is not None
  261. assert result.record.amount == Decimal("100.00")
  262. assert any(a.approval_status == "disbursed" for a in result.approvals)
  263. def test_approve_forbidden_on_disburse_node(monkeypatch):
  264. from fastapi import HTTPException
  265. from app.services import pl_vehicle_recharge_approval_service as approval_svc
  266. from app.services import pl_vehicle_recharge_service as svc
  267. db = _make_session()
  268. _seed_vehicle(db)
  269. wf_id, _node_gm, node_fin = _seed_workflow(db)
  270. db.add(
  271. PlVehicleRechargeApply(
  272. id="apply-1",
  273. apply_no="ETC2026092601",
  274. license_plate="鄂A00001",
  275. recharge_type="etc",
  276. apply_date=date(2026, 9, 26),
  277. applicant_id="u-applicant",
  278. status="in_approval",
  279. wf_id=wf_id,
  280. current_node=node_fin,
  281. approval_round=1,
  282. tenant_no="t1",
  283. create_dept_id="d1",
  284. )
  285. )
  286. db.commit()
  287. _patch_scope(monkeypatch, svc)
  288. with pytest.raises(HTTPException) as exc:
  289. approval_svc.approve_apply(
  290. db, "t1", _user("u-fin", "财务"), "apply-1", opinion="ok"
  291. )
  292. assert exc.value.status_code == 400