2026-05-30 15:46:51 +08:00
|
|
|
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
|
2026-06-03 09:25:23 +08:00
|
|
|
from app.services.digital_employee_dashboard import DigitalEmployeeDashboardService
|
|
|
|
|
from app.services.employee_profile_scan_task import EmployeeProfileScanTaskService
|
2026-05-30 15:46:51 +08:00
|
|
|
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"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 09:25:23 +08:00
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
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}",
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
)
|