refactor(server): split oversized backend services
This commit is contained in:
@@ -80,7 +80,11 @@ def test_activate_pending_rule_endpoint_is_blocked() -> None:
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/agent-assets/{pending_rule.id}/activate",
|
||||
headers={"x-actor": "pytest"},
|
||||
headers={
|
||||
"x-actor": "pytest",
|
||||
"x-auth-username": "pytest",
|
||||
"x-auth-role-codes": "manager",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.schemas.ontology import OntologyParseRequest
|
||||
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead
|
||||
from app.schemas.reimbursement import ExpenseClaimItemCreate, ExpenseClaimItemUpdate, ExpenseClaimUpdate
|
||||
from app.services.agent_conversations import AgentConversationService
|
||||
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
|
||||
from app.services.expense_claims import ExpenseClaimService
|
||||
from app.services.ontology import SemanticOntologyService
|
||||
from app.services.ocr import OcrService
|
||||
@@ -1200,7 +1201,7 @@ def test_update_claim_item_reanalyzes_existing_attachment(monkeypatch, tmp_path)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="office", location="深圳南山")
|
||||
@@ -1296,7 +1297,7 @@ def test_upload_train_ticket_attachment_backfills_item_amount(monkeypatch, tmp_p
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="travel", location="北京")
|
||||
@@ -1390,7 +1391,7 @@ def test_upload_hotel_attachment_audits_date_like_amount(monkeypatch, tmp_path)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="hotel", location="北京")
|
||||
@@ -1469,7 +1470,7 @@ def test_upload_hotel_attachment_flags_amount_over_travel_policy(monkeypatch, tm
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
@@ -1568,10 +1569,14 @@ def test_attachment_risk_flag_message_uses_specific_points(monkeypatch, tmp_path
|
||||
file_path = tmp_path / "invoice.png"
|
||||
file_path.write_bytes(b"fake")
|
||||
service = ExpenseClaimService(db)
|
||||
monkeypatch.setattr(service, "_resolve_attachment_path", lambda storage_key: file_path)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_read_attachment_meta",
|
||||
ExpenseClaimAttachmentStorage,
|
||||
"resolve_path",
|
||||
lambda self, storage_key: file_path,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service._attachment_storage,
|
||||
"read_meta",
|
||||
lambda path: {
|
||||
"analysis": {
|
||||
"severity": "medium",
|
||||
@@ -1635,7 +1640,7 @@ def test_upload_ride_receipt_backfills_item_reason_from_addresses(monkeypatch, t
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="transport", location="深圳")
|
||||
@@ -1696,7 +1701,7 @@ def test_delete_claim_item_removes_row_and_attachment_files(monkeypatch, tmp_pat
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="office", location="深圳南山")
|
||||
@@ -1743,7 +1748,7 @@ def test_delete_claim_removes_all_claim_attachment_files(monkeypatch, tmp_path)
|
||||
role_codes=[],
|
||||
is_admin=False,
|
||||
)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="office", location="深圳南山")
|
||||
@@ -1785,7 +1790,7 @@ def test_attachment_preview_resolves_legacy_filename_in_claim_item_directory(mon
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
claim = build_claim(expense_type="transport", location="上海")
|
||||
@@ -1964,7 +1969,7 @@ def test_submit_claim_routes_high_risk_attachment_to_approval_with_review_flag(
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
@@ -2077,7 +2082,7 @@ def test_submit_claim_routes_travel_route_mismatch_to_approval_with_review_flag(
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
@@ -2228,7 +2233,7 @@ def test_submit_claim_routes_hotel_amount_over_travel_policy_to_approval_with_re
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
with build_session() as db:
|
||||
manager = Employee(
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from datetime import UTC, date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
@@ -25,6 +26,14 @@ def build_session_factory() -> sessionmaker[Session]:
|
||||
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def skip_agent_foundation_bootstrap(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"app.services.agent_foundation.AgentFoundationService.ensure_foundation_ready",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
|
||||
|
||||
def test_review_next_step_run_submits_existing_claim_and_returns_draft_payload(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.models.employee import Employee
|
||||
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
|
||||
from app.models.role import Role
|
||||
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead
|
||||
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
|
||||
from app.services.expense_claims import ExpenseClaimService
|
||||
from app.services.ocr import OcrService
|
||||
|
||||
@@ -134,7 +135,7 @@ def test_claim_item_attachment_upload_preview_and_delete(monkeypatch, tmp_path)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
client, session_factory = build_client()
|
||||
with session_factory() as db:
|
||||
@@ -227,7 +228,7 @@ def test_claim_item_attachment_upload_flags_purpose_and_amount_mismatch(monkeypa
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
client, session_factory = build_client()
|
||||
with session_factory() as db:
|
||||
@@ -273,7 +274,7 @@ def test_claim_item_attachment_upload_flags_non_invoice_image_as_high_risk(monke
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
client, session_factory = build_client()
|
||||
with session_factory() as db:
|
||||
@@ -395,7 +396,7 @@ def test_claim_item_pdf_attachment_preview_returns_generated_image(monkeypatch,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
client, session_factory = build_client()
|
||||
with session_factory() as db:
|
||||
@@ -447,7 +448,7 @@ def test_claim_item_delete_removes_item_and_attachment(monkeypatch, tmp_path) ->
|
||||
)
|
||||
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimService, "_get_attachment_storage_root", lambda self: tmp_path)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||
|
||||
client, session_factory = build_client()
|
||||
with session_factory() as db:
|
||||
|
||||
@@ -16,6 +16,7 @@ from app.schemas.user_agent import UserAgentCitation, UserAgentRequest, UserAgen
|
||||
from app.services.agent_assets import AgentAssetService
|
||||
from app.services.ontology import SemanticOntologyService
|
||||
from app.services.user_agent import UserAgentService
|
||||
from app.services.user_agent_documents import UserAgentDocumentService
|
||||
|
||||
|
||||
def build_session_factory() -> sessionmaker[Session]:
|
||||
@@ -1096,6 +1097,42 @@ def test_user_agent_returns_submitted_draft_payload_for_review_next_step() -> No
|
||||
assert "当前节点为 直属领导审批" in response.answer
|
||||
|
||||
|
||||
def test_user_agent_document_service_normalizes_ocr_fields_and_scene() -> None:
|
||||
document_service = UserAgentDocumentService()
|
||||
|
||||
fields = document_service.extract_document_fields(
|
||||
{
|
||||
"filename": "北京南站火车票.png",
|
||||
"document_type": "train_ticket",
|
||||
"scene_code": "travel",
|
||||
"summary": "电子发票 2026-03-04 广州南至北京南 二等座 票价 ¥560.00 中国铁路",
|
||||
"text": "电子发票 2026-03-04 广州南至北京南 二等座 票价 ¥560.00 中国铁路",
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "票价", "value": "¥560.00"},
|
||||
{"key": "date", "label": "业务发生时间", "value": "2026-03-04"},
|
||||
{"key": "merchant_name", "label": "商户", "value": "中国铁路"},
|
||||
],
|
||||
}
|
||||
)
|
||||
classified = document_service.classify_document(
|
||||
{"filename": "客户餐饮发票.jpg", "summary": "餐饮发票 客户招待 金额 320 元"},
|
||||
expense_type_code="entertainment",
|
||||
has_customer=True,
|
||||
)
|
||||
|
||||
assert fields["金额"] == "560.00元"
|
||||
assert fields["列车出发时间"] == "2026-03-04"
|
||||
assert "商户/酒店" not in fields
|
||||
assert document_service.extract_amount_text_from_value("滴滴出行 支付金额 1 元,实付 13.4 元,订单号 12345678") == "13.40元"
|
||||
assert classified["document_type"] == "meal_receipt"
|
||||
assert classified["expense_type"] == "entertainment"
|
||||
assert document_service.infer_expense_type_from_documents(
|
||||
[{"filename": "客户餐饮发票.jpg", "summary": "餐饮发票 客户招待 金额 320 元"}],
|
||||
expense_type_code="entertainment",
|
||||
has_customer=True,
|
||||
) == "业务招待费"
|
||||
|
||||
|
||||
def test_user_agent_builds_review_payload_for_multi_document_expense_flow() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
|
||||
Reference in New Issue
Block a user