from __future__ import annotations from datetime import UTC, datetime, timedelta from decimal import Decimal from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool from app.db.base import Base from app.models.budget import BudgetAllocation, BudgetTransaction from app.models.financial_record import ExpenseClaim from app.models.risk_observation import RiskObservation from app.services.finance_dashboard import FinanceDashboardService def build_session() -> Session: engine = create_engine( "sqlite+pysqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) Base.metadata.create_all(bind=engine) session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) return session_factory() def test_finance_dashboard_service_aggregates_claim_risk_and_budget_data() -> None: now = datetime.now(UTC) with build_session() as db: db.add_all( [ ExpenseClaim( claim_no="CLM-DASH-001", employee_name="陈雨晴", department_name="财务部", expense_type="travel", reason="项目差旅", location="广州", amount=Decimal("1200.00"), invoice_count=2, occurred_at=now - timedelta(hours=4), submitted_at=now - timedelta(hours=3), status="submitted", approval_stage="finance_review", risk_flags_json=[], hermes_risk_flag=False, created_at=now - timedelta(hours=4), updated_at=now - timedelta(hours=3), ), ExpenseClaim( claim_no="CLM-DASH-002", employee_name="顾成宇", department_name="研发中心", expense_type="meal", reason="客户招待", location="深圳", amount=Decimal("800.00"), invoice_count=1, occurred_at=now - timedelta(days=1, hours=2), submitted_at=now - timedelta(days=1, hours=1), status="paid", approval_stage="payment", risk_flags_json=[{"label": "招待费超标"}], hermes_risk_flag=False, created_at=now - timedelta(days=1, hours=2), updated_at=now - timedelta(days=1), ), ExpenseClaim( claim_no="CLM-DASH-003", employee_name="李文静", department_name="行政部", expense_type="office", reason="办公用品", location="珠海", amount=Decimal("5000.00"), invoice_count=3, occurred_at=now - timedelta(hours=1), submitted_at=None, status="draft", approval_stage=None, risk_flags_json=[], hermes_risk_flag=False, created_at=now - timedelta(hours=1), updated_at=now - timedelta(hours=1), ), ] ) db.add( RiskObservation( observation_key="risk-dashboard-001", subject_type="expense_claim", subject_key="CLM-DASH-002", subject_label="CLM-DASH-002", claim_no="CLM-DASH-002", risk_type="policy", risk_signal="amount_outlier", title="金额异常", risk_level="high", status="pending_review", created_at=now - timedelta(hours=2), updated_at=now - timedelta(hours=2), ) ) allocation = BudgetAllocation( budget_no="BUD-DASH-001", fiscal_year=now.year, period_type="year", period_key=f"{now.year}", department_name="财务部", subject_code="travel", subject_name="差旅费", original_amount=Decimal("10000.00"), adjusted_amount=Decimal("0.00"), status="active", warning_threshold=Decimal("80.00"), control_action="warn", ) db.add(allocation) db.flush() db.add( BudgetTransaction( transaction_no="BTX-DASH-001", allocation_id=allocation.id, source_type="expense_claim", source_id="CLM-DASH-002", source_no="CLM-DASH-002", transaction_type="consume", amount=Decimal("4000.00"), before_available_amount=Decimal("10000.00"), after_available_amount=Decimal("6000.00"), operator="finance", reason="测试消耗", created_at=now - timedelta(hours=1), ) ) db.commit() dashboard = FinanceDashboardService(db).build_dashboard( range_key="近10日", trend_range="近7天", department_range="本月", ) assert dashboard.has_real_data is True assert dashboard.totals["pendingCount"] == 1 assert dashboard.totals["pendingAmount"] == 1200.0 assert dashboard.totals["riskCount"] == 1 assert dashboard.trend["applications"][-1] >= 1 assert dashboard.spend_by_category[0]["value"] == 1200.0 assert dashboard.department_ranking[0]["name"] == "财务部" assert dashboard.department_ranking[0]["amount"] == 1200.0 assert dashboard.budget_summary["ratio"] == 40.0 assert dashboard.budget_summary["used"] == "¥4,000"