test_pl_vehicle_recharge_workflow.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  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. @pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"])
  96. def test_submit_sets_in_approval_and_first_node(monkeypatch, recharge_type):
  97. from app.services import pl_vehicle_recharge_service as svc
  98. from app.schemas.pl.vehicle_recharge import VehicleRechargeApplyCreate
  99. db = _make_session()
  100. _seed_vehicle(db)
  101. wf_id, node_gm, _node_fin = _seed_workflow(db)
  102. db.commit()
  103. _patch_scope(monkeypatch, svc)
  104. user = _user()
  105. created = svc.create_apply(
  106. db,
  107. "t1",
  108. user,
  109. VehicleRechargeApplyCreate(
  110. licensePlate="鄂A00001",
  111. rechargeType=recharge_type,
  112. applyDate=date(2026, 9, 26),
  113. odometerKm=Decimal("1000"),
  114. attachmentUrls="https://example.com/a.jpg",
  115. ),
  116. )
  117. assert created.recharge_type == recharge_type
  118. assert created.status == "draft"
  119. assert created.approval_round == 0
  120. result = svc.submit_apply(db, "t1", user, created.id)
  121. assert result.status == "in_approval"
  122. assert result.approval_round == 1
  123. assert result.wf_id == wf_id
  124. assert result.current_node == node_gm
  125. def test_resubmit_increments_round_keeps_history(monkeypatch):
  126. from app.services import pl_vehicle_recharge_service as svc
  127. db = _make_session()
  128. _seed_vehicle(db)
  129. wf_id, node_gm, _node_fin = _seed_workflow(db)
  130. apply = PlVehicleRechargeApply(
  131. id="apply-1",
  132. apply_no="ETC2026092601",
  133. license_plate="鄂A00001",
  134. recharge_type="etc",
  135. apply_date=date(2026, 9, 26),
  136. applicant_id="u-applicant",
  137. status="rejected",
  138. wf_id=wf_id,
  139. current_node=None,
  140. approval_round=1,
  141. tenant_no="t1",
  142. create_dept_id="d1",
  143. )
  144. db.add(apply)
  145. db.add(
  146. PlVehicleRechargeApproval(
  147. id="appr-1",
  148. apply_id="apply-1",
  149. approval_round=1,
  150. node_id=node_gm,
  151. node_name="总经理审批",
  152. approver_id="u-gm",
  153. approver_name="总经理",
  154. approval_status="rejected",
  155. approval_opinion="驳回",
  156. approval_time=datetime(2026, 9, 26, 10, 0, 0),
  157. tenant_no="t1",
  158. create_dept_id="d1",
  159. )
  160. )
  161. db.commit()
  162. _patch_scope(monkeypatch, svc)
  163. result = svc.submit_apply(db, "t1", _user(), "apply-1")
  164. assert result.approval_round == 2
  165. assert result.status == "in_approval"
  166. assert result.current_node == node_gm
  167. assert len(result.approvals) == 1
  168. assert result.approvals[0].approval_status == "rejected"
  169. # 当前轮进度:全部节点均展示,尚未审批则为 pending
  170. assert len(result.approval_flow) == 2
  171. assert all(n.approval_status == "pending" for n in result.approval_flow)
  172. # 历史:保留打回前的审批动作
  173. assert len(result.approval_history) == 1
  174. assert result.approval_history[0].approval_status == "rejected"
  175. assert result.approval_history[0].approval_round == 1
  176. def test_approve_advances_to_next_node(monkeypatch):
  177. from app.services import pl_vehicle_recharge_approval_service as approval_svc
  178. from app.services import pl_vehicle_recharge_service as svc
  179. db = _make_session()
  180. _seed_vehicle(db)
  181. wf_id, node_gm, node_fin = _seed_workflow(db)
  182. db.add(
  183. PlVehicleRechargeApply(
  184. id="apply-1",
  185. apply_no="ETC2026092601",
  186. license_plate="鄂A00001",
  187. recharge_type="etc",
  188. apply_date=date(2026, 9, 26),
  189. applicant_id="u-applicant",
  190. status="in_approval",
  191. wf_id=wf_id,
  192. current_node=node_gm,
  193. approval_round=1,
  194. tenant_no="t1",
  195. create_dept_id="d1",
  196. )
  197. )
  198. db.commit()
  199. _patch_scope(monkeypatch, svc)
  200. result = approval_svc.approve_apply(
  201. db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="同意"
  202. )
  203. assert result.status == "in_approval"
  204. assert result.current_node == node_fin
  205. assert any(a.approval_status == "approved" for a in result.approvals)
  206. assert len(result.approval_flow) == 2
  207. assert result.approval_flow[0].approval_status == "approved"
  208. assert result.approval_flow[0].approver_name == "总经理"
  209. assert result.approval_flow[0].approval_opinion == "同意"
  210. assert result.approval_flow[1].approval_status == "pending"
  211. assert result.approval_flow[1].node_name == "财务发放"
  212. assert len(result.approval_history) == 1
  213. assert result.approval_history[0].approval_status == "approved"
  214. def test_reject_sets_rejected_and_appends_history(monkeypatch):
  215. from app.services import pl_vehicle_recharge_approval_service as approval_svc
  216. from app.services import pl_vehicle_recharge_service as svc
  217. db = _make_session()
  218. _seed_vehicle(db)
  219. wf_id, node_gm, _node_fin = _seed_workflow(db)
  220. db.add(
  221. PlVehicleRechargeApply(
  222. id="apply-1",
  223. apply_no="ETC2026092601",
  224. license_plate="鄂A00001",
  225. recharge_type="etc",
  226. apply_date=date(2026, 9, 26),
  227. applicant_id="u-applicant",
  228. status="in_approval",
  229. wf_id=wf_id,
  230. current_node=node_gm,
  231. approval_round=1,
  232. tenant_no="t1",
  233. create_dept_id="d1",
  234. )
  235. )
  236. db.commit()
  237. _patch_scope(monkeypatch, svc)
  238. result = approval_svc.reject_apply(
  239. db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="材料不全"
  240. )
  241. assert result.status == "rejected"
  242. assert result.current_node is None
  243. assert result.approvals[-1].approval_status == "rejected"
  244. @pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"])
  245. def test_disburse_writes_record_and_disbursed(monkeypatch, recharge_type):
  246. from app.services import pl_vehicle_recharge_approval_service as approval_svc
  247. from app.services import pl_vehicle_recharge_service as svc
  248. from app.schemas.pl.vehicle_recharge import VehicleRechargeRecordCreate
  249. db = _make_session()
  250. _seed_vehicle(db)
  251. wf_id, _node_gm, node_fin = _seed_workflow(db)
  252. db.add(
  253. PlVehicleRechargeApply(
  254. id="apply-1",
  255. apply_no="ETC2026092601",
  256. license_plate="鄂A00001",
  257. recharge_type=recharge_type,
  258. apply_date=date(2026, 9, 26),
  259. applicant_id="u-applicant",
  260. status="in_approval",
  261. wf_id=wf_id,
  262. current_node=node_fin,
  263. approval_round=1,
  264. tenant_no="t1",
  265. create_dept_id="d1",
  266. )
  267. )
  268. db.commit()
  269. _patch_scope(monkeypatch, svc)
  270. payload = VehicleRechargeRecordCreate(
  271. rechargeDate=date(2026, 9, 26),
  272. rechargeType=recharge_type,
  273. amount=Decimal("100.00"),
  274. applyId="apply-1",
  275. )
  276. result = approval_svc.disburse_apply(
  277. db, "t1", _user("u-fin", "财务"), "apply-1", payload
  278. )
  279. assert result.status == "disbursed"
  280. assert result.record is not None
  281. assert result.record.recharge_type == recharge_type
  282. assert result.record.amount == Decimal("100.00")
  283. assert any(a.approval_status == "disbursed" for a in result.approvals)
  284. def test_approve_forbidden_on_disburse_node(monkeypatch):
  285. from fastapi import HTTPException
  286. from app.services import pl_vehicle_recharge_approval_service as approval_svc
  287. from app.services import pl_vehicle_recharge_service as svc
  288. db = _make_session()
  289. _seed_vehicle(db)
  290. wf_id, _node_gm, node_fin = _seed_workflow(db)
  291. db.add(
  292. PlVehicleRechargeApply(
  293. id="apply-1",
  294. apply_no="ETC2026092601",
  295. license_plate="鄂A00001",
  296. recharge_type="etc",
  297. apply_date=date(2026, 9, 26),
  298. applicant_id="u-applicant",
  299. status="in_approval",
  300. wf_id=wf_id,
  301. current_node=node_fin,
  302. approval_round=1,
  303. tenant_no="t1",
  304. create_dept_id="d1",
  305. )
  306. )
  307. db.commit()
  308. _patch_scope(monkeypatch, svc)
  309. with pytest.raises(HTTPException) as exc:
  310. approval_svc.approve_apply(
  311. db, "t1", _user("u-fin", "财务"), "apply-1", opinion="ok"
  312. )
  313. assert exc.value.status_code == 400
  314. def test_withdraw_in_approval_to_draft_keeps_round_and_approvals(monkeypatch):
  315. from app.services import pl_vehicle_recharge_service as svc
  316. db = _make_session()
  317. _seed_vehicle(db)
  318. wf_id, node_gm, _node_fin = _seed_workflow(db)
  319. db.add(
  320. PlVehicleRechargeApply(
  321. id="a-wd",
  322. apply_no="SXSQWD001",
  323. license_plate="鄂A00001",
  324. recharge_type="etc",
  325. apply_date=date.today(),
  326. applicant_id="u-applicant",
  327. status="in_approval",
  328. wf_id=wf_id,
  329. current_node=node_gm,
  330. approval_round=1,
  331. tenant_no="t1",
  332. create_dept_id="d1",
  333. )
  334. )
  335. db.add(
  336. PlVehicleRechargeApproval(
  337. id="ap1",
  338. apply_id="a-wd",
  339. node_id=node_gm,
  340. node_name="总经理审批",
  341. approval_round=1,
  342. approval_status="approved",
  343. approver_id="u-gm",
  344. approver_name="总经理",
  345. approval_time=datetime.now(),
  346. tenant_no="t1",
  347. create_dept_id="d1",
  348. )
  349. )
  350. db.commit()
  351. _patch_scope(monkeypatch, svc)
  352. out = svc.withdraw_apply(db, "t1", _user("u-applicant"), "a-wd")
  353. assert out.status == "draft"
  354. assert out.current_node in (None, "")
  355. row = db.query(PlVehicleRechargeApply).filter_by(id="a-wd").one()
  356. assert row.status == "draft"
  357. assert row.current_node is None
  358. assert row.approval_round == 1
  359. assert (
  360. db.query(PlVehicleRechargeApproval)
  361. .filter_by(apply_id="a-wd", del_flag=0)
  362. .count()
  363. == 1
  364. )
  365. def test_withdraw_rejects_non_applicant(monkeypatch):
  366. from fastapi import HTTPException
  367. from app.services import pl_vehicle_recharge_service as svc
  368. db = _make_session()
  369. _seed_vehicle(db)
  370. db.add(
  371. PlVehicleRechargeApply(
  372. id="a-wd2",
  373. apply_no="SXSQWD002",
  374. license_plate="鄂A00001",
  375. recharge_type="etc",
  376. apply_date=date.today(),
  377. applicant_id="u-applicant",
  378. status="in_approval",
  379. tenant_no="t1",
  380. create_dept_id="d1",
  381. )
  382. )
  383. db.commit()
  384. _patch_scope(monkeypatch, svc)
  385. with pytest.raises(HTTPException) as ei:
  386. svc.withdraw_apply(db, "t1", _user("u-other"), "a-wd2")
  387. assert ei.value.status_code == 403
  388. def test_withdraw_rejects_non_in_approval(monkeypatch):
  389. from fastapi import HTTPException
  390. from app.services import pl_vehicle_recharge_service as svc
  391. db = _make_session()
  392. _seed_vehicle(db)
  393. db.add(
  394. PlVehicleRechargeApply(
  395. id="a-wd3",
  396. apply_no="SXSQWD003",
  397. license_plate="鄂A00001",
  398. recharge_type="etc",
  399. apply_date=date.today(),
  400. applicant_id="u-applicant",
  401. status="draft",
  402. tenant_no="t1",
  403. create_dept_id="d1",
  404. )
  405. )
  406. db.commit()
  407. _patch_scope(monkeypatch, svc)
  408. with pytest.raises(HTTPException) as ei:
  409. svc.withdraw_apply(db, "t1", _user("u-applicant"), "a-wd3")
  410. assert ei.value.status_code == 400