feat(platform): close AI expense value loop

Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
caoxiaozhu
2026-07-17 14:14:08 +08:00
parent 242d68c36f
commit 787bc3a481
507 changed files with 82072 additions and 6344 deletions

View File

@@ -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