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

902 lines
33 KiB
Python
Raw Normal View History

from __future__ import annotations
import base64
from collections.abc import Generator
from datetime import UTC, date, datetime
from decimal import Decimal
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
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.employee import Employee
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.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
from app.services.expense_claims import ExpenseClaimService
from app.services.ocr import OcrService
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 build_client() -> tuple[TestClient, sessionmaker[Session]]:
session_factory = build_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
return TestClient(app), session_factory
def seed_claim(db: Session) -> tuple[ExpenseClaim, ExpenseClaimItem]:
manager = Employee(
id="mgr-1",
employee_no="E20001",
name="李总",
email="manager@example.com",
position="市场总监",
grade="P7",
)
role = Role(
id="role-user",
role_code="user",
name="员工",
description="普通员工",
)
employee = Employee(
id="emp-1",
employee_no="E10001",
name="张三",
email="zhangsan@example.com",
position="招商主管",
grade="P4",
manager=manager,
roles=[role],
)
claim = ExpenseClaim(
id="claim-attachment-1",
claim_no="EXP-202605-101",
employee_id=employee.id,
employee_name="张三",
department_id="dept-1",
department_name="市场部",
project_code=None,
expense_type="office",
reason="办公用品采购",
location="深圳南山",
amount=Decimal("88.00"),
currency="CNY",
invoice_count=0,
occurred_at=datetime(2026, 5, 13, tzinfo=UTC),
submitted_at=None,
status="draft",
approval_stage="待提交",
risk_flags_json=[],
)
item = ExpenseClaimItem(
id="item-attachment-1",
claim_id=claim.id,
item_date=date(2026, 5, 13),
item_type="office",
item_reason="办公用品采购",
item_location="深圳南山",
item_amount=Decimal("88.00"),
invoice_id=None,
)
claim.items = [item]
db.add(manager)
db.add(role)
db.add(employee)
db.add(claim)
db.commit()
return claim, item
def test_claim_read_uses_organization_manager_and_dedupes_budget_warnings() -> None:
client, session_factory = build_client()
with session_factory() as db:
department = OrganizationUnit(
id="dept-org-manager",
unit_code="DEPT-ORG-MANAGER",
name="交付部",
manager_name="王总",
)
employee = Employee(
id="emp-org-manager",
employee_no="E30001",
name="赵六",
email="zhaoliu@example.com",
organization_unit=department,
position="实施顾问",
grade="P5",
finance_owner_name="Wang Finance",
)
duplicated_warning = {
"source": "budget_control",
"event_type": "budget_warning",
"severity": "medium",
"label": "预算接近预警线",
"message": "预算 SIM-BUD-2026-R0048 本次占用后使用率预计达到 99.27%,已达到预警线 80.00%",
"budget_no": "SIM-BUD-2026-R0048",
"allocation_id": "allocation-0048",
"subject_code": "travel",
}
claim = ExpenseClaim(
id="claim-org-manager",
claim_no="EXP-202606-ORG-MGR",
employee_id=employee.id,
employee_name=employee.name,
department_id=department.id,
department_name=department.name,
project_code=None,
expense_type="travel",
reason="差旅报销",
location="上海",
amount=Decimal("880.00"),
currency="CNY",
invoice_count=1,
occurred_at=datetime(2026, 6, 3, tzinfo=UTC),
submitted_at=datetime(2026, 6, 3, 10, 0, tzinfo=UTC),
status="submitted",
approval_stage="直属领导审批",
risk_flags_json=[
{**duplicated_warning, "created_at": "2026-06-03T10:00:00+00:00"},
{**duplicated_warning, "created_at": "2026-06-03T10:01:00+00:00"},
],
)
db.add_all([department, employee, claim])
db.commit()
headers = {"x-auth-username": "zhaoliu@example.com"}
response = client.get("/api/v1/reimbursements/claims/claim-org-manager", headers=headers)
assert response.status_code == 200
payload = response.json()
assert payload["manager_name"] == "王总"
assert payload["finance_owner_name"] == "Wang Finance"
budget_warnings = [
flag
for flag in payload["risk_flags_json"]
if flag.get("source") == "budget_control" and flag.get("event_type") == "budget_warning"
]
assert len(budget_warnings) == 1
assert budget_warnings[0]["message"] == duplicated_warning["message"]
def test_claim_read_attaches_finance_approver_name_for_finance_stage() -> None:
client, session_factory = build_client()
with session_factory() as db:
finance_role = Role(
id="role-finance-reader",
role_code="finance",
name="财务",
description="可处理财务复核任务",
)
applicant = Employee(
id="emp-finance-stage-applicant",
employee_no="E30002",
name="钱七",
email="qianqi@example.com",
position="实施顾问",
grade="P5",
finance_owner_name="Wang Finance Group",
)
finance_user = Employee(
id="emp-finance-stage-approver",
employee_no="F30002",
name="Wang Finance",
email="wang.finance@example.com",
position="财务专员",
grade="P6",
finance_owner_name="Wang Finance Group",
roles=[finance_role],
)
claim = ExpenseClaim(
id="claim-finance-stage-reader",
claim_no="EXP-202606-FINANCE-MGR",
employee_id=applicant.id,
employee_name=applicant.name,
department_id=None,
department_name="交付部",
project_code=None,
expense_type="travel",
reason="差旅报销",
location="上海",
amount=Decimal("880.00"),
currency="CNY",
invoice_count=1,
occurred_at=datetime(2026, 6, 3, tzinfo=UTC),
submitted_at=datetime(2026, 6, 3, 10, 0, tzinfo=UTC),
status="submitted",
approval_stage="财务审批",
risk_flags_json=[],
)
db.add_all([finance_role, applicant, finance_user, claim])
db.commit()
headers = {"x-auth-username": "qianqi@example.com"}
response = client.get("/api/v1/reimbursements/claims/claim-finance-stage-reader", headers=headers)
assert response.status_code == 200
payload = response.json()
assert payload["finance_owner_name"] == "Wang Finance Group"
assert payload["finance_approver_name"] == "Wang Finance"
def test_claim_standard_adjustment_endpoint_recalculates_and_marks_reviewer_notice() -> None:
client, session_factory = build_client()
with session_factory() as db:
claim, item = seed_claim(db)
claim.expense_type = "hotel"
claim.location = "北京"
claim.amount = Decimal("1000.00")
item.item_type = "hotel_ticket"
item.item_reason = "北京住宿"
item.item_location = "北京"
item.item_amount = Decimal("1000.00")
db.commit()
claim_id = claim.id
item_id = item.id
response = client.post(
f"/api/v1/reimbursements/claims/{claim_id}/standard-adjustment",
json={
"risks": [
{
"risk_id": "risk-hotel-endpoint-1",
"item_id": item_id,
"title": "住宿超标待说明",
"risk": "住宿票据金额超过职级标准。",
"application_days": 2,
"original_amount": "1000.00",
"reimbursable_amount": "1000.00",
}
]
},
headers={"x-auth-username": "emp-1", "x-auth-name": "Zhang San", "x-auth-grade": "P4"},
)
assert response.status_code == 200
payload = response.json()
assert payload["amount"] == "900.00"
standard_flag = next(
flag
for flag in payload["risk_flags_json"]
if isinstance(flag, dict) and flag.get("source") == "reimbursement_standard_adjustment"
)
assert standard_flag["original_amount"] == "1000.00"
assert standard_flag["reimbursable_amount"] == "900.00"
assert standard_flag["employee_absorbed_amount"] == "100.00"
assert standard_flag["visibility_scope"] == "leader"
def test_claim_item_attachment_upload_preview_and_delete(monkeypatch, tmp_path) -> None:
def fake_recognize(
self,
files: list[tuple[str, bytes, str | None]],
) -> OcrRecognizeBatchRead:
assert files[0][0] == "office-note.png"
return OcrRecognizeBatchRead(
total_file_count=1,
success_count=1,
documents=[
OcrRecognizeDocumentRead(
filename="office-note.png",
media_type="image/png",
text="办公用品发票 金额88元 2026-05-13",
summary="识别到办公用品发票,金额 88 元。",
avg_score=0.98,
line_count=1,
page_count=1,
warnings=[],
)
],
)
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
client, session_factory = build_client()
with session_factory() as db:
claim, item = seed_claim(db)
claim_id = claim.id
item_id = item.id
headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
file_bytes = b"fake-image-bytes"
upload_response = client.post(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
headers=headers,
files=[("file", ("office-note.png", file_bytes, "image/png"))],
)
assert upload_response.status_code == 200
upload_payload = upload_response.json()
assert upload_payload["attachment"]["file_name"] == "office-note.png"
assert upload_payload["attachment"]["analysis"]["label"] == "AI提示符合条件"
assert upload_payload["attachment"]["document_info"]["document_type"] == "office_invoice"
assert upload_payload["attachment"]["requirement_check"]["matches"] is True
assert upload_payload["invoice_id"]
assert upload_payload["item_type"] == "office"
assert upload_payload["item_reason"] == "识别到办公用品发票,金额 88 元。"
assert upload_payload["item_location"] == "深圳南山"
assert upload_payload["item_date"] == "2026-05-13"
assert upload_payload["item_amount"] == "88.00"
meta_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/meta",
headers=headers,
)
assert meta_response.status_code == 200
meta_payload = meta_response.json()
assert meta_payload["media_type"] == "image/png"
assert meta_payload["preview_kind"] == "image"
assert meta_payload["preview_url"].endswith(f"/reimbursements/claims/{claim_id}/items/{item_id}/attachment/preview")
assert meta_payload["analysis"]["headline"]
assert meta_payload["document_info"]["fields"][0]["label"] == "金额"
preview_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/preview",
headers=headers,
)
assert preview_response.status_code == 200
assert preview_response.content == file_bytes
content_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
headers=headers,
)
assert content_response.status_code == 200
assert content_response.content == file_bytes
delete_response = client.delete(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
headers=headers,
)
assert delete_response.status_code == 200
assert delete_response.json()["invoice_id"] is None
deleted_meta_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/meta",
headers=headers,
)
assert deleted_meta_response.status_code == 404
def test_claim_item_attachment_upload_flags_purpose_and_amount_mismatch(monkeypatch, tmp_path) -> None:
def fake_recognize(
self,
files: list[tuple[str, bytes, str | None]],
) -> OcrRecognizeBatchRead:
return OcrRecognizeBatchRead(
total_file_count=1,
success_count=1,
documents=[
OcrRecognizeDocumentRead(
filename="taxi-note.png",
media_type="image/png",
text="滴滴出行电子发票 金额120元 2026-05-13",
summary="识别到交通出行发票,金额 120 元。",
avg_score=0.97,
line_count=1,
page_count=1,
warnings=[],
)
],
)
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
client, session_factory = build_client()
with session_factory() as db:
claim, item = seed_claim(db)
claim_id = claim.id
item_id = item.id
headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
upload_response = client.post(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
headers=headers,
files=[("file", ("taxi-note.png", b"fake-image-bytes", "image/png"))],
)
assert upload_response.status_code == 200
analysis = upload_response.json()["attachment"]["analysis"]
assert analysis["severity"] == "high"
assert any("金额字段" in point for point in analysis["points"])
assert any("附件类型要求" in point for point in analysis["points"])
assert upload_response.json()["attachment"]["requirement_check"]["matches"] is False
def test_claim_item_attachment_upload_flags_non_invoice_image_as_high_risk(monkeypatch, tmp_path) -> None:
def fake_recognize(
self,
files: list[tuple[str, bytes, str | None]],
) -> OcrRecognizeBatchRead:
return OcrRecognizeBatchRead(
total_file_count=1,
success_count=1,
documents=[
OcrRecognizeDocumentRead(
filename="random-image.png",
media_type="image/png",
text="",
summary="",
avg_score=0.0,
line_count=0,
page_count=1,
warnings=[],
)
],
)
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
client, session_factory = build_client()
with session_factory() as db:
claim, item = seed_claim(db)
claim_id = claim.id
item_id = item.id
headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
upload_response = client.post(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
headers=headers,
files=[("file", ("random-image.png", b"fake-image-bytes", "image/png"))],
)
assert upload_response.status_code == 200
analysis = upload_response.json()["attachment"]["analysis"]
assert analysis["severity"] == "high"
assert any("附件内容" in point for point in analysis["points"])
def test_approve_claim_endpoint_routes_direct_manager_claim_to_finance_review() -> None:
client, session_factory = build_client()
with session_factory() as db:
manager = Employee(
id="mgr-approve-1",
employee_no="E21001",
name="李经理",
email="manager-approve-api@example.com",
)
employee = Employee(
id="emp-approve-1",
employee_no="E11001",
name="张三",
email="zhangsan-approve-api@example.com",
manager=manager,
)
claim = ExpenseClaim(
id="claim-approve-1",
claim_no="EXP-APP-API-001",
employee_id=employee.id,
employee_name="张三",
department_id="dept-1",
department_name="市场部",
project_code=None,
expense_type="transport",
reason="交通报销",
location="上海",
amount=Decimal("88.00"),
currency="CNY",
invoice_count=1,
occurred_at=datetime(2026, 5, 13, tzinfo=UTC),
submitted_at=datetime(2026, 5, 13, 10, 0, tzinfo=UTC),
status="submitted",
approval_stage="直属领导审批",
risk_flags_json=[],
)
db.add_all([manager, employee, claim])
db.commit()
response = client.post(
"/api/v1/reimbursements/claims/claim-approve-1/approve",
json={"opinion": "情况属实,同意报销。"},
headers={
"X-Auth-Username": "manager-approve-api@example.com",
"X-Auth-Name": "manager-approve-api@example.com",
"X-Auth-Role-Codes": "manager",
},
)
assert response.status_code == 200
payload = response.json()
assert payload["status"] == "submitted"
assert payload["approval_stage"] == "财务审批"
assert any(
item["source"] == "manual_approval"
and item["opinion"] == "情况属实,同意报销。"
and item["operator"] == "李经理"
and item["next_approval_stage"] == "财务审批"
for item in payload["risk_flags_json"]
)
approval_events = [
item
for item in payload["risk_flags_json"]
if item["source"] == "manual_approval"
]
assert approval_events[0]["operator"] == "李经理"
assert "manager-approve-api@example.com" not in approval_events[0]["message"]
def test_approve_application_endpoint_routes_direct_manager_review_to_budget_review() -> None:
client, session_factory = build_client()
with session_factory() as db:
department = OrganizationUnit(
id="dept-1",
unit_code="DELIVERY-API",
name="交付部",
unit_type="department",
)
budget_role = Role(
id="role-budget-application-approve-1",
role_code="budget_monitor",
name="预算监控员",
)
manager = Employee(
id="mgr-application-approve-1",
employee_no="E21002",
name="李经理",
email="manager-application-approve-api@example.com",
organization_unit=department,
)
budget_manager = Employee(
id="budget-application-approve-1",
employee_no="E31002",
name="赵预算",
email="budget-application-approve-api@example.com",
grade="P8",
organization_unit=department,
roles=[budget_role],
)
employee = Employee(
id="emp-application-approve-1",
employee_no="E11002",
name="张三",
email="zhangsan-application-approve-api@example.com",
manager=manager,
organization_unit=department,
)
claim = ExpenseClaim(
id="claim-application-approve-1",
claim_no="APP-20260525-API001",
employee_id=employee.id,
employee_name="张三",
department_id="dept-1",
department_name="交付部",
project_code=None,
expense_type="travel_application",
reason="支撑国网服务器上线部署",
location="上海",
amount=Decimal("12000.00"),
currency="CNY",
invoice_count=0,
occurred_at=datetime(2026, 5, 25, tzinfo=UTC),
submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC),
status="submitted",
approval_stage="直属领导审批",
risk_flags_json=[
{
"source": "submission_review",
"severity": "high",
"label": "申请风险复核",
"message": "申请金额和行程安排需要预算管理者二次确认。",
}
],
)
db.add_all([department, budget_role, manager, budget_manager, employee, claim])
db.commit()
response = client.post(
"/api/v1/reimbursements/claims/claim-application-approve-1/approve",
json={"opinion": "业务必要,同意申请。"},
headers={
"X-Auth-Username": "manager-application-approve-api@example.com",
"X-Auth-Name": "manager-application-approve-api@example.com",
"X-Auth-Role-Codes": "manager",
},
)
assert response.status_code == 200
payload = response.json()
assert payload["status"] == "submitted"
assert payload["approval_stage"] == "预算管理者审批"
assert any(
item["source"] == "manual_approval"
and item["event_type"] == "expense_application_approval"
and item["opinion"] == "业务必要,同意申请。"
and item["operator"] == "李经理"
and item["next_status"] == "submitted"
and item["next_approval_stage"] == "预算管理者审批"
and item["next_approver_name"] == "赵预算"
for item in payload["risk_flags_json"]
)
def test_claim_item_pdf_attachment_preview_returns_generated_image(monkeypatch, tmp_path) -> None:
preview_bytes = b"fake-preview-png"
preview_data_url = f"data:image/png;base64,{base64.b64encode(preview_bytes).decode('ascii')}"
def fake_recognize(
self,
files: list[tuple[str, bytes, str | None]],
) -> OcrRecognizeBatchRead:
return OcrRecognizeBatchRead(
total_file_count=1,
success_count=1,
documents=[
OcrRecognizeDocumentRead(
filename="invoice.pdf",
media_type="application/pdf",
text="滴滴出行电子发票 金额13.4元",
summary="识别到交通票据,金额 13.4 元。",
avg_score=0.96,
line_count=1,
page_count=1,
document_type="taxi_receipt",
document_type_label="出租车/网约车票据",
scene_code="transport",
scene_label="交通票据",
preview_kind="image",
preview_data_url=preview_data_url,
warnings=[],
)
],
)
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
client, session_factory = build_client()
with session_factory() as db:
claim, item = seed_claim(db)
claim_id = claim.id
item_id = item.id
headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
upload_response = client.post(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
headers=headers,
files=[("file", ("invoice.pdf", b"%PDF-1.4 fake", "application/pdf"))],
)
assert upload_response.status_code == 200
meta_payload = upload_response.json()["attachment"]
assert meta_payload["preview_kind"] == "image"
assert meta_payload["preview_url"].endswith(f"/reimbursements/claims/{claim_id}/items/{item_id}/attachment/preview")
preview_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/preview",
headers=headers,
)
assert preview_response.status_code == 200
assert preview_response.headers["content-type"].startswith("image/png")
assert preview_response.content == preview_bytes
def test_claim_item_delete_removes_item_and_attachment(monkeypatch, tmp_path) -> None:
def fake_recognize(
self,
files: list[tuple[str, bytes, str | None]],
) -> OcrRecognizeBatchRead:
return OcrRecognizeBatchRead(
total_file_count=1,
success_count=1,
documents=[
OcrRecognizeDocumentRead(
filename="office-note.png",
media_type="image/png",
text="办公用品发票 金额88元 2026-05-13",
summary="识别到办公用品发票,金额 88 元。",
avg_score=0.98,
line_count=1,
page_count=1,
warnings=[],
)
],
)
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
client, session_factory = build_client()
with session_factory() as db:
claim, item = seed_claim(db)
claim_id = claim.id
item_id = item.id
headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
upload_response = client.post(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
headers=headers,
files=[("file", ("office-note.png", b"fake-image-bytes", "image/png"))],
)
assert upload_response.status_code == 200
assert (tmp_path / claim_id / item_id).exists()
delete_response = client.delete(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}",
headers=headers,
)
assert delete_response.status_code == 200
assert delete_response.json()["item_id"] == item_id
assert not (tmp_path / claim_id / item_id).exists()
detail_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}",
headers=headers,
)
assert detail_response.status_code == 200
detail_payload = detail_response.json()
assert detail_payload["items"] == []
assert detail_payload["invoice_count"] == 0
assert detail_payload["employee_position"] == "招商主管"
assert detail_payload["employee_grade"] == "P4"
assert detail_payload["manager_name"] == "李总"
assert detail_payload["role_labels"] == ["员工"]
deleted_meta_response = client.get(
f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/meta",
headers=headers,
)
assert deleted_meta_response.status_code == 404
def test_claim_delete_allows_admin_and_cleans_risk_observations(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
client, session_factory = build_client()
with session_factory() as db:
claim, _ = seed_claim(db)
observation = RiskObservation(
id="risk-observation-delete-1",
observation_key="claim-delete-risk-observation-1",
subject_type="expense_claim",
subject_key=claim.id,
subject_label=claim.claim_no,
claim_id=claim.id,
claim_no=claim.claim_no,
risk_type="policy",
risk_signal="draft_pre_review",
title="草稿预审风险",
description="删除草稿时应同步清理关联风险观察。",
risk_score=70,
risk_level="medium",
confidence_score=0.8,
)
feedback = RiskObservationFeedback(
id="risk-observation-feedback-delete-1",
observation=observation,
feedback_type="confirm",
actor="auditor",
)
db.add(observation)
db.add(feedback)
db.commit()
claim_id = claim.id
response = client.delete(
f"/api/v1/reimbursements/claims/{claim_id}",
headers={"x-auth-username": "admin", "x-auth-name": "Admin User"},
)
assert response.status_code == 200
payload = response.json()
assert payload["claim_id"] == claim_id
assert payload["status"] == "deleted"
with session_factory() as db:
assert db.get(ExpenseClaim, claim_id) is None
assert db.get(RiskObservation, "risk-observation-delete-1") is None
assert db.get(RiskObservationFeedback, "risk-observation-feedback-delete-1") is None
def test_claim_delete_allows_legacy_superadmin_without_is_admin_header(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
client, session_factory = build_client()
with session_factory() as db:
claim, _ = seed_claim(db)
claim_id = claim.id
response = client.delete(
f"/api/v1/reimbursements/claims/{claim_id}",
headers={
"x-auth-username": "superadmin",
"x-auth-name": "superadmin",
"x-auth-role-codes": "manager",
},
)
assert response.status_code == 200
payload = response.json()
assert payload["claim_id"] == claim_id
assert payload["status"] == "deleted"
with session_factory() as db:
assert db.get(ExpenseClaim, claim_id) is None
def test_application_preview_action_submits_without_orchestrator_run(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
client, session_factory = build_client()
with session_factory() as db:
seed_claim(db)
response = client.post(
"/api/v1/reimbursements/application-preview-action",
headers={
"x-auth-username": "zhangsan@example.com",
"x-auth-name": "Zhang San",
"x-auth-employee-no": "E10001",
"x-auth-role-codes": "user",
},
json={
"source": "user_message",
"user_id": "zhangsan@example.com",
"conversation_id": "conversation-fast-submit",
"message": "差旅费用申请提交审批\n申请类型:差旅费用申请\n申请时间2026-07-01 至 2026-07-03\n地点:北京\n事由:项目实施\n天数3天\n出行方式:火车\n申请金额1000元\n直接提交",
"context_json": {
"session_type": "application",
"entry_source": "workbench_ai_inline",
"document_type": "expense_application",
"application_stage": "expense_application",
"application_preview": {
"fields": {
"applicationType": "差旅费用申请",
"time": "2026-07-01 至 2026-07-03",
"location": "北京",
"reason": "项目实施",
"days": "3天",
"transportMode": "火车",
"amount": "1000元",
"applicant": "张三",
"department": "市场部",
"position": "招商主管",
"grade": "P4",
"managerName": "李总",
}
},
},
},
)
assert response.status_code == 200
payload = response.json()
assert payload["status"] == "succeeded"
draft_payload = payload["result"]["draft_payload"]
assert draft_payload["draft_type"] == "expense_application"
assert draft_payload["status"] == "submitted"
assert draft_payload["approval_stage"] == "直属领导审批"
assert draft_payload["claim_no"].startswith("AP-")
with session_factory() as db:
claim = db.get(ExpenseClaim, draft_payload["claim_id"])
assert claim is not None
assert claim.status == "submitted"
assert claim.employee_name == "张三"