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.employee import Employee from app.models.financial_record import ExpenseClaim from app.models.role import Role from app.services.digital_employee_dashboard import DigitalEmployeeDashboardService from app.services.digital_employee_reminder_task import DigitalEmployeeReminderTaskService 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_digital_employee_reminder_task_generates_actionable_report() -> None: now = datetime(2026, 6, 2, 2, 0, tzinfo=UTC) with build_session() as db: _seed_reminder_data(db, now) result = DigitalEmployeeReminderTaskService(db).refresh_reminders(now=now) summary = result["summary"] report = result["report"] assert result["task_type"] == "digital_employee_reminder_scan" assert summary["recipient_count"] >= 3 assert summary["reminder_count"] >= 4 assert summary["approval_pending_count"] == 1 assert summary["budget_reminder_count"] == 1 assert summary["travel_application_reminder_count"] == 1 assert summary["reimbursement_overdue_count"] == 1 reminder_types = { reminder["type"] for recipient in report["recipients"] for reminder in recipient["reminders"] } assert { "approval_pending", "budget_compilation", "travel_application_expiry", "reimbursement_overdue", }.issubset(reminder_types) dashboard = DigitalEmployeeDashboardService(db).build_dashboard(days=7) assert dashboard.totals["reminders"] >= 4 assert dashboard.totals["businessOutputs"] >= 4 assert dashboard.task_distribution[0]["taskType"] == "digital_employee_reminder_scan" def _seed_reminder_data(db: Session, now: datetime) -> None: budget_role = Role( id="role-budget", role_code="budget_monitor", name="预算管理员", description="预算编制提醒接收人", ) manager = Employee( id="emp-manager", employee_no="M001", name="审批领导", email="manager@example.com", position="部门负责人", grade="M2", ) employee = Employee( id="emp-user", employee_no="E001", name="出差员工", email="employee@example.com", position="客户经理", grade="P5", manager=manager, finance_owner_name="财务BP", ) budget_admin = Employee( id="emp-budget", employee_no="B001", name="预算管理员甲", email="budget@example.com", position="预算管理员", grade="P6", roles=[budget_role], ) db.add_all([budget_role, manager, employee, budget_admin]) db.add_all( [ _claim( "claim-approval", "EXP-APPROVAL-001", employee, "travel", "12000.00", now - timedelta(days=3), "submitted", "直属领导审批", ), _claim( "claim-travel-app", "APP-TRAVEL-001", employee, "travel_application", "8000.00", now - timedelta(days=1), "approved", "已审批", risk_flags=[ { "source": "application_detail", "application_detail": { "application_type": "差旅申请", "time": "2026-06-01", }, } ], ), _claim( "claim-supplement", "EXP-SUPPLEMENT-001", employee, "meal", "600.00", now - timedelta(days=2), "returned", "材料待补", ), ] ) db.commit() def _claim( claim_id: str, claim_no: str, employee: Employee, expense_type: str, amount: str, happened_at: datetime, status: str, approval_stage: str, *, risk_flags: list[dict] | None = None, ) -> ExpenseClaim: return ExpenseClaim( id=claim_id, claim_no=claim_no, employee_id=employee.id, employee_name=employee.name, department_name="市场部", expense_type=expense_type, reason="客户拜访", location="上海", amount=Decimal(amount), invoice_count=1, occurred_at=happened_at, submitted_at=happened_at, status=status, approval_stage=approval_stage, risk_flags_json=risk_flags or [], created_at=happened_at, updated_at=happened_at, )