feat(expenses): add authoritative pre-review workflow
This commit is contained in:
@@ -13,7 +13,10 @@ 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.expense_cases import ExpenseCaseService
|
||||
from app.services.linked_reimbursement_draft_jobs import (
|
||||
_build_direct_context_json,
|
||||
_find_application_claim,
|
||||
clear_linked_reimbursement_draft_jobs_for_tests,
|
||||
)
|
||||
from app.services.orchestrator import OrchestratorService
|
||||
@@ -150,6 +153,122 @@ def test_linked_reimbursement_draft_job_runs_after_conversation_leaves(monkeypat
|
||||
clear_linked_reimbursement_draft_jobs_for_tests()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_linked_reimbursement_draft_job_uses_direct_save_path(monkeypatch) -> None:
|
||||
clear_linked_reimbursement_draft_jobs_for_tests()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user