from __future__ import annotations from datetime import UTC, date, 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.employee import Employee from app.models.employee_behavior_profile import EmployeeBehaviorProfileSnapshot from app.models.financial_record import ExpenseClaim, ExpenseClaimItem from app.models.organization import OrganizationUnit from app.services.digital_employee_dashboard import DigitalEmployeeDashboardService from app.services.employee_profile_scan_task import EmployeeProfileScanTaskService from app.services.hermes_employee_profile_scanner import HermesEmployeeProfileScannerService def test_hermes_employee_profile_scan_returns_profile_baseline_summary() -> None: session_factory = _build_session_factory() with session_factory() as db: _seed_scan_data(db) summary = HermesEmployeeProfileScannerService(db).scan_employee_profiles(log_id=None) assert summary["target_employee_count"] == 3 assert db.query(EmployeeBehaviorProfileSnapshot).count() >= 12 baseline_summary = summary["baseline_summary"] assert baseline_summary["dimension_counts"]["employee"] == 3 assert baseline_summary["dimension_counts"]["department"] == 1 assert baseline_summary["dimension_counts"]["supplier"] == 2 assert baseline_summary["dimension_counts"]["expense_type"] == 2 assert any( bucket["dimension"] == "supplier" and bucket["key"] == "s-hotel" for bucket in baseline_summary["buckets"] ) def test_employee_profile_scan_task_records_digital_employee_run() -> None: session_factory = _build_session_factory() with session_factory() as db: _seed_scan_data(db) result = EmployeeProfileScanTaskService(db).refresh_profiles() summary = result["summary"] assert result["task_type"] == "employee_behavior_profile_scan" assert summary["target_employee_count"] == 3 assert summary["snapshot_count"] >= 12 assert db.query(EmployeeBehaviorProfileSnapshot).count() >= 12 dashboard = DigitalEmployeeDashboardService(db).build_dashboard(days=7) assert dashboard.totals["profileSnapshots"] >= 12 assert dashboard.task_distribution[0]["taskType"] == "employee_behavior_profile_scan" def _build_session_factory() -> sessionmaker[Session]: engine = create_engine( "sqlite+pysqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) Base.metadata.create_all(bind=engine) return sessionmaker(bind=engine, autoflush=False, autocommit=False) def _seed_scan_data(db: Session) -> None: org = OrganizationUnit( id="dept-sales", unit_code="SALES", name="市场部", unit_type="department", ) employees = [ Employee( id=f"emp-{index}", employee_no=f"E10{index}", name=f"员工{index}", email=f"emp{index}@example.com", position="客户经理", grade="P5", organization_unit=org, ) for index in range(1, 4) ] db.add(org) db.add_all(employees) now = datetime.now(UTC) claims = [ _claim("c1", employees[0], "travel", "600", "s-hotel", "Hotel A", now), _claim("c2", employees[1], "travel", "900", "s-hotel", "Hotel A", now), _claim("c3", employees[2], "meal", "300", "s-meal", "Meal B", now), ] db.add_all(claims) db.commit() def _claim( claim_id: str, employee: Employee, expense_type: str, amount: str, supplier_id: str, supplier_name: str, now: datetime, ) -> ExpenseClaim: return ExpenseClaim( id=claim_id, claim_no=f"EXP-{claim_id}", employee_id=employee.id, employee_name=employee.name, department_id="dept-sales", department_name="市场部", project_code="PRJ-001", expense_type=expense_type, reason="客户拜访", location="北京", amount=Decimal(amount), currency="CNY", invoice_count=1, occurred_at=now - timedelta(days=5), submitted_at=now - timedelta(days=5), status="submitted", approval_stage="直属领导审批", risk_flags_json=[ { "supplier_id": supplier_id, "supplier_name": supplier_name, } ], items=[ ExpenseClaimItem( id=f"item-{claim_id}", claim_id=claim_id, item_date=date.today(), item_type=expense_type, item_reason="客户拜访", item_location="北京", item_amount=Decimal(amount), invoice_id=f"invoice-{claim_id}", ) ], )