2026-06-24 10:42:05 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from collections.abc import Generator
|
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
|
|
2026-07-13 14:45:36 +08:00
|
|
|
|
from auth_helpers import install_legacy_header_auth_override
|
2026-06-24 10:42:05 +08:00
|
|
|
|
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
|
2026-07-16 11:42:08 +08:00
|
|
|
|
from app.services.expense_cases import ExpenseCaseService
|
2026-07-13 14:45:36 +08:00
|
|
|
|
from app.services.linked_reimbursement_draft_jobs import (
|
2026-07-16 11:42:08 +08:00
|
|
|
|
_build_direct_context_json,
|
|
|
|
|
|
_find_application_claim,
|
2026-07-13 14:45:36 +08:00
|
|
|
|
clear_linked_reimbursement_draft_jobs_for_tests,
|
|
|
|
|
|
)
|
2026-06-24 10:42:05 +08:00
|
|
|
|
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()
|
2026-07-13 14:45:36 +08:00
|
|
|
|
install_legacy_header_auth_override(app)
|
2026-06-24 10:42:05 +08:00
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 11:42:08 +08:00
|
|
|
|
def test_linked_job_overrides_forged_tenant_and_rejects_same_owner_cross_tenant(
|
|
|
|
|
|
monkeypatch,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
clear_linked_reimbursement_draft_jobs_for_tests()
|
|
|
|
|
|
captured_contexts = []
|
|
|
|
|
|
|
|
|
|
|
|
def fake_run(self, payload):
|
|
|
|
|
|
captured_contexts.append(dict(payload.context_json or {}))
|
|
|
|
|
|
return OrchestratorResponse(
|
|
|
|
|
|
run_id="run-linked-tenant-guard",
|
|
|
|
|
|
conversation_id=None,
|
|
|
|
|
|
selected_agent="user_agent",
|
|
|
|
|
|
route_reason="验证租户边界。",
|
|
|
|
|
|
permission_level="draft_write",
|
|
|
|
|
|
status="succeeded",
|
|
|
|
|
|
result={
|
|
|
|
|
|
"message": "报销草稿已生成。",
|
|
|
|
|
|
"draft_payload": {
|
|
|
|
|
|
"claim_id": "tenant-guard-draft",
|
|
|
|
|
|
"claim_no": "RE-TENANT-GUARD",
|
|
|
|
|
|
"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)
|
|
|
|
|
|
tenant_a_headers = {
|
|
|
|
|
|
"x-auth-username": "same-owner@example.com",
|
|
|
|
|
|
"x-auth-name": "Same Owner",
|
|
|
|
|
|
"x-auth-role-codes": "user",
|
|
|
|
|
|
"x-auth-tenant-id": "tenant-a",
|
|
|
|
|
|
}
|
|
|
|
|
|
response = client.post(
|
|
|
|
|
|
"/api/v1/reimbursements/linked-reimbursement-draft-jobs",
|
|
|
|
|
|
headers=tenant_a_headers,
|
|
|
|
|
|
json={
|
|
|
|
|
|
"message": "创建普通报销草稿",
|
|
|
|
|
|
"context_json": {
|
|
|
|
|
|
"tenant_id": "tenant-forged",
|
|
|
|
|
|
"tenantId": "tenant-forged-camel",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 202
|
|
|
|
|
|
assert captured_contexts == [
|
|
|
|
|
|
{
|
|
|
|
|
|
"tenant_id": "tenant-a",
|
|
|
|
|
|
"entry_source": "workbench-ai",
|
|
|
|
|
|
"session_type": "expense",
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
job_id = response.json()["job_id"]
|
|
|
|
|
|
cross_tenant_response = client.get(
|
|
|
|
|
|
f"/api/v1/reimbursements/linked-reimbursement-draft-jobs/{job_id}",
|
|
|
|
|
|
headers={**tenant_a_headers, "x-auth-tenant-id": "tenant-b"},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert cross_tenant_response.status_code == 404
|
|
|
|
|
|
finally:
|
|
|
|
|
|
clear_linked_reimbursement_draft_jobs_for_tests()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_linked_application_lookup_and_context_are_tenant_scoped() -> None:
|
|
|
|
|
|
session_factory = build_in_memory_session_factory()
|
|
|
|
|
|
with session_factory() as db:
|
|
|
|
|
|
seed_employee_and_application(db)
|
|
|
|
|
|
application = db.get(ExpenseClaim, "application-linked-draft-fast")
|
|
|
|
|
|
assert application is not None
|
|
|
|
|
|
ExpenseCaseService(db).ensure_case_for_claim(
|
|
|
|
|
|
application,
|
|
|
|
|
|
tenant_id="tenant-a",
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
|
_find_application_claim(
|
|
|
|
|
|
db,
|
|
|
|
|
|
claim_no="AP-202606-FAST",
|
|
|
|
|
|
tenant_id="tenant-b",
|
|
|
|
|
|
)
|
|
|
|
|
|
is None
|
|
|
|
|
|
)
|
|
|
|
|
|
resolved = _find_application_claim(
|
|
|
|
|
|
db,
|
|
|
|
|
|
claim_no="AP-202606-FAST",
|
|
|
|
|
|
tenant_id="tenant-a",
|
|
|
|
|
|
)
|
|
|
|
|
|
assert resolved is not None
|
|
|
|
|
|
context = _build_direct_context_json(
|
|
|
|
|
|
db,
|
|
|
|
|
|
{
|
|
|
|
|
|
"tenant_id": "tenant-forged",
|
|
|
|
|
|
"tenantId": "tenant-forged-camel",
|
|
|
|
|
|
"review_form_values": {
|
|
|
|
|
|
"application_claim_no": "AP-202606-FAST",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
tenant_id="tenant-a",
|
|
|
|
|
|
)
|
|
|
|
|
|
assert context["tenant_id"] == "tenant-a"
|
|
|
|
|
|
assert "tenantId" not in context
|
|
|
|
|
|
assert context["review_form_values"]["application_claim_id"] == application.id
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-24 10:42:05 +08:00
|
|
|
|
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()
|