Files
X-Financial/server/tests/test_linked_reimbursement_draft_jobs.py

297 lines
12 KiB
Python
Raw Normal View History

from __future__ import annotations
from collections.abc import Generator
from datetime import UTC, datetime
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.api.v1.endpoints import linked_reimbursement_draft_jobs as draft_jobs_endpoint
from app.main import create_app
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.schemas.orchestrator import OrchestratorResponse, OrchestratorTraceSummary
from app.services.linked_reimbursement_draft_jobs import clear_linked_reimbursement_draft_jobs_for_tests
from app.services.orchestrator import OrchestratorService
from app.test_helpers.db import build_in_memory_session_factory
def seed_employee_and_application(db: Session) -> None:
employee = Employee(
id="emp-linked-draft-fast",
employee_no="E10001",
name="张三",
email="zhangsan@example.com",
position="实施顾问",
grade="P5",
)
application = ExpenseClaim(
id="application-linked-draft-fast",
claim_no="AP-202606-FAST",
employee_id=employee.id,
employee_name=employee.name,
department_id="dept-delivery",
department_name="交付部",
project_code=None,
expense_type="travel_application",
reason="支撑国网仿生产服务器部署",
location="上海",
amount=3000,
currency="CNY",
invoice_count=0,
occurred_at=datetime(2026, 2, 20, tzinfo=UTC),
submitted_at=None,
status="approved",
approval_stage="已完成",
risk_flags_json=[],
)
db.add_all([employee, application])
db.commit()
def build_client(monkeypatch) -> tuple[TestClient, object]:
session_factory = build_in_memory_session_factory()
app = create_app()
def override_db() -> Generator[Session, None, None]:
db = session_factory()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_db
monkeypatch.setattr(draft_jobs_endpoint, "get_session_factory", lambda: session_factory)
return TestClient(app), session_factory
def test_linked_reimbursement_draft_job_runs_after_conversation_leaves(monkeypatch) -> None:
clear_linked_reimbursement_draft_jobs_for_tests()
captured_messages = []
def fake_run(self, payload):
captured_messages.append(payload.message)
return OrchestratorResponse(
run_id="run-linked-draft-job",
conversation_id=None,
selected_agent="user_agent",
route_reason="测试后台生成报销草稿。",
permission_level="draft_write",
status="succeeded",
result={
"message": "报销草稿已生成。",
"draft_payload": {
"claim_id": "draft-linked-1",
"claim_no": "RE-202606-009",
"status": "draft",
"expense_type": "travel",
},
},
requires_confirmation=False,
trace_summary=OrchestratorTraceSummary(
scenario="expense",
intent="draft",
tool_count=1,
failed_tool_count=0,
selected_capability_codes=[],
degraded=False,
),
)
monkeypatch.setattr(OrchestratorService, "run", fake_run)
try:
client, _session_factory = build_client(monkeypatch)
headers = {
"x-auth-username": "zhangsan@example.com",
"x-auth-name": "Zhang San",
"x-auth-employee-no": "E10001",
"x-auth-role-codes": "user",
}
response = client.post(
"/api/v1/reimbursements/linked-reimbursement-draft-jobs",
headers=headers,
json={
"message": "我要报销\n用户选择报销场景:差旅费\n关联申请单AP-202606-001",
"conversation_id": "inline-test",
"context_json": {
"review_action": "save_draft",
"expense_scene_selection": {
"expense_type": "travel",
"expense_type_label": "差旅费",
"application_claim_no": "AP-202606-001",
},
"review_form_values": {
"application_claim_no": "AP-202606-001",
},
},
},
)
assert response.status_code == 202
job_id = response.json()["job_id"]
status_response = client.get(
f"/api/v1/reimbursements/linked-reimbursement-draft-jobs/{job_id}",
headers=headers,
)
assert status_response.status_code == 200
payload = status_response.json()
assert payload["status"] == "succeeded"
assert payload["draft_payload"]["claim_no"] == "RE-202606-009"
assert payload["run_id"] == "run-linked-draft-job"
assert captured_messages == ["我要报销\n用户选择报销场景:差旅费\n关联申请单AP-202606-001"]
finally:
clear_linked_reimbursement_draft_jobs_for_tests()
def test_linked_reimbursement_draft_job_uses_direct_save_path(monkeypatch) -> None:
clear_linked_reimbursement_draft_jobs_for_tests()
def fail_if_orchestrator_runs(self, payload):
raise AssertionError("linked draft job should not run full orchestrator")
monkeypatch.setattr(OrchestratorService, "run", fail_if_orchestrator_runs)
try:
client, session_factory = build_client(monkeypatch)
with session_factory() as db:
seed_employee_and_application(db)
headers = {
"x-auth-username": "zhangsan@example.com",
"x-auth-name": "Zhang San",
"x-auth-employee-no": "E10001",
"x-auth-role-codes": "user",
}
response = client.post(
"/api/v1/reimbursements/linked-reimbursement-draft-jobs",
headers=headers,
json={
"message": "我要报销\n用户选择报销场景:差旅费\n关联申请单AP-202606-FAST",
"conversation_id": "inline-fast-test",
"context_json": {
"name": "张三",
"review_action": "save_draft",
"expense_scene_selection": {
"expense_type": "travel",
"expense_type_label": "差旅费",
"application_claim_id": "application-linked-draft-fast",
"application_claim_no": "AP-202606-FAST",
},
"review_form_values": {
"expense_type": "差旅费",
"reason": "支撑国网仿生产服务器部署",
"location": "上海",
"time_range": "2026-02-20 至 2026-02-23",
"application_claim_id": "application-linked-draft-fast",
"application_claim_no": "AP-202606-FAST",
"application_reason": "支撑国网仿生产服务器部署",
"application_location": "上海",
"application_amount": "3000",
"application_amount_label": "¥3,000",
"application_business_time": "2026-02-20 至 2026-02-23",
},
},
},
)
assert response.status_code == 202
job_id = response.json()["job_id"]
status_response = client.get(
f"/api/v1/reimbursements/linked-reimbursement-draft-jobs/{job_id}",
headers=headers,
)
assert status_response.status_code == 200
payload = status_response.json()
assert payload["status"] == "succeeded"
assert payload["draft_payload"]["claim_no"]
assert payload["draft_payload"]["claim_id"]
assert payload["run_id"].startswith("linked-reimbursement-draft-")
with session_factory() as db:
draft = db.get(ExpenseClaim, payload["draft_payload"]["claim_id"])
assert draft is not None
assert draft.status == "draft"
assert draft.expense_type == "travel"
assert draft.reason == "支撑国网仿生产服务器部署"
assert draft.items == []
finally:
clear_linked_reimbursement_draft_jobs_for_tests()
def test_linked_reimbursement_draft_job_uses_direct_save_path_with_application_no_only(monkeypatch) -> None:
clear_linked_reimbursement_draft_jobs_for_tests()
def fail_if_orchestrator_runs(self, payload):
raise AssertionError("linked draft job should resolve application no without full orchestrator")
monkeypatch.setattr(OrchestratorService, "run", fail_if_orchestrator_runs)
try:
client, session_factory = build_client(monkeypatch)
with session_factory() as db:
seed_employee_and_application(db)
headers = {
"x-auth-username": "zhangsan@example.com",
"x-auth-name": "Zhang San",
"x-auth-employee-no": "E10001",
"x-auth-role-codes": "user",
}
response = client.post(
"/api/v1/reimbursements/linked-reimbursement-draft-jobs",
headers=headers,
json={
"message": "我要报销\n用户选择报销场景:差旅费\n关联申请单AP-202606-FAST",
"conversation_id": "inline-fast-no-id-test",
"context_json": {
"name": "张三",
"review_action": "save_draft",
"expense_scene_selection": {
"expense_type": "travel",
"expense_type_label": "差旅费",
"application_claim_no": "AP-202606-FAST",
},
"review_form_values": {
"expense_type": "差旅费",
"reason": "支撑国网仿生产服务器部署",
"location": "上海",
"time_range": "2026-02-20 至 2026-02-23",
"application_claim_no": "AP-202606-FAST",
"application_reason": "支撑国网仿生产服务器部署",
"application_location": "上海",
"application_amount": "3000",
},
},
},
)
assert response.status_code == 202
job_id = response.json()["job_id"]
status_response = client.get(
f"/api/v1/reimbursements/linked-reimbursement-draft-jobs/{job_id}",
headers=headers,
)
assert status_response.status_code == 200
payload = status_response.json()
assert payload["status"] == "succeeded"
assert payload["draft_payload"]["claim_no"]
assert payload["draft_payload"]["claim_id"]
with session_factory() as db:
draft = db.get(ExpenseClaim, payload["draft_payload"]["claim_id"])
assert draft is not None
link_flag = next(
flag
for flag in draft.risk_flags_json
if flag.get("source") == "application_link"
)
assert link_flag["application_claim_no"] == "AP-202606-FAST"
assert link_flag["application_claim_id"] == "application-linked-draft-fast"
finally:
clear_linked_reimbursement_draft_jobs_for_tests()