feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -14,13 +14,20 @@ from app.services.expense_claims import ExpenseClaimService
|
||||
from app.test_helpers.db import build_in_memory_session_factory
|
||||
|
||||
|
||||
def _build_claim(*, claim_id: str, claim_no: str, employee: Employee) -> ExpenseClaim:
|
||||
def _build_claim(
|
||||
*,
|
||||
claim_id: str,
|
||||
claim_no: str,
|
||||
tenant_id: str,
|
||||
employee: Employee,
|
||||
) -> ExpenseClaim:
|
||||
return ExpenseClaim(
|
||||
id=claim_id,
|
||||
tenant_id=tenant_id,
|
||||
claim_no=claim_no,
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
department_id="tenant-scope-department",
|
||||
department_id=None,
|
||||
department_name="租户隔离部",
|
||||
project_code=None,
|
||||
expense_type="office",
|
||||
@@ -37,6 +44,16 @@ def _build_claim(*, claim_id: str, claim_no: str, employee: Employee) -> Expense
|
||||
)
|
||||
|
||||
|
||||
def _build_employee(*, tenant_id: str, suffix: str, name: str, email: str) -> Employee:
|
||||
return Employee(
|
||||
id=f"tenant-{suffix}-employee",
|
||||
tenant_id=tenant_id,
|
||||
employee_no=f"TENANT-{suffix.upper()}-001",
|
||||
name=name,
|
||||
email=email,
|
||||
)
|
||||
|
||||
|
||||
def _current_user(tenant_id: str) -> CurrentUserContext:
|
||||
return CurrentUserContext(
|
||||
username="same-owner@example.com",
|
||||
@@ -50,28 +67,52 @@ def _current_user(tenant_id: str) -> CurrentUserContext:
|
||||
def test_same_identity_claims_are_isolated_by_server_tenant() -> None:
|
||||
session_factory = build_in_memory_session_factory()
|
||||
with session_factory() as db:
|
||||
employee = Employee(
|
||||
id="tenant-scope-employee",
|
||||
employee_no="TENANT-SCOPE-001",
|
||||
employee_a = _build_employee(
|
||||
tenant_id="tenant-a",
|
||||
suffix="scope-a",
|
||||
name="同名员工",
|
||||
email="same-owner@example.com",
|
||||
)
|
||||
employee_b = _build_employee(
|
||||
tenant_id="tenant-b",
|
||||
suffix="scope-b",
|
||||
name="同名员工",
|
||||
email="same-owner@example.com",
|
||||
)
|
||||
default_employee = _build_employee(
|
||||
tenant_id="default",
|
||||
suffix="scope-default",
|
||||
name="同名员工",
|
||||
email="same-owner@example.com",
|
||||
)
|
||||
tenant_a_claim = _build_claim(
|
||||
claim_id="tenant-a-claim",
|
||||
claim_no="RE-TENANT-A",
|
||||
employee=employee,
|
||||
tenant_id="tenant-a",
|
||||
employee=employee_a,
|
||||
)
|
||||
tenant_b_claim = _build_claim(
|
||||
claim_id="tenant-b-claim",
|
||||
claim_no="RE-TENANT-B",
|
||||
employee=employee,
|
||||
tenant_id="tenant-b",
|
||||
employee=employee_b,
|
||||
)
|
||||
legacy_default_claim = _build_claim(
|
||||
claim_id="legacy-default-claim",
|
||||
claim_no="RE-LEGACY-DEFAULT",
|
||||
employee=employee,
|
||||
tenant_id="default",
|
||||
employee=default_employee,
|
||||
)
|
||||
db.add_all(
|
||||
[
|
||||
employee_a,
|
||||
employee_b,
|
||||
default_employee,
|
||||
tenant_a_claim,
|
||||
tenant_b_claim,
|
||||
legacy_default_claim,
|
||||
]
|
||||
)
|
||||
db.add_all([employee, tenant_a_claim, tenant_b_claim, legacy_default_claim])
|
||||
db.flush()
|
||||
case_service = ExpenseCaseService(db)
|
||||
case_service.ensure_case_for_claim(tenant_a_claim, tenant_id="tenant-a")
|
||||
@@ -83,12 +124,8 @@ def test_same_identity_claims_are_isolated_by_server_tenant() -> None:
|
||||
tenant_b_user = _current_user("tenant-b")
|
||||
default_user = _current_user("default")
|
||||
|
||||
assert {claim.id for claim in service.list_claims(tenant_a_user)} == {
|
||||
tenant_a_claim.id
|
||||
}
|
||||
assert {claim.id for claim in service.list_claims(tenant_b_user)} == {
|
||||
tenant_b_claim.id
|
||||
}
|
||||
assert {claim.id for claim in service.list_claims(tenant_a_user)} == {tenant_a_claim.id}
|
||||
assert {claim.id for claim in service.list_claims(tenant_b_user)} == {tenant_b_claim.id}
|
||||
assert {claim.id for claim in service.list_claims(default_user)} == {
|
||||
legacy_default_claim.id
|
||||
}
|
||||
@@ -108,21 +145,29 @@ def test_same_identity_claims_are_isolated_by_server_tenant() -> None:
|
||||
|
||||
|
||||
def _seed_cross_tenant_risk_history(db):
|
||||
employee = Employee(
|
||||
id="tenant-history-employee",
|
||||
employee_no="TENANT-HISTORY-001",
|
||||
employee_a = _build_employee(
|
||||
tenant_id="tenant-a",
|
||||
suffix="history-a",
|
||||
name="同名风险员工",
|
||||
email="same-risk-owner@example.com",
|
||||
)
|
||||
employee_b = _build_employee(
|
||||
tenant_id="tenant-b",
|
||||
suffix="history-b",
|
||||
name="同名风险员工",
|
||||
email="same-risk-owner@example.com",
|
||||
)
|
||||
clean_claim = _build_claim(
|
||||
claim_id="tenant-a-clean-claim",
|
||||
claim_no="RE-TENANT-A-CLEAN",
|
||||
employee=employee,
|
||||
tenant_id="tenant-a",
|
||||
employee=employee_a,
|
||||
)
|
||||
risky_claim = _build_claim(
|
||||
claim_id="tenant-b-risky-claim",
|
||||
claim_no="RE-TENANT-B-RISKY",
|
||||
employee=employee,
|
||||
tenant_id="tenant-b",
|
||||
employee=employee_b,
|
||||
)
|
||||
risky_claim.risk_flags_json = [
|
||||
{
|
||||
@@ -132,7 +177,7 @@ def _seed_cross_tenant_risk_history(db):
|
||||
"message": "该风险只属于 tenant-b。",
|
||||
}
|
||||
]
|
||||
db.add_all([employee, clean_claim, risky_claim])
|
||||
db.add_all([employee_a, employee_b, clean_claim, risky_claim])
|
||||
db.flush()
|
||||
case_service = ExpenseCaseService(db)
|
||||
case_service.ensure_case_for_claim(clean_claim, tenant_id="tenant-a")
|
||||
@@ -207,28 +252,52 @@ def test_cross_tenant_risk_history_does_not_route_to_p8(monkeypatch) -> None:
|
||||
def test_draft_lookup_never_returns_cross_tenant_claim_and_keeps_default_legacy() -> None:
|
||||
session_factory = build_in_memory_session_factory()
|
||||
with session_factory() as db:
|
||||
employee = Employee(
|
||||
id="tenant-draft-employee",
|
||||
employee_no="TENANT-DRAFT-001",
|
||||
employee_a = _build_employee(
|
||||
tenant_id="tenant-a",
|
||||
suffix="draft-a",
|
||||
name="同名草稿员工",
|
||||
email="same-draft-owner@example.com",
|
||||
)
|
||||
employee_b = _build_employee(
|
||||
tenant_id="tenant-b",
|
||||
suffix="draft-b",
|
||||
name="同名草稿员工",
|
||||
email="same-draft-owner@example.com",
|
||||
)
|
||||
default_employee = _build_employee(
|
||||
tenant_id="default",
|
||||
suffix="draft-default",
|
||||
name="同名草稿员工",
|
||||
email="same-draft-owner@example.com",
|
||||
)
|
||||
tenant_a_claim = _build_claim(
|
||||
claim_id="tenant-a-draft",
|
||||
claim_no="RE-TENANT-A-DRAFT",
|
||||
employee=employee,
|
||||
tenant_id="tenant-a",
|
||||
employee=employee_a,
|
||||
)
|
||||
tenant_b_claim = _build_claim(
|
||||
claim_id="tenant-b-draft",
|
||||
claim_no="RE-TENANT-B-DRAFT",
|
||||
employee=employee,
|
||||
tenant_id="tenant-b",
|
||||
employee=employee_b,
|
||||
)
|
||||
legacy_default_claim = _build_claim(
|
||||
claim_id="legacy-default-draft",
|
||||
claim_no="RE-LEGACY-DEFAULT-DRAFT",
|
||||
employee=employee,
|
||||
tenant_id="default",
|
||||
employee=default_employee,
|
||||
)
|
||||
db.add_all(
|
||||
[
|
||||
employee_a,
|
||||
employee_b,
|
||||
default_employee,
|
||||
tenant_a_claim,
|
||||
tenant_b_claim,
|
||||
legacy_default_claim,
|
||||
]
|
||||
)
|
||||
db.add_all([employee, tenant_a_claim, tenant_b_claim, legacy_default_claim])
|
||||
db.flush()
|
||||
case_service = ExpenseCaseService(db)
|
||||
case_service.ensure_case_for_claim(tenant_a_claim, tenant_id="tenant-a")
|
||||
@@ -272,8 +341,8 @@ def test_draft_lookup_never_returns_cross_tenant_claim_and_keeps_default_legacy(
|
||||
"tenant_id": "tenant-a",
|
||||
"draft_claim_id": tenant_b_claim.id,
|
||||
},
|
||||
user_id=employee.email,
|
||||
employee=employee,
|
||||
user_id=employee_a.email,
|
||||
employee=employee_a,
|
||||
)
|
||||
assert association_candidate is not None
|
||||
assert association_candidate.id == tenant_a_claim.id
|
||||
|
||||
Reference in New Issue
Block a user