Files
shanxia2.0/backend/tests/test_pl_vehicle_recharge_summary.py
T

337 lines
15 KiB
Python

"""充值看板:自然日金额口径、权限隔离及真实动态。"""
from datetime import date, datetime
from decimal import Decimal
from types import SimpleNamespace
import pytest
from sqlalchemy import MetaData, UniqueConstraint, create_engine
from sqlalchemy import event
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.models.models import SysRole, SysRoleDataScopeDetail, SysUser, SysUserRole
from app.models.pl_models import (
PlPersonnel,
PlVehicle,
PlVehiclePersonnel,
PlVehicleRechargeApply,
PlVehicleRechargeApproval,
PlVehicleRechargeRecord,
)
@pytest.fixture
def db():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
for model in (PlVehicle, PlPersonnel, PlVehiclePersonnel, PlVehicleRechargeApply, PlVehicleRechargeApproval,
PlVehicleRechargeRecord, SysUser, SysRole, SysUserRole,
SysRoleDataScopeDetail):
model.__table__.create(engine)
session = sessionmaker(bind=engine)()
session.add(SysUser(id="u1", user_no="applicant", nick_name="真实申请人", tenant_no="t1"))
session.add(SysRole(id="role", role_name="本人", data_scope="4", tenant_no="t1"))
session.add(SysUserRole(id="ur", user_id="u1", role_id="role", tenant_no="t1"))
session.commit()
yield session
session.close()
engine.dispose()
def _add(db, model, id, **kwargs):
values = dict(id=id, tenant_no="t1", create_by="u1", create_dept_id="d1",
create_time=datetime(2026, 1, 1), del_flag=0)
values.update(kwargs)
row = model(**values)
db.add(row)
return row
def _vehicle(db, id="v1", **kwargs):
values = dict(vehicle_type="self", license_plate=id, default_driver_name="张司机",
trailer_no="鄂挂01")
values.update(kwargs)
return _add(db, PlVehicle, id, **values)
def _apply(db, id, **kwargs):
values = dict(apply_no=id, license_plate="v1", recharge_type="etc",
apply_date=date(2026, 1, 1), applicant_id="u1", status="disbursed")
values.update(kwargs)
return _add(db, PlVehicleRechargeApply, id, **values)
def _record(db, id, day=date(2026, 1, 1), amount="10.00", **kwargs):
values = dict(recharge_order_no=id, apply_id=id, license_plate="v1", recharge_type="etc",
recharge_date=day, amount=Decimal(amount))
values.update(kwargs)
return _add(db, PlVehicleRechargeRecord, "record-" + id, **values)
def _summary(db, today=date(2026, 1, 15)):
from app.services.pl_vehicle_recharge_summary_service import get_summary
db.commit()
user = SimpleNamespace(to_user_info=lambda: dict(user_id="u1", role="admin", dept_id="d1"))
return get_summary(db, "t1", user, today=today).model_dump(by_alias=True)
@pytest.mark.parametrize("legacy_name", [None, "过期司机姓名"])
def test_driver_name_comes_from_vehicle_personnel_link(db, legacy_name):
_vehicle(db, default_driver_name=legacy_name)
for id, role, default in (("escort", "escort", 0), ("backup", "driver", 0),
("current", "driver", 1)):
_add(db, PlPersonnel, id, personnel_name=id)
_add(db, PlVehiclePersonnel, "link-" + id, vehicle_id="v1", personnel_id=id,
role=role, is_default=default)
assert _summary(db)["vehicles"][0]["driverName"] == "current"
def test_driver_without_default_flag_and_invalid_links(db):
_vehicle(db, default_driver_name=None)
for id, person_values, link_values in (
("foreign-person", {"tenant_no": "t2"}, {}),
("deleted-person", {"del_flag": 1}, {}),
("foreign-link", {}, {"tenant_no": "t2"}),
("deleted-link", {}, {"del_flag": 1}),
("current", {}, {"is_default": 0}),
):
_add(db, PlPersonnel, id, personnel_name=id, **person_values)
values = dict(role="driver", is_default=1)
values.update(link_values)
_add(db, PlVehiclePersonnel, "link-" + id, vehicle_id="v1", personnel_id=id, **values)
assert _summary(db)["vehicles"][0]["driverName"] == "current"
def test_natural_periods_cross_year_precision_and_zero_vehicles(db):
_vehicle(db)
_vehicle(db, "v-zero", vehicle_status="maintaining")
for id, day, amount in (
("older", date(2025, 9, 30), "0.10"),
("quarter", date(2025, 10, 1), "0.20"),
("month", date(2025, 12, 31), "10.11"),
("current", date(2026, 1, 1), "20.22"),
("today", date(2026, 1, 15), "30.33"),
("future", date(2026, 1, 16), "999.99"),
):
_apply(db, id)
_record(db, id, day, amount)
result = _summary(db)
assert result["asOfDate"] == date(2026, 1, 15)
rows = {v["id"]: v for v in result["vehicles"]}
assert rows["v1"]["etc"] == dict(monthAmount=Decimal("50.55"), previousMonthAmount=Decimal("10.11"),
quarterAmount=Decimal("50.55"), previousQuarterAmount=Decimal("10.31"),
totalAmount=Decimal("60.96"), previousTotalAmount=Decimal("10.41"), pendingCount=0)
assert all(value == 0 for value in rows["v1"]["fuel"].values())
assert all(value == 0 for value in rows["v-zero"]["etc"].values())
assert rows["v1"]["driverName"] == "张司机"
assert rows["v-zero"]["vehicleStatus"] == "maintaining"
def test_pending_includes_disburse_node_and_excludes_invalid_records(db):
_vehicle(db)
for status in ("draft", "in_approval", "pending", "rejected", "cancelled", "disbursed"):
_apply(db, status, status=status, current_node="finance-disburse")
_record(db, status)
_apply(db, "fuel", recharge_type="fuel", status="pending")
_apply(db, "shanxia", recharge_type="shanxia", status="pending")
_record(db, "orphan", apply_id=None)
result = _summary(db)["vehicles"][0]
assert result["etc"]["pendingCount"] == 2
assert result["fuel"]["pendingCount"] == 1
assert result["etc"]["totalAmount"] == Decimal("10.00")
assert result["fuel"]["totalAmount"] == 0
@pytest.mark.parametrize("model", [PlVehicle, PlVehicleRechargeApply, PlVehicleRechargeRecord])
@pytest.mark.parametrize("excluded", [dict(tenant_no="t2"), dict(del_flag=1), dict(create_by="u-other")])
def test_money_and_notices_apply_tenant_delete_and_scope_on_every_entity(db, model, excluded):
_vehicle(db, **(excluded if model is PlVehicle else {}))
_apply(db, "a", **(excluded if model is PlVehicleRechargeApply else {}))
_record(db, "a", **(excluded if model is PlVehicleRechargeRecord else {}))
_add(db, PlVehicleRechargeApproval, "approval", apply_id="a", approver_id="u1",
approver_name="真实申请人", approval_status="disbursed", node_name="财务发放",
approval_time=datetime(2026, 1, 2))
result = _summary(db)
assert sum(v["etc"]["totalAmount"] for v in result["vehicles"]) == 0
assert not any(n["action"] == "已发放" for n in result["notices"])
if model in (PlVehicle, PlVehicleRechargeApply):
assert result["notices"] == []
def test_pending_scoped_and_carrier_hidden(db):
_vehicle(db)
_vehicle(db, "carrier", vehicle_type="carrier")
_apply(db, "hidden-pending", status="pending", create_by="other")
_apply(db, "carrier", license_plate="carrier", status="pending")
result = _summary(db)
assert [v["id"] for v in result["vehicles"]] == ["v1"]
assert result["vehicles"][0]["etc"]["pendingCount"] == 0
assert result["notices"] == []
def test_notices_latest_three_real_actors_nodes_and_disbursement_deduplication(db):
_vehicle(db)
_apply(db, "a", create_time=datetime(2026, 1, 1, 8))
_record(db, "a", create_time=datetime(2026, 1, 4, 8))
for id, status, time, node in (
("reject", "rejected", datetime(2026, 1, 2, 8), "经理审核"),
("approve", "approved", datetime(2026, 1, 3, 8), "经理审核"),
("disburse", "disbursed", datetime(2026, 1, 4, 8), "财务发放"),
):
_add(db, PlVehicleRechargeApproval, id, apply_id="a", approver_id="u1",
approver_name="历史姓名", node_name=node, approval_status=status, approval_time=time)
result = _summary(db)
notices = result["notices"]
assert [n["action"] for n in notices] == ["已发放", "已审批", "已驳回"]
assert [n["roleName"] for n in notices] == ["财务发放", "经理审核", "经理审核"]
assert all(n["actorName"] == "历史姓名" for n in notices)
assert len({n["id"] for n in notices}) == 3
def test_apply_notice_does_not_resolve_other_tenant_user(db):
_vehicle(db)
_add(db, SysUser, "outsider", tenant_no="t2", user_no="secret", nick_name="另一租户姓名")
_apply(db, "a", status="in_approval", applicant_id="outsider")
notices = _summary(db)["notices"]
assert all(n["actorName"] != "另一租户姓名" for n in notices)
def test_no_vehicles_returns_empty_summary(db):
assert _summary(db) == dict(asOfDate=date(2026, 1, 15), vehicles=[], notices=[])
def test_legacy_record_notice_uses_linked_vehicle_when_redundant_plate_missing(db):
_vehicle(db)
_apply(db, "a")
_record(db, "a", license_plate=None, create_time=datetime(2026, 1, 2))
notice = _summary(db)["notices"][0]
assert notice["action"] == "已发放"
assert notice["licensePlate"] == "v1"
assert notice["actorName"] == "真实申请人"
def test_empty_approval_name_resolves_current_tenant_user(db):
_vehicle(db)
_apply(db, "a", status="in_approval")
_add(db, PlVehicleRechargeApproval, "approval", apply_id="a", approver_id="u1",
approver_name="", approval_status="approved", node_name="经理审核",
approval_time=datetime(2026, 1, 2))
assert _summary(db)["notices"][0]["actorName"] == "真实申请人"
@pytest.mark.parametrize("excluded", [dict(tenant_no="t2"), dict(del_flag=1), dict(create_by="other")])
def test_approval_events_are_scoped(db, excluded):
_vehicle(db)
_apply(db, "a", status="in_approval")
_add(db, PlVehicleRechargeApproval, "secret", apply_id="a", approver_id="u1",
approver_name="隐藏审批人", approval_status="approved", approval_time=datetime(2026, 1, 2), **excluded)
assert not any(n["actorName"] == "隐藏审批人" for n in _summary(db)["notices"])
def test_no_vehicle_pagination_cap(db):
for index in range(501):
_vehicle(db, "v-" + str(index))
assert len(_summary(db)["vehicles"]) == 501
def test_later_quarter_uses_previous_full_month_and_quarter(db):
_vehicle(db)
for id, day, amount in (
("q1", date(2026, 3, 31), "1.01"),
("q2-april", date(2026, 4, 1), "2.02"),
("q2-june", date(2026, 6, 30), "3.03"),
("q3-july", date(2026, 7, 1), "4.04"),
("q3-august", date(2026, 8, 1), "5.05"),
):
_apply(db, id)
_record(db, id, day, amount)
result = _summary(db, today=date(2026, 8, 10))["vehicles"][0]["etc"]
assert result["monthAmount"] == Decimal("5.05")
assert result["previousMonthAmount"] == Decimal("4.04")
assert result["quarterAmount"] == Decimal("9.09")
assert result["previousQuarterAmount"] == Decimal("5.05")
assert result["previousTotalAmount"] == Decimal("6.06")
def test_http_summary_authentication_route_and_json_contract(db):
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api.pl.vehicle_recharge import apply_router
from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
_vehicle(db)
_apply(db, "a")
_record(db, "a", date(2000, 1, 1), "10.10")
db.commit()
app = FastAPI()
app.include_router(apply_router, prefix="/api/transport")
app.dependency_overrides[get_db] = lambda: db
with TestClient(app) as client:
path = "/api/transport/vehicle-recharge-apply/summary"
assert client.get(path).status_code == 401
app.dependency_overrides[get_db_current_user] = lambda: DbUserContext(
id="u1", name="真实申请人", dept_id="d1", role="admin", tenant_no="t1"
)
response = client.get(path + "?tenantNo=t2")
assert response.status_code == 200, response.text
body = response.json()
assert set(body) == {"asOfDate", "vehicles", "notices"}
assert date.fromisoformat(body["asOfDate"])
vehicle = body["vehicles"][0]
assert set(vehicle) == {"id", "licensePlate", "trailerNo", "driverName",
"vehicleType", "vehicleStatus", "etc", "fuel"}
assert Decimal(vehicle["etc"]["totalAmount"]) == Decimal("10.10")
assert all(set(n) == {"id", "occurredAt", "actorName", "roleName", "action",
"licensePlate", "rechargeType"} for n in body["notices"])
def test_notice_user_names_use_one_batch_without_cross_table_collation_joins(db):
_vehicle(db)
_apply(db, "a")
_record(db, "a", create_time=datetime(2026, 1, 3))
_add(db, PlVehicleRechargeApproval, "approval", apply_id="a", approver_id="u1",
approver_name="", approval_status="approved", node_name="经理审核",
approval_time=datetime(2026, 1, 2))
statements = []
def capture(_connection, _cursor, statement, _parameters, _context, _executemany):
statements.append(statement.lower())
event.listen(db.bind, "before_cursor_execute", capture)
try:
notices = _summary(db)["notices"]
finally:
event.remove(db.bind, "before_cursor_execute", capture)
assert len(notices) == 3
assert all(n["actorName"] == "真实申请人" for n in notices)
assert not any("join sys_user " in statement for statement in statements)
user_queries = [statement for statement in statements if "from sys_user " in statement]
assert len(user_queries) == 1
assert "sys_user.id in (" in user_queries[0]
assert "sys_user.tenant_no =" in user_queries[0]
assert "sys_user.del_flag =" in user_queries[0]
def test_duplicate_vehicle_archives_count_each_apply_once_and_show_latest_card(db):
# 历史 MySQL 库允许同车牌多挂车档案,与 ORM 的唯一约束声明不同。
vehicle_table = PlVehicle.__table__.to_metadata(MetaData())
for constraint in list(vehicle_table.constraints):
if isinstance(constraint, UniqueConstraint):
vehicle_table.constraints.remove(constraint)
PlVehicle.__table__.drop(db.bind)
vehicle_table.create(db.bind)
_vehicle(db, "older", license_plate="同车牌", trailer_no="旧挂车",
create_time=datetime(2026, 1, 1))
_vehicle(db, "newer", license_plate="同车牌", trailer_no="新挂车",
create_time=datetime(2026, 1, 2))
_apply(db, "pending", license_plate="同车牌", status="in_approval")
_apply(db, "paid", license_plate="同车牌")
_record(db, "paid", license_plate="同车牌", amount="12.34")
result = _summary(db)
assert all(v["etc"]["pendingCount"] == 1 for v in result["vehicles"])
assert all(v["etc"]["totalAmount"] == Decimal("12.34") for v in result["vehicles"])
assert len(result["vehicles"]) == 1
assert result["vehicles"][0]["id"] == "newer"
assert result["vehicles"][0]["trailerNo"] == "新挂车"