337 lines
15 KiB
Python
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"] == "新挂车"
|