"""车辆充值审批工作流:状态常量与核心流转单元测试。""" from __future__ import annotations from datetime import date, datetime from decimal import Decimal from types import SimpleNamespace import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.models.bx_models import BxWorkflowDef, BxWorkflowNode from app.models.pl_models import ( PlVehicle, PlVehicleRechargeApply, PlVehicleRechargeApproval, PlVehicleRechargeRecord, ) from app.schemas.pl.vehicle_recharge import APPLY_STATUSES, NODE_ACTION_TYPES, RECHARGE_TYPES def test_apply_statuses_include_workflow_states(): assert {"draft", "in_approval", "rejected", "disbursed", "cancelled"} <= APPLY_STATUSES assert {"approve", "disburse"} <= NODE_ACTION_TYPES assert {"etc", "fuel"} <= RECHARGE_TYPES def _make_session(): engine = create_engine("sqlite://") for table in ( PlVehicle.__table__, PlVehicleRechargeApply.__table__, PlVehicleRechargeApproval.__table__, PlVehicleRechargeRecord.__table__, BxWorkflowDef.__table__, BxWorkflowNode.__table__, ): table.create(engine) return sessionmaker(bind=engine)() def _user(uid: str = "u-applicant", name: str = "申请人") -> SimpleNamespace: return SimpleNamespace(id=uid, name=name, dept_id="d1", role="admin", tenant_no="t1") def _seed_vehicle(db, plate: str = "鄂A00001") -> None: db.add( PlVehicle( id="veh-1", vehicle_type="self", license_plate=plate, tenant_no="t1", create_dept_id="d1", ) ) def _seed_workflow(db) -> tuple[str, str, str]: wf_id = "wf-1" node_gm = "node-gm" node_fin = "node-fin" db.add( BxWorkflowDef( id=wf_id, biz_type="vehicle_recharge", wf_name="充值流程", status=1, tenant_no="t1", create_dept_id="d1", ) ) db.add( BxWorkflowNode( id=node_gm, wf_id=wf_id, node_name="总经理审批", node_order=1, approver_id="u-gm", approver_name="总经理", action_type="approve", tenant_no="t1", create_dept_id="d1", ) ) db.add( BxWorkflowNode( id=node_fin, wf_id=wf_id, node_name="财务发放", node_order=2, approver_id="u-fin", approver_name="财务", action_type="disburse", tenant_no="t1", create_dept_id="d1", ) ) return wf_id, node_gm, node_fin def _patch_scope(monkeypatch, *modules) -> None: for mod in modules: monkeypatch.setattr(mod, "apply_pl_scope", lambda query, *_a, **_k: query) monkeypatch.setattr(mod, "assert_pl_row_in_scope", lambda *_a, **_k: None) from app.services import pl_vehicle_recharge_service as svc monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "测试用户") monkeypatch.setattr( svc, "_resolve_applicant_id", lambda *_a, **_k: "u-applicant" ) @pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"]) def test_submit_sets_in_approval_and_first_node(monkeypatch, recharge_type): from app.services import pl_vehicle_recharge_service as svc from app.schemas.pl.vehicle_recharge import VehicleRechargeApplyCreate db = _make_session() _seed_vehicle(db) wf_id, node_gm, _node_fin = _seed_workflow(db) db.commit() _patch_scope(monkeypatch, svc) user = _user() created = svc.create_apply( db, "t1", user, VehicleRechargeApplyCreate( licensePlate="鄂A00001", rechargeType=recharge_type, applyDate=date(2026, 9, 26), odometerKm=Decimal("1000"), attachmentUrls="https://example.com/a.jpg", ), ) assert created.recharge_type == recharge_type assert created.status == "draft" assert created.approval_round == 0 result = svc.submit_apply(db, "t1", user, created.id) assert result.status == "in_approval" assert result.approval_round == 1 assert result.wf_id == wf_id assert result.current_node == node_gm def test_resubmit_increments_round_keeps_history(monkeypatch): from app.services import pl_vehicle_recharge_service as svc db = _make_session() _seed_vehicle(db) wf_id, node_gm, _node_fin = _seed_workflow(db) apply = PlVehicleRechargeApply( id="apply-1", apply_no="ETC2026092601", license_plate="鄂A00001", recharge_type="etc", apply_date=date(2026, 9, 26), applicant_id="u-applicant", status="rejected", wf_id=wf_id, current_node=None, approval_round=1, tenant_no="t1", create_dept_id="d1", ) db.add(apply) db.add( PlVehicleRechargeApproval( id="appr-1", apply_id="apply-1", approval_round=1, node_id=node_gm, node_name="总经理审批", approver_id="u-gm", approver_name="总经理", approval_status="rejected", approval_opinion="驳回", approval_time=datetime(2026, 9, 26, 10, 0, 0), tenant_no="t1", create_dept_id="d1", ) ) db.commit() _patch_scope(monkeypatch, svc) result = svc.submit_apply(db, "t1", _user(), "apply-1") assert result.approval_round == 2 assert result.status == "in_approval" assert result.current_node == node_gm assert len(result.approvals) == 1 assert result.approvals[0].approval_status == "rejected" # 当前轮进度:全部节点均展示,尚未审批则为 pending assert len(result.approval_flow) == 2 assert all(n.approval_status == "pending" for n in result.approval_flow) # 历史:保留打回前的审批动作 assert len(result.approval_history) == 1 assert result.approval_history[0].approval_status == "rejected" assert result.approval_history[0].approval_round == 1 def test_approve_advances_to_next_node(monkeypatch): from app.services import pl_vehicle_recharge_approval_service as approval_svc from app.services import pl_vehicle_recharge_service as svc db = _make_session() _seed_vehicle(db) wf_id, node_gm, node_fin = _seed_workflow(db) db.add( PlVehicleRechargeApply( id="apply-1", apply_no="ETC2026092601", license_plate="鄂A00001", recharge_type="etc", apply_date=date(2026, 9, 26), applicant_id="u-applicant", status="in_approval", wf_id=wf_id, current_node=node_gm, approval_round=1, tenant_no="t1", create_dept_id="d1", ) ) db.commit() _patch_scope(monkeypatch, svc) result = approval_svc.approve_apply( db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="同意" ) assert result.status == "in_approval" assert result.current_node == node_fin assert any(a.approval_status == "approved" for a in result.approvals) assert len(result.approval_flow) == 2 assert result.approval_flow[0].approval_status == "approved" assert result.approval_flow[0].approver_name == "总经理" assert result.approval_flow[0].approval_opinion == "同意" assert result.approval_flow[1].approval_status == "pending" assert result.approval_flow[1].node_name == "财务发放" assert len(result.approval_history) == 1 assert result.approval_history[0].approval_status == "approved" def test_reject_sets_rejected_and_appends_history(monkeypatch): from app.services import pl_vehicle_recharge_approval_service as approval_svc from app.services import pl_vehicle_recharge_service as svc db = _make_session() _seed_vehicle(db) wf_id, node_gm, _node_fin = _seed_workflow(db) db.add( PlVehicleRechargeApply( id="apply-1", apply_no="ETC2026092601", license_plate="鄂A00001", recharge_type="etc", apply_date=date(2026, 9, 26), applicant_id="u-applicant", status="in_approval", wf_id=wf_id, current_node=node_gm, approval_round=1, tenant_no="t1", create_dept_id="d1", ) ) db.commit() _patch_scope(monkeypatch, svc) result = approval_svc.reject_apply( db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="材料不全" ) assert result.status == "rejected" assert result.current_node is None assert result.approvals[-1].approval_status == "rejected" @pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"]) def test_disburse_writes_record_and_disbursed(monkeypatch, recharge_type): from app.services import pl_vehicle_recharge_approval_service as approval_svc from app.services import pl_vehicle_recharge_service as svc from app.schemas.pl.vehicle_recharge import VehicleRechargeRecordCreate db = _make_session() _seed_vehicle(db) wf_id, _node_gm, node_fin = _seed_workflow(db) db.add( PlVehicleRechargeApply( id="apply-1", apply_no="ETC2026092601", license_plate="鄂A00001", recharge_type=recharge_type, apply_date=date(2026, 9, 26), applicant_id="u-applicant", status="in_approval", wf_id=wf_id, current_node=node_fin, approval_round=1, tenant_no="t1", create_dept_id="d1", ) ) db.commit() _patch_scope(monkeypatch, svc) payload = VehicleRechargeRecordCreate( rechargeDate=date(2026, 9, 26), rechargeType=recharge_type, amount=Decimal("100.00"), applyId="apply-1", ) result = approval_svc.disburse_apply( db, "t1", _user("u-fin", "财务"), "apply-1", payload ) assert result.status == "disbursed" assert result.record is not None assert result.record.recharge_type == recharge_type assert result.record.amount == Decimal("100.00") assert any(a.approval_status == "disbursed" for a in result.approvals) def test_approve_forbidden_on_disburse_node(monkeypatch): from fastapi import HTTPException from app.services import pl_vehicle_recharge_approval_service as approval_svc from app.services import pl_vehicle_recharge_service as svc db = _make_session() _seed_vehicle(db) wf_id, _node_gm, node_fin = _seed_workflow(db) db.add( PlVehicleRechargeApply( id="apply-1", apply_no="ETC2026092601", license_plate="鄂A00001", recharge_type="etc", apply_date=date(2026, 9, 26), applicant_id="u-applicant", status="in_approval", wf_id=wf_id, current_node=node_fin, approval_round=1, tenant_no="t1", create_dept_id="d1", ) ) db.commit() _patch_scope(monkeypatch, svc) with pytest.raises(HTTPException) as exc: approval_svc.approve_apply( db, "t1", _user("u-fin", "财务"), "apply-1", opinion="ok" ) assert exc.value.status_code == 400 def test_withdraw_in_approval_to_draft_keeps_round_and_approvals(monkeypatch): from app.services import pl_vehicle_recharge_service as svc db = _make_session() _seed_vehicle(db) wf_id, node_gm, _node_fin = _seed_workflow(db) db.add( PlVehicleRechargeApply( id="a-wd", apply_no="SXSQWD001", license_plate="鄂A00001", recharge_type="etc", apply_date=date.today(), applicant_id="u-applicant", status="in_approval", wf_id=wf_id, current_node=node_gm, approval_round=1, tenant_no="t1", create_dept_id="d1", ) ) db.add( PlVehicleRechargeApproval( id="ap1", apply_id="a-wd", node_id=node_gm, node_name="总经理审批", approval_round=1, approval_status="approved", approver_id="u-gm", approver_name="总经理", approval_time=datetime.now(), tenant_no="t1", create_dept_id="d1", ) ) db.commit() _patch_scope(monkeypatch, svc) out = svc.withdraw_apply(db, "t1", _user("u-applicant"), "a-wd") assert out.status == "draft" assert out.current_node in (None, "") row = db.query(PlVehicleRechargeApply).filter_by(id="a-wd").one() assert row.status == "draft" assert row.current_node is None assert row.approval_round == 1 assert ( db.query(PlVehicleRechargeApproval) .filter_by(apply_id="a-wd", del_flag=0) .count() == 1 ) def test_withdraw_rejects_non_applicant(monkeypatch): from fastapi import HTTPException from app.services import pl_vehicle_recharge_service as svc db = _make_session() _seed_vehicle(db) db.add( PlVehicleRechargeApply( id="a-wd2", apply_no="SXSQWD002", license_plate="鄂A00001", recharge_type="etc", apply_date=date.today(), applicant_id="u-applicant", status="in_approval", tenant_no="t1", create_dept_id="d1", ) ) db.commit() _patch_scope(monkeypatch, svc) with pytest.raises(HTTPException) as ei: svc.withdraw_apply(db, "t1", _user("u-other"), "a-wd2") assert ei.value.status_code == 403 def test_withdraw_rejects_non_in_approval(monkeypatch): from fastapi import HTTPException from app.services import pl_vehicle_recharge_service as svc db = _make_session() _seed_vehicle(db) db.add( PlVehicleRechargeApply( id="a-wd3", apply_no="SXSQWD003", license_plate="鄂A00001", recharge_type="etc", apply_date=date.today(), applicant_id="u-applicant", status="draft", tenant_no="t1", create_dept_id="d1", ) ) db.commit() _patch_scope(monkeypatch, svc) with pytest.raises(HTTPException) as ei: svc.withdraw_apply(db, "t1", _user("u-applicant"), "a-wd3") assert ei.value.status_code == 400