fix(expenses): unify AI application submission transaction

This commit is contained in:
caoxiaozhu
2026-07-13 15:35:08 +08:00
parent 653eda0596
commit a616b30cb2
7 changed files with 138 additions and 21 deletions

View File

@@ -6,24 +6,31 @@ from collections.abc import Generator
from datetime import UTC, date, datetime
from decimal import Decimal
import pytest
from auth_helpers import install_legacy_header_auth_override
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.api.deps import get_db
from app.db.base import Base
from app.main import create_app
from app.models.budget import BudgetAllocation, BudgetReservation, BudgetTransaction
from app.models.employee import Employee
from app.models.expense_case import BusinessEvent, ExpenseCaseLink
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.organization import OrganizationUnit
from app.models.risk_observation import RiskObservation, RiskObservationFeedback
from app.models.role import Role
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead
from app.schemas.ontology import OntologyParseResult
from app.schemas.user_agent import UserAgentRequest
from app.services.document_preview import DocumentPreviewAssets
from app.services.expense_cases import ExpenseCaseService
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
from app.services.ocr import OcrService
from app.services.user_agent import UserAgentService
def build_session_factory() -> sessionmaker[Session]:
@@ -891,6 +898,7 @@ def test_application_preview_action_submits_without_orchestrator_run(monkeypatch
"x-auth-name": "Zhang San",
"x-auth-employee-no": "E10001",
"x-auth-role-codes": "user",
"x-auth-department": "Marketing",
},
json={
"source": "user_message",
@@ -948,6 +956,83 @@ def test_application_preview_action_submits_without_orchestrator_run(monkeypatch
assert claim is not None
assert claim.status == "submitted"
assert claim.employee_name == "张三"
reservation = db.scalar(
select(BudgetReservation).where(
BudgetReservation.source_type == "application",
BudgetReservation.source_id == claim.id,
BudgetReservation.source_status == "active",
)
)
assert reservation is not None
assert reservation.amount == Decimal("1000.00")
case_link = db.scalar(
select(ExpenseCaseLink).where(
ExpenseCaseLink.resource_type == "expense_claim",
ExpenseCaseLink.resource_id == claim.id,
)
)
assert case_link is not None
event = db.scalar(
select(BusinessEvent).where(
BusinessEvent.aggregate_id == claim.id,
BusinessEvent.event_type == "application_submitted",
)
)
assert event is not None
assert event.expense_case_id == case_link.expense_case_id
assert event.payload_json["previous_status"] == "draft"
assert event.payload_json["next_status"] == "submitted"
assert any(
isinstance(flag, dict)
and flag.get("event_type") == "expense_application_submission"
for flag in list(claim.risk_flags_json or [])
)
def test_application_direct_submit_rolls_back_budget_when_case_event_fails(
monkeypatch,
) -> None:
session_factory = build_session_factory()
def fail_event(*_args, **_kwargs):
raise RuntimeError("模拟费用事件写入失败")
monkeypatch.setattr(ExpenseCaseService, "record_claim_event", fail_event)
request = UserAgentRequest(
run_id="application-submit-rollback",
user_id="rollback@example.com",
message="直接提交",
ontology=OntologyParseResult(run_id="application-submit-rollback"),
context_json={
"session_type": "application",
"name": "回滚测试用户",
"department_name": "Test Department",
},
)
facts = {
"application_type": "差旅费用申请",
"time": "2026-07-01 至 2026-07-03",
"location": "北京",
"reason": "验证提交失败回滚",
"amount": "1000元",
}
with session_factory() as db:
with pytest.raises(RuntimeError, match="模拟费用事件写入失败"):
UserAgentService(db)._create_expense_application_record(
request,
facts,
submit=True,
)
# 模拟外层捕获异常后继续记录工具日志并提交,失败草稿仍不能被顺带落库。
db.commit()
assert list(db.scalars(select(ExpenseClaim)).all()) == []
assert list(db.scalars(select(BudgetAllocation)).all()) == []
assert list(db.scalars(select(BudgetReservation)).all()) == []
assert list(db.scalars(select(BudgetTransaction)).all()) == []
assert list(db.scalars(select(ExpenseCaseLink)).all()) == []
assert list(db.scalars(select(BusinessEvent)).all()) == []
def test_application_preview_action_saves_draft_with_detail_reference(monkeypatch, tmp_path) -> None:
@@ -1026,3 +1111,12 @@ def test_application_preview_action_saves_draft_with_detail_reference(monkeypatc
assert claim.approval_stage == "待提交"
assert claim.submitted_at is None
assert claim.employee_name == "张三"
assert db.scalar(
select(BudgetReservation).where(BudgetReservation.source_id == claim.id)
) is None
assert db.scalar(
select(BusinessEvent).where(BusinessEvent.aggregate_id == claim.id)
) is None
assert db.scalar(
select(ExpenseCaseLink).where(ExpenseCaseLink.resource_id == claim.id)
) is None

View File

@@ -627,7 +627,7 @@ def test_user_agent_application_submit_enters_leader_review() -> None:
response = build_application_user_agent_response(
db,
"确认提交",
context_overrides={"manager_name": "陈硕"},
context_overrides={"manager_name": "陈硕", "department_name": "技术部"},
history=[
{"role": "user", "content": initial_message},
{"role": "user", "content": "飞机"},
@@ -683,7 +683,7 @@ def test_user_agent_application_submit_blocks_duplicate_business_time() -> None:
first_response = build_application_user_agent_response(
db,
"确认提交",
context_overrides={"manager_name": "陈硕"},
context_overrides={"manager_name": "陈硕", "department_name": "技术部"},
history=history,
)
first_claim = application_claim_query(db).one()
@@ -691,7 +691,7 @@ def test_user_agent_application_submit_blocks_duplicate_business_time() -> None:
second_response = build_application_user_agent_response(
db,
"确认提交",
context_overrides={"manager_name": "陈硕"},
context_overrides={"manager_name": "陈硕", "department_name": "技术部"},
history=history,
)
@@ -742,6 +742,7 @@ def test_user_agent_application_submit_blocks_overlapping_travel_dates() -> None
"确认提交",
context_overrides={
"manager_name": "向万红",
"department_name": "技术部",
"application_preview": {
"fields": {
"applicationType": "差旅费用申请",
@@ -772,6 +773,7 @@ def test_user_agent_application_submit_normalizes_location_mixed_with_business_c
"确认提交",
context_overrides={
"manager_name": "向万红",
"department_name": "技术部",
"application_preview": {
"fields": {
"applicationType": "差旅费用申请",