feat(server): 新增附件关联/关联报销草稿后台任务与申请位置语义
- attachment_association_jobs:从票据夹批量关联附件到报销单,识别城市/日期并创建明细项,内存态 job 跟踪 - linked_reimbursement_draft_jobs:基于申请单异步生成关联报销草稿,调用 Orchestrator 编排,区分 succeeded/failed 终态 - application_location_semantics:抽取差旅出发/到达城市、判断具体地址/业务动作等位置语义,供申请单校验复用 - router 注册两个 job 端点,新增对应 job/语义单元测试
This commit is contained in:
280
server/tests/test_attachment_association_jobs.py
Normal file
280
server/tests/test_attachment_association_jobs.py
Normal file
@@ -0,0 +1,280 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import UTC, date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.api.deps import CurrentUserContext, get_db
|
||||
from app.api.v1.endpoints import attachment_association_jobs as attachment_jobs_endpoint
|
||||
from app.core.config import get_settings
|
||||
from app.main import create_app
|
||||
from app.models.employee import Employee
|
||||
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
|
||||
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead, OcrRecognizeFieldRead
|
||||
from app.services.attachment_association_jobs import clear_attachment_association_jobs_for_tests
|
||||
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
|
||||
from app.services.ocr import OcrService
|
||||
from app.services.receipt_folder import ReceiptFolderService
|
||||
from app.test_helpers.db import build_in_memory_session_factory
|
||||
|
||||
|
||||
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(attachment_jobs_endpoint, "get_session_factory", lambda: session_factory)
|
||||
return TestClient(app), session_factory
|
||||
|
||||
|
||||
def seed_travel_claim(db: Session) -> ExpenseClaim:
|
||||
employee = Employee(
|
||||
id="emp-bg-association",
|
||||
employee_no="E10001",
|
||||
name="张三",
|
||||
email="zhangsan@example.com",
|
||||
position="实施顾问",
|
||||
grade="P4",
|
||||
)
|
||||
claim = ExpenseClaim(
|
||||
id="claim-bg-association",
|
||||
claim_no="BX-20260220-001",
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
department_id="dept-delivery",
|
||||
department_name="交付部",
|
||||
project_code=None,
|
||||
expense_type="travel",
|
||||
reason="辅助国网仿生产服务器部署,武汉往返上海",
|
||||
location="上海",
|
||||
amount=Decimal("0.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 2, 20, tzinfo=UTC),
|
||||
submitted_at=None,
|
||||
status="draft",
|
||||
approval_stage="待提交",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
item = ExpenseClaimItem(
|
||||
id="item-bg-association-1",
|
||||
claim_id=claim.id,
|
||||
item_date=date(2026, 2, 20),
|
||||
item_type="train_ticket",
|
||||
item_reason="武汉至上海高铁",
|
||||
item_location="上海",
|
||||
item_amount=Decimal("0.00"),
|
||||
invoice_id=None,
|
||||
)
|
||||
claim.items = [item]
|
||||
db.add_all([employee, claim])
|
||||
db.commit()
|
||||
return claim
|
||||
|
||||
|
||||
def save_train_receipt(
|
||||
*,
|
||||
service: ReceiptFolderService,
|
||||
current_user: CurrentUserContext,
|
||||
filename: str,
|
||||
route: str,
|
||||
trip_date: str,
|
||||
) -> str:
|
||||
receipt = service.save_receipt(
|
||||
filename=filename,
|
||||
content=f"fake-pdf-{filename}".encode("utf-8"),
|
||||
media_type="application/pdf",
|
||||
current_user=current_user,
|
||||
document=OcrRecognizeDocumentRead(
|
||||
filename=filename,
|
||||
media_type="application/pdf",
|
||||
text=f"电子发票(铁路电子客票) {route} {trip_date} 票价 354 元",
|
||||
summary=f"铁路电子客票,{route},票价 354 元。",
|
||||
avg_score=0.96,
|
||||
line_count=1,
|
||||
page_count=1,
|
||||
document_type="train_ticket",
|
||||
document_type_label="火车/高铁票",
|
||||
scene_code="travel",
|
||||
scene_label="差旅票据",
|
||||
document_fields=[
|
||||
OcrRecognizeFieldRead(key="date", label="列车出发时间", value=trip_date),
|
||||
OcrRecognizeFieldRead(key="route", label="行程", value=route),
|
||||
OcrRecognizeFieldRead(key="amount", label="金额", value="354元"),
|
||||
],
|
||||
),
|
||||
)
|
||||
return receipt.id
|
||||
|
||||
|
||||
def fake_ocr_recognize(
|
||||
self,
|
||||
files: list[tuple[str, bytes, str | None]],
|
||||
) -> OcrRecognizeBatchRead:
|
||||
filename = files[0][0]
|
||||
return OcrRecognizeBatchRead(
|
||||
total_file_count=1,
|
||||
success_count=1,
|
||||
documents=[
|
||||
OcrRecognizeDocumentRead(
|
||||
filename=filename,
|
||||
media_type=files[0][2] or "application/pdf",
|
||||
text="电子发票(铁路电子客票) 武汉 上海 2026-02-20 票价 354 元",
|
||||
summary="铁路电子客票,武汉至上海,票价 354 元。",
|
||||
avg_score=0.96,
|
||||
line_count=1,
|
||||
page_count=1,
|
||||
document_type="train_ticket",
|
||||
document_type_label="火车/高铁票",
|
||||
scene_code="travel",
|
||||
scene_label="差旅票据",
|
||||
document_fields=[
|
||||
OcrRecognizeFieldRead(key="date", label="列车出发时间", value="2026-02-20"),
|
||||
OcrRecognizeFieldRead(key="route", label="行程", value="武汉-上海"),
|
||||
OcrRecognizeFieldRead(key="amount", label="金额", value="354元"),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_attachment_association_job_links_receipts_after_conversation_exit(monkeypatch, tmp_path) -> None:
|
||||
monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path / "storage"))
|
||||
get_settings.cache_clear()
|
||||
clear_attachment_association_jobs_for_tests()
|
||||
monkeypatch.setattr(OcrService, "recognize_files", fake_ocr_recognize)
|
||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path / "attachments")
|
||||
try:
|
||||
client, session_factory = build_client(monkeypatch)
|
||||
current_user = CurrentUserContext(
|
||||
username="zhangsan@example.com",
|
||||
name="张三",
|
||||
role_codes=["user"],
|
||||
is_admin=False,
|
||||
employee_no="E10001",
|
||||
)
|
||||
with session_factory() as db:
|
||||
seed_travel_claim(db)
|
||||
|
||||
receipt_service = ReceiptFolderService()
|
||||
receipt_ids = [
|
||||
save_train_receipt(
|
||||
service=receipt_service,
|
||||
current_user=current_user,
|
||||
filename="2月20 武汉-上海.pdf",
|
||||
route="武汉-上海",
|
||||
trip_date="2026-02-20",
|
||||
),
|
||||
save_train_receipt(
|
||||
service=receipt_service,
|
||||
current_user=current_user,
|
||||
filename="2月23 上海-武汉.pdf",
|
||||
route="上海-武汉",
|
||||
trip_date="2026-02-23",
|
||||
),
|
||||
]
|
||||
|
||||
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/attachment-association-jobs",
|
||||
headers=headers,
|
||||
json={
|
||||
"receipt_ids": receipt_ids,
|
||||
"prompt": "请帮我处理已上传的附件。",
|
||||
"conversation_id": "inline-test",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 202
|
||||
job_id = response.json()["job_id"]
|
||||
|
||||
status_response = client.get(
|
||||
f"/api/v1/reimbursements/attachment-association-jobs/{job_id}",
|
||||
headers=headers,
|
||||
)
|
||||
assert status_response.status_code == 200
|
||||
payload = status_response.json()
|
||||
assert payload["status"] == "succeeded"
|
||||
assert payload["claim_id"] == "claim-bg-association"
|
||||
assert payload["claim_no"] == "BX-20260220-001"
|
||||
assert payload["uploaded_count"] == 2
|
||||
|
||||
with session_factory() as db:
|
||||
claim = db.scalar(
|
||||
select(ExpenseClaim)
|
||||
.options(selectinload(ExpenseClaim.items))
|
||||
.where(ExpenseClaim.id == "claim-bg-association")
|
||||
)
|
||||
assert claim is not None
|
||||
attached_items = [item for item in claim.items if item.invoice_id]
|
||||
assert len(attached_items) == 2
|
||||
|
||||
linked_receipts = receipt_service.list_receipts(current_user=current_user, status_filter="linked")
|
||||
assert {item.id for item in linked_receipts} == set(receipt_ids)
|
||||
assert {item.linked_claim_id for item in linked_receipts} == {"claim-bg-association"}
|
||||
finally:
|
||||
clear_attachment_association_jobs_for_tests()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_attachment_association_job_fails_without_editable_claim(monkeypatch, tmp_path) -> None:
|
||||
monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path / "storage"))
|
||||
get_settings.cache_clear()
|
||||
clear_attachment_association_jobs_for_tests()
|
||||
try:
|
||||
client, _session_factory = build_client(monkeypatch)
|
||||
current_user = CurrentUserContext(
|
||||
username="zhangsan@example.com",
|
||||
name="张三",
|
||||
role_codes=["user"],
|
||||
is_admin=False,
|
||||
employee_no="E10001",
|
||||
)
|
||||
receipt_id = save_train_receipt(
|
||||
service=ReceiptFolderService(),
|
||||
current_user=current_user,
|
||||
filename="2月20 武汉-上海.pdf",
|
||||
route="武汉-上海",
|
||||
trip_date="2026-02-20",
|
||||
)
|
||||
|
||||
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/attachment-association-jobs",
|
||||
headers=headers,
|
||||
json={"receipt_ids": [receipt_id], "conversation_id": "inline-empty"},
|
||||
)
|
||||
assert response.status_code == 202
|
||||
|
||||
status_response = client.get(
|
||||
f"/api/v1/reimbursements/attachment-association-jobs/{response.json()['job_id']}",
|
||||
headers=headers,
|
||||
)
|
||||
assert status_response.status_code == 200
|
||||
payload = status_response.json()
|
||||
assert payload["status"] == "failed"
|
||||
assert "没有找到可自动关联的报销草稿" in payload["message"]
|
||||
finally:
|
||||
clear_attachment_association_jobs_for_tests()
|
||||
get_settings.cache_clear()
|
||||
296
server/tests/test_linked_reimbursement_draft_jobs.py
Normal file
296
server/tests/test_linked_reimbursement_draft_jobs.py
Normal file
@@ -0,0 +1,296 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user