from __future__ import annotations from datetime import UTC, date, datetime 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, ExpenseClaimItem from app.schemas.orchestrator import OrchestratorRequest from app.services.agent_conversations import AgentConversationService from app.services.orchestrator import OrchestratorService 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 test_review_next_step_run_submits_existing_claim_and_returns_draft_payload( monkeypatch, ) -> None: monkeypatch.setattr( "app.services.runtime_chat.RuntimeChatService.complete", lambda *_args, **_kwargs: None, ) session_factory = build_session_factory() with session_factory() as db: manager = Employee( employee_no="E9000", name="李经理", email="manager-next@example.com", ) employee = Employee( employee_no="E9001", name="张三", email="emp-next@example.com", manager=manager, ) claim = ExpenseClaim( id="claim-next-step", claim_no="EXP-202605-001", employee=employee, employee_id=employee.id, employee_name="张三", department_name="销售部", expense_type="office", reason="采购办公用品", location="上海", amount=Decimal("128.00"), currency="CNY", invoice_count=1, occurred_at=datetime(2026, 5, 20, 9, 0, tzinfo=UTC), status="draft", approval_stage="待提交", items=[ ExpenseClaimItem( item_date=date(2026, 5, 20), item_type="office", item_reason="采购办公用品", item_location="上海", item_amount=Decimal("128.00"), invoice_id="office-invoice.png", ) ], ) db.add_all([manager, employee, claim]) db.commit() response = OrchestratorService(db).run( OrchestratorRequest( source="user_message", user_id="emp-next@example.com", message="我已核对右侧识别结果,请进入下一步。", context_json={ "review_action": "next_step", "draft_claim_id": claim.id, "attachment_count": 1, "name": "张三", }, ) ) db.refresh(claim) assert response.status == "succeeded" assert response.requires_confirmation is False assert response.result["draft_payload"]["status"] == "submitted" assert response.result["draft_payload"]["approval_stage"] == "直属领导审批" assert claim.status == "submitted" assert claim.approval_stage == "直属领导审批" assert claim.submitted_at is not None assert response.conversation_id assert AgentConversationService(db).get_conversation(response.conversation_id) is None def test_review_next_step_blocked_returns_reasons_and_removes_next_step_action( monkeypatch, ) -> None: monkeypatch.setattr( "app.services.runtime_chat.RuntimeChatService.complete", lambda *_args, **_kwargs: None, ) session_factory = build_session_factory() with session_factory() as db: employee = Employee( employee_no="E9011", name="张三", email="emp-blocked@example.com", ) claim = ExpenseClaim( id="claim-next-step-blocked", claim_no="EXP-202605-002", employee=employee, employee_id=employee.id, employee_name="张三", department_name="待补充", expense_type="office", reason="采购办公用品", location="上海", amount=Decimal("128.00"), currency="CNY", invoice_count=1, occurred_at=datetime(2026, 5, 20, 9, 0, tzinfo=UTC), status="draft", approval_stage="待提交", items=[ ExpenseClaimItem( item_date=date(2026, 5, 20), item_type="office", item_reason="采购办公用品", item_location="上海", item_amount=Decimal("128.00"), invoice_id="office-invoice.png", ) ], ) db.add_all([employee, claim]) db.commit() response = OrchestratorService(db).run( OrchestratorRequest( source="user_message", user_id="emp-blocked@example.com", message="我已核对右侧识别结果,请进入下一步。", context_json={ "review_action": "next_step", "draft_claim_id": claim.id, "attachment_count": 1, "name": "张三", }, ) ) result = response.result review_payload = result["review_payload"] actions = { str(item.get("action_type") or "").strip() for item in review_payload["confirmation_actions"] } assert response.status == "succeeded" assert result["draft_payload"]["status"] == "draft" assert response.conversation_id assert AgentConversationService(db).get_conversation(response.conversation_id) is not None assert "AI预审暂未通过" in result["answer"] assert "所属部门未完善" in result["answer"] assert "next_step" not in actions assert "save_draft" in actions assert any( "所属部门未完善" in str(item.get("content") or "") for item in review_payload["risk_briefs"] )