feat: 同步报销流程与工作台改动
This commit is contained in:
@@ -234,6 +234,124 @@ def test_budget_warning_application_still_skips_budget_manager_when_not_over_bud
|
||||
)
|
||||
|
||||
|
||||
def test_application_routes_to_budget_manager_when_usage_reaches_90_percent() -> None:
|
||||
with build_session() as db:
|
||||
department, manager, _budget_manager, employee = _seed_people(db, suffix="OVER-90-APP")
|
||||
_seed_budget_allocation(
|
||||
db,
|
||||
department_id=department.id,
|
||||
department_name=department.name,
|
||||
amount=Decimal("10000.00"),
|
||||
)
|
||||
claim = ExpenseClaim(
|
||||
claim_no="APP-20260530-OVER-90",
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
department_id=department.id,
|
||||
department_name=department.name,
|
||||
project_code=None,
|
||||
expense_type="travel_application",
|
||||
reason="客户现场支持",
|
||||
location="上海",
|
||||
amount=Decimal("9500.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
|
||||
risk_flags_json=[],
|
||||
)
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
|
||||
routed = ExpenseClaimService(db).approve_claim(
|
||||
claim.id,
|
||||
CurrentUserContext(
|
||||
username=manager.email,
|
||||
name=manager.name,
|
||||
role_codes=["manager"],
|
||||
is_admin=False,
|
||||
),
|
||||
opinion="业务必要,同意申请。",
|
||||
)
|
||||
|
||||
assert routed is not None
|
||||
assert routed.status == "submitted"
|
||||
assert routed.approval_stage == BUDGET_MANAGER_APPROVAL_STAGE
|
||||
assert any(
|
||||
isinstance(flag, dict)
|
||||
and flag.get("source") == "approval_routing"
|
||||
and flag.get("requires_budget_review") is True
|
||||
and flag.get("route") == "budget_manager"
|
||||
and any("90%" in item or "90" in item for item in flag.get("reasons", []))
|
||||
for flag in routed.risk_flags_json
|
||||
)
|
||||
|
||||
|
||||
def test_application_stage_risk_under_90_percent_does_not_route_to_budget_manager() -> None:
|
||||
with build_session() as db:
|
||||
department, manager, _budget_manager, employee = _seed_people(db, suffix="RISK-APP")
|
||||
_seed_budget_allocation(
|
||||
db,
|
||||
department_id=department.id,
|
||||
department_name=department.name,
|
||||
amount=Decimal("10000.00"),
|
||||
)
|
||||
claim = ExpenseClaim(
|
||||
claim_no="APP-20260530-RISK-UNDER-90",
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
department_id=department.id,
|
||||
department_name=department.name,
|
||||
project_code=None,
|
||||
expense_type="travel_application",
|
||||
reason="客户现场支持",
|
||||
location="上海",
|
||||
amount=Decimal("500.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
|
||||
risk_flags_json=[
|
||||
{
|
||||
"source": "submission_review",
|
||||
"severity": "high",
|
||||
"label": "申请信息风险",
|
||||
"message": "申请事由需要领导关注。",
|
||||
"business_stage": "expense_application",
|
||||
}
|
||||
],
|
||||
)
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
|
||||
approved = ExpenseClaimService(db).approve_claim(
|
||||
claim.id,
|
||||
CurrentUserContext(
|
||||
username=manager.email,
|
||||
name=manager.name,
|
||||
role_codes=["manager"],
|
||||
is_admin=False,
|
||||
),
|
||||
opinion="业务必要,同意申请。",
|
||||
)
|
||||
|
||||
assert approved is not None
|
||||
assert approved.status == "approved"
|
||||
assert approved.approval_stage == APPLICATION_LINK_STATUS_STAGE
|
||||
route_flag = [
|
||||
flag
|
||||
for flag in approved.risk_flags_json
|
||||
if isinstance(flag, dict) and flag.get("source") == "approval_routing"
|
||||
][0]
|
||||
assert route_flag["requires_budget_review"] is False
|
||||
assert route_flag["route"] == "approval_done"
|
||||
assert route_flag["current_risk_count"] == 1
|
||||
|
||||
|
||||
def test_application_route_ignores_reimbursement_stage_current_risks() -> None:
|
||||
with build_session() as db:
|
||||
department, manager, _budget_manager, employee = _seed_people(db, suffix="MIXED-STAGE")
|
||||
|
||||
@@ -28,6 +28,7 @@ from app.schemas.reimbursement import (
|
||||
)
|
||||
from app.services.agent_conversations import AgentConversationService
|
||||
from app.services.budget import BudgetService
|
||||
from app.services.expense_claim_budget_flow import ExpenseClaimBudgetFlowMixin
|
||||
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
|
||||
from app.services.expense_claims import ExpenseClaimService
|
||||
from app.services.expense_claim_workflow_constants import (
|
||||
@@ -119,6 +120,42 @@ def build_session() -> Session:
|
||||
return session_factory()
|
||||
|
||||
|
||||
def test_append_budget_flags_replaces_duplicate_budget_warning() -> None:
|
||||
base_warning = {
|
||||
"source": "budget_control",
|
||||
"event_type": "budget_warning",
|
||||
"severity": "medium",
|
||||
"label": "预算接近预警线",
|
||||
"message": "预算 SIM-BUD-2026-R0048 本次占用后使用率预计达到 98.00%,已达到预警线 80.00%。",
|
||||
"budget_no": "SIM-BUD-2026-R0048",
|
||||
"allocation_id": "allocation-0048",
|
||||
"subject_code": "travel",
|
||||
"created_at": "2026-06-03T10:00:00+00:00",
|
||||
}
|
||||
latest_warning = {
|
||||
**base_warning,
|
||||
"message": "预算 SIM-BUD-2026-R0048 本次占用后使用率预计达到 99.27%,已达到预警线 80.00%。",
|
||||
"created_at": "2026-06-03T10:01:00+00:00",
|
||||
}
|
||||
|
||||
flags = ExpenseClaimBudgetFlowMixin._append_budget_flags(
|
||||
[base_warning],
|
||||
latest_warning,
|
||||
business_stage="reimbursement",
|
||||
)
|
||||
|
||||
warnings = [
|
||||
flag
|
||||
for flag in flags
|
||||
if isinstance(flag, dict)
|
||||
and flag.get("source") == "budget_control"
|
||||
and flag.get("event_type") == "budget_warning"
|
||||
]
|
||||
assert len(warnings) == 1
|
||||
assert "99.27%" in warnings[0]["message"]
|
||||
assert warnings[0]["business_stage"] == "reimbursement"
|
||||
|
||||
|
||||
def _count_claims(db: Session) -> int:
|
||||
return int(db.query(ExpenseClaim).count())
|
||||
|
||||
@@ -2360,6 +2397,112 @@ def test_upload_hotel_attachment_flags_amount_over_travel_policy(monkeypatch, tm
|
||||
)
|
||||
|
||||
|
||||
def test_delete_claim_item_attachment_removes_attachment_analysis_risk(monkeypatch, tmp_path) -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="emp-hotel-risk@example.com",
|
||||
name="张三",
|
||||
role_codes=[],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
def fake_recognize(
|
||||
self,
|
||||
files: list[tuple[str, bytes, str | None]],
|
||||
) -> OcrRecognizeBatchRead:
|
||||
return OcrRecognizeBatchRead(
|
||||
total_file_count=1,
|
||||
success_count=1,
|
||||
documents=[
|
||||
OcrRecognizeDocumentRead(
|
||||
filename="hotel-risk.png",
|
||||
media_type="image/png",
|
||||
text="北京全季酒店 住宿 1晚 金额800元 2026-05-13",
|
||||
summary="北京全季酒店住宿发票,住宿 1 晚,金额 800 元。",
|
||||
avg_score=0.98,
|
||||
line_count=1,
|
||||
page_count=1,
|
||||
document_type="hotel_invoice",
|
||||
document_type_label="酒店住宿票据",
|
||||
scene_code="hotel",
|
||||
scene_label="住宿票据",
|
||||
document_fields=[
|
||||
{"key": "merchant_name", "label": "商户", "value": "北京全季酒店"},
|
||||
{"key": "amount", "label": "金额", "value": "800元"},
|
||||
{"key": "date", "label": "日期", "value": "2026-05-13"},
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
employee_no="E7402",
|
||||
name="张三",
|
||||
email="emp-hotel-risk@example.com",
|
||||
grade="P4",
|
||||
)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
|
||||
claim = build_claim(expense_type="travel", location="北京")
|
||||
claim.employee = employee
|
||||
claim.employee_id = employee.id
|
||||
claim.reason = "北京客户现场出差"
|
||||
claim.invoice_count = 0
|
||||
claim.items[0].item_type = "hotel"
|
||||
claim.items[0].item_reason = "北京住宿"
|
||||
claim.items[0].item_location = "北京"
|
||||
claim.items[0].item_amount = Decimal("0.00")
|
||||
claim.items[0].invoice_id = None
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
|
||||
service = ExpenseClaimService(db)
|
||||
upload_payload = service.upload_claim_item_attachment(
|
||||
claim_id=claim.id,
|
||||
item_id=claim.items[0].id,
|
||||
filename="hotel-risk.png",
|
||||
content=b"fake-image-bytes",
|
||||
media_type="image/png",
|
||||
current_user=current_user,
|
||||
)
|
||||
|
||||
assert upload_payload is not None
|
||||
assert any(
|
||||
isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis"
|
||||
for flag in upload_payload["claim_risk_flags"]
|
||||
)
|
||||
|
||||
delete_payload = service.delete_claim_item_attachment(
|
||||
claim_id=claim.id,
|
||||
item_id=claim.items[0].id,
|
||||
current_user=current_user,
|
||||
)
|
||||
|
||||
assert delete_payload is not None
|
||||
assert delete_payload["invoice_id"] is None
|
||||
assert not any(
|
||||
isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis"
|
||||
for flag in delete_payload["claim_risk_flags"]
|
||||
)
|
||||
assert not any(
|
||||
"高风险附件" in str(flag.get("message") or "")
|
||||
for flag in delete_payload["claim_risk_flags"]
|
||||
if isinstance(flag, dict)
|
||||
)
|
||||
|
||||
db.refresh(claim)
|
||||
assert claim.invoice_count == 0
|
||||
assert claim.items[0].invoice_id is None
|
||||
assert not any(
|
||||
isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis"
|
||||
for flag in list(claim.risk_flags_json or [])
|
||||
)
|
||||
|
||||
|
||||
def test_attachment_analysis_does_not_compare_business_purpose_with_ticket_scene() -> None:
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="travel", location="上海")
|
||||
@@ -3741,6 +3884,101 @@ def test_list_claims_returns_company_reimbursements_for_finance_document_center(
|
||||
assert "EXP-FIN-COMPANY-PAID" in archived_nos
|
||||
|
||||
|
||||
def test_list_claims_returns_all_active_documents_for_admin_document_center() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="admin",
|
||||
name="admin",
|
||||
role_codes=["admin"],
|
||||
is_admin=True,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
db.add_all(
|
||||
[
|
||||
ExpenseClaim(
|
||||
claim_no="AP-ADMIN-DRAFT",
|
||||
employee_name="Applicant A",
|
||||
department_name="Tech",
|
||||
project_code="PRJ-APP",
|
||||
expense_type="travel_application",
|
||||
reason="Travel application draft",
|
||||
location="Shanghai",
|
||||
amount=Decimal("1200.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 5, 11, 9, 0, tzinfo=UTC),
|
||||
submitted_at=None,
|
||||
status="draft",
|
||||
approval_stage="draft",
|
||||
risk_flags_json=[],
|
||||
),
|
||||
ExpenseClaim(
|
||||
claim_no="AP-ADMIN-LINKING",
|
||||
employee_name="Applicant B",
|
||||
department_name="Tech",
|
||||
project_code="PRJ-APP",
|
||||
expense_type="travel_application",
|
||||
reason="Travel application approved",
|
||||
location="Beijing",
|
||||
amount=Decimal("2200.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 12, 10, 0, tzinfo=UTC),
|
||||
status="approved",
|
||||
approval_stage=APPLICATION_LINK_STATUS_STAGE,
|
||||
risk_flags_json=[],
|
||||
),
|
||||
ExpenseClaim(
|
||||
claim_no="EXP-ADMIN-DRAFT",
|
||||
employee_name="Applicant C",
|
||||
department_name="Finance",
|
||||
project_code="PRJ-EXP",
|
||||
expense_type="office",
|
||||
reason="Office draft",
|
||||
location="Hangzhou",
|
||||
amount=Decimal("300.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 5, 13, 9, 0, tzinfo=UTC),
|
||||
submitted_at=None,
|
||||
status="draft",
|
||||
approval_stage="draft",
|
||||
risk_flags_json=[],
|
||||
),
|
||||
ExpenseClaim(
|
||||
claim_no="EXP-ADMIN-ARCHIVED",
|
||||
employee_name="Applicant D",
|
||||
department_name="Finance",
|
||||
project_code="PRJ-EXP",
|
||||
expense_type="office",
|
||||
reason="Archived reimbursement",
|
||||
location="Hangzhou",
|
||||
amount=Decimal("500.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 14, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 14, 10, 0, tzinfo=UTC),
|
||||
status="paid",
|
||||
approval_stage="payment",
|
||||
risk_flags_json=[],
|
||||
),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
claim_nos = {claim.claim_no for claim in ExpenseClaimService(db).list_claims(current_user)}
|
||||
archived_nos = {
|
||||
claim.claim_no for claim in ExpenseClaimService(db).list_archived_claims(current_user)
|
||||
}
|
||||
|
||||
assert "AP-ADMIN-DRAFT" in claim_nos
|
||||
assert "AP-ADMIN-LINKING" in claim_nos
|
||||
assert "EXP-ADMIN-DRAFT" in claim_nos
|
||||
assert "EXP-ADMIN-ARCHIVED" not in claim_nos
|
||||
assert "EXP-ADMIN-ARCHIVED" in archived_nos
|
||||
|
||||
|
||||
def test_list_claims_limits_executive_to_personal_records() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="executive@example.com",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import get_settings
|
||||
@@ -88,6 +89,46 @@ print("__OCR_JSON__=" + json.dumps(payload, ensure_ascii=False))
|
||||
assert skipped.warnings == ["当前仅支持图片和 PDF 文件进行 OCR。"]
|
||||
|
||||
|
||||
def test_ocr_service_passes_configured_device_to_worker(
|
||||
monkeypatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
captured_commands: list[list[str]] = []
|
||||
|
||||
def fake_run(
|
||||
command: list[str],
|
||||
*,
|
||||
capture_output: bool,
|
||||
text: bool,
|
||||
timeout: int,
|
||||
check: bool,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
captured_commands.append(command)
|
||||
return subprocess.CompletedProcess(
|
||||
args=command,
|
||||
returncode=0,
|
||||
stdout='__OCR_JSON__={"engine":"paddleocr_mobile","model":"PP-OCRv5_mobile","documents":[]}\n',
|
||||
stderr="",
|
||||
)
|
||||
|
||||
monkeypatch.setenv("OCR_DEVICE", "gpu:0")
|
||||
get_settings.cache_clear()
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
try:
|
||||
payload = OcrService()._invoke_worker(
|
||||
python_bin="python",
|
||||
worker_path="worker.py",
|
||||
input_paths=[tmp_path / "invoice.png"],
|
||||
)
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
assert payload["engine"] == "paddleocr_mobile"
|
||||
command = captured_commands[0]
|
||||
device_index = command.index("--device")
|
||||
assert command[device_index + 1] == "gpu:0"
|
||||
|
||||
|
||||
def test_ocr_service_converts_pdf_to_images_and_returns_image_preview(
|
||||
monkeypatch,
|
||||
tmp_path: Path,
|
||||
|
||||
@@ -113,6 +113,135 @@ def seed_claim(db: Session) -> tuple[ExpenseClaim, ExpenseClaimItem]:
|
||||
return claim, item
|
||||
|
||||
|
||||
def test_claim_read_uses_organization_manager_and_dedupes_budget_warnings() -> None:
|
||||
client, session_factory = build_client()
|
||||
with session_factory() as db:
|
||||
department = OrganizationUnit(
|
||||
id="dept-org-manager",
|
||||
unit_code="DEPT-ORG-MANAGER",
|
||||
name="交付部",
|
||||
manager_name="王总",
|
||||
)
|
||||
employee = Employee(
|
||||
id="emp-org-manager",
|
||||
employee_no="E30001",
|
||||
name="赵六",
|
||||
email="zhaoliu@example.com",
|
||||
organization_unit=department,
|
||||
position="实施顾问",
|
||||
grade="P5",
|
||||
finance_owner_name="Wang Finance",
|
||||
)
|
||||
duplicated_warning = {
|
||||
"source": "budget_control",
|
||||
"event_type": "budget_warning",
|
||||
"severity": "medium",
|
||||
"label": "预算接近预警线",
|
||||
"message": "预算 SIM-BUD-2026-R0048 本次占用后使用率预计达到 99.27%,已达到预警线 80.00%。",
|
||||
"budget_no": "SIM-BUD-2026-R0048",
|
||||
"allocation_id": "allocation-0048",
|
||||
"subject_code": "travel",
|
||||
}
|
||||
claim = ExpenseClaim(
|
||||
id="claim-org-manager",
|
||||
claim_no="EXP-202606-ORG-MGR",
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
department_id=department.id,
|
||||
department_name=department.name,
|
||||
project_code=None,
|
||||
expense_type="travel",
|
||||
reason="差旅报销",
|
||||
location="上海",
|
||||
amount=Decimal("880.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 6, 3, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 6, 3, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="直属领导审批",
|
||||
risk_flags_json=[
|
||||
{**duplicated_warning, "created_at": "2026-06-03T10:00:00+00:00"},
|
||||
{**duplicated_warning, "created_at": "2026-06-03T10:01:00+00:00"},
|
||||
],
|
||||
)
|
||||
db.add_all([department, employee, claim])
|
||||
db.commit()
|
||||
|
||||
headers = {"x-auth-username": "zhaoliu@example.com"}
|
||||
response = client.get("/api/v1/reimbursements/claims/claim-org-manager", headers=headers)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["manager_name"] == "王总"
|
||||
assert payload["finance_owner_name"] == "Wang Finance"
|
||||
budget_warnings = [
|
||||
flag
|
||||
for flag in payload["risk_flags_json"]
|
||||
if flag.get("source") == "budget_control" and flag.get("event_type") == "budget_warning"
|
||||
]
|
||||
assert len(budget_warnings) == 1
|
||||
assert budget_warnings[0]["message"] == duplicated_warning["message"]
|
||||
|
||||
|
||||
def test_claim_read_attaches_finance_approver_name_for_finance_stage() -> None:
|
||||
client, session_factory = build_client()
|
||||
with session_factory() as db:
|
||||
finance_role = Role(
|
||||
id="role-finance-reader",
|
||||
role_code="finance",
|
||||
name="财务",
|
||||
description="可处理财务复核任务",
|
||||
)
|
||||
applicant = Employee(
|
||||
id="emp-finance-stage-applicant",
|
||||
employee_no="E30002",
|
||||
name="钱七",
|
||||
email="qianqi@example.com",
|
||||
position="实施顾问",
|
||||
grade="P5",
|
||||
finance_owner_name="Wang Finance Group",
|
||||
)
|
||||
finance_user = Employee(
|
||||
id="emp-finance-stage-approver",
|
||||
employee_no="F30002",
|
||||
name="Wang Finance",
|
||||
email="wang.finance@example.com",
|
||||
position="财务专员",
|
||||
grade="P6",
|
||||
finance_owner_name="Wang Finance Group",
|
||||
roles=[finance_role],
|
||||
)
|
||||
claim = ExpenseClaim(
|
||||
id="claim-finance-stage-reader",
|
||||
claim_no="EXP-202606-FINANCE-MGR",
|
||||
employee_id=applicant.id,
|
||||
employee_name=applicant.name,
|
||||
department_id=None,
|
||||
department_name="交付部",
|
||||
project_code=None,
|
||||
expense_type="travel",
|
||||
reason="差旅报销",
|
||||
location="上海",
|
||||
amount=Decimal("880.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 6, 3, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 6, 3, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="财务审批",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
db.add_all([finance_role, applicant, finance_user, claim])
|
||||
db.commit()
|
||||
|
||||
headers = {"x-auth-username": "qianqi@example.com"}
|
||||
response = client.get("/api/v1/reimbursements/claims/claim-finance-stage-reader", headers=headers)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["finance_owner_name"] == "Wang Finance Group"
|
||||
assert payload["finance_approver_name"] == "Wang Finance"
|
||||
|
||||
|
||||
def test_claim_standard_adjustment_endpoint_recalculates_and_marks_reviewer_notice() -> None:
|
||||
client, session_factory = build_client()
|
||||
with session_factory() as db:
|
||||
|
||||
@@ -147,3 +147,52 @@ def test_system_dashboard_service_aggregates_real_runtime_metrics() -> None:
|
||||
assert dashboard.accuracy_comparison["wrong"][
|
||||
dashboard.accuracy_comparison["categories"].index("异常诊断")
|
||||
] == 1
|
||||
|
||||
|
||||
def test_system_dashboard_counts_online_users_from_active_sessions_across_window() -> None:
|
||||
now = datetime.now(UTC)
|
||||
|
||||
with build_session() as db:
|
||||
db.add_all(
|
||||
[
|
||||
UserSessionMetric(
|
||||
session_id="session-online-old-001",
|
||||
username="active.user@example.com",
|
||||
display_name="在线用户",
|
||||
email="active.user@example.com",
|
||||
login_at=now - timedelta(days=2),
|
||||
last_activity_at=now - timedelta(days=2),
|
||||
activity_event_count=3,
|
||||
status="active",
|
||||
),
|
||||
UserSessionMetric(
|
||||
session_id="session-online-old-002",
|
||||
username="active.user@example.com",
|
||||
display_name="在线用户",
|
||||
email="active.user@example.com",
|
||||
login_at=now - timedelta(minutes=15),
|
||||
last_activity_at=now - timedelta(minutes=15),
|
||||
activity_event_count=5,
|
||||
status="active",
|
||||
),
|
||||
UserSessionMetric(
|
||||
session_id="session-closed-outside-window",
|
||||
username="offline.user@example.com",
|
||||
display_name="离线用户",
|
||||
email="offline.user@example.com",
|
||||
login_at=now - timedelta(days=3),
|
||||
logout_at=now - timedelta(days=2, hours=23),
|
||||
duration_ms=60 * 60 * 1000,
|
||||
status="closed",
|
||||
),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
dashboard = SystemDashboardService(db).build_dashboard(days=1)
|
||||
login_users = dashboard.login_wave["loginUsers"]
|
||||
|
||||
assert dashboard.totals["onlineUsers"] == 1
|
||||
assert max(login_users) == 1
|
||||
assert "00:00" in dashboard.login_wave["labels"]
|
||||
assert "23:00" in dashboard.login_wave["labels"]
|
||||
|
||||
Reference in New Issue
Block a user