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,6 +14,7 @@ from app.api.deps import CurrentUserContext, get_db
from app.main import create_app
from app.models.ai_learning import AIDecision, AIDecisionFeedback, WorkflowOutcome
from app.models.ai_memory import MemoryEntry, MemoryEvidenceLink
from app.models.employee import Employee
from app.models.expense_case import BusinessEvent, ExpenseCase
from app.models.financial_record import ExpenseClaim
from app.schemas.expense_application_preview import ExpenseApplicationPreviewDecisionCreate
@@ -345,9 +346,10 @@ def test_invalidated_feedback_and_reversed_outcome_remove_active_evidence() -> N
reversed_outcome.outcome_status = "reversed"
db.flush()
assert ExpenseApplicationMemoryService(db).apply_active_transport_memory(
{}, current_user
) == []
assert (
ExpenseApplicationMemoryService(db).apply_active_transport_memory({}, current_user)
== []
)
db.refresh(entry)
assert entry.status == "suppressed"
assert entry.evidence_count == 1
@@ -369,9 +371,10 @@ def test_opposite_evidence_immediately_suppresses_old_active_memory() -> None:
assert old_entry.status == "suppressed"
assert old_entry.suppressed_at is not None
assert new_entry.status == "candidate"
assert ExpenseApplicationMemoryService(db).apply_active_transport_memory(
{}, current_user
) == []
assert (
ExpenseApplicationMemoryService(db).apply_active_transport_memory({}, current_user)
== []
)
def test_non_whitelisted_correction_suppresses_old_active_without_storing_value() -> None:
@@ -392,9 +395,10 @@ def test_non_whitelisted_correction_suppresses_old_active_without_storing_value(
assert old_entry.status == "suppressed"
assert old_entry.suppressed_at is not None
assert len(list(db.scalars(select(MemoryEntry)).all())) == entry_count
assert ExpenseApplicationMemoryService(db).apply_active_transport_memory(
{}, current_user
) == []
assert (
ExpenseApplicationMemoryService(db).apply_active_transport_memory({}, current_user)
== []
)
def test_expiry_sets_expired_at_and_replay_does_not_extend_ttl() -> None:
@@ -428,9 +432,7 @@ def test_expiry_sets_expired_at_and_replay_does_not_extend_ttl() -> None:
entry.created_at = old_created_at
entry.candidate_expires_at = datetime.now(UTC) - timedelta(days=1)
db.flush()
memories = ExpenseApplicationMemoryService(db).list_current_user_memories(
current_user
)
memories = ExpenseApplicationMemoryService(db).list_current_user_memories(current_user)
db.refresh(entry)
assert memories.items[0].status == "expired"
assert entry.status == "expired"
@@ -472,10 +474,13 @@ def test_replay_after_revoke_or_suppression_never_recreates_memory() -> None:
)
claim = db.get(ExpenseClaim, decision.expense_claim_id)
assert claim is not None
assert ExpenseApplicationMemoryService(db).revoke_current_user_memory(
revoked_entry.id,
current_user,
) is not None
assert (
ExpenseApplicationMemoryService(db).revoke_current_user_memory(
revoked_entry.id,
current_user,
)
is not None
)
entry_count = len(list(db.scalars(select(MemoryEntry)).all()))
revoked_replay = ExpenseApplicationMemoryService(db).record_transport_edit_evidence(
@@ -515,9 +520,7 @@ def test_replay_after_revoke_or_suppression_never_recreates_memory() -> None:
assert suppressed_entry.status == "suppressed"
entry_count = len(list(db.scalars(select(MemoryEntry)).all()))
suppressed_replay = ExpenseApplicationMemoryService(
db
).record_transport_edit_evidence(
suppressed_replay = ExpenseApplicationMemoryService(db).record_transport_edit_evidence(
current_user=current_user,
decision=decision,
feedback=feedback,
@@ -537,13 +540,14 @@ def test_memory_isolated_by_tenant_and_owner_and_revoke_clears_value() -> None:
service = ExpenseApplicationMemoryService(db)
assert service.list_current_user_memories(_user(employee_id="employee-other")).items == []
assert service.list_current_user_memories(
_user(tenant_id="tenant-other")
).items == []
assert service.revoke_current_user_memory(
entry.id,
_user(employee_id="employee-other"),
) is None
assert service.list_current_user_memories(_user(tenant_id="tenant-other")).items == []
assert (
service.revoke_current_user_memory(
entry.id,
_user(employee_id="employee-other"),
)
is None
)
revoked = service.revoke_current_user_memory(entry.id, owner)
assert revoked is not None
@@ -576,10 +580,7 @@ def test_memory_rejects_same_tenant_evidence_from_another_owner() -> None:
transport_mode="火车",
)
assert (
ExpenseApplicationMemoryService(db)
.list_current_user_memories(other_owner)
.items
== []
ExpenseApplicationMemoryService(db).list_current_user_memories(other_owner).items == []
)
@@ -638,13 +639,29 @@ def test_memory_read_failure_degrades_without_mutating_facts(
def test_preview_and_orchestrator_apply_active_memory() -> None:
with build_in_memory_session_factory()() as db:
current_user = _user()
manager = Employee(
id="employee-memory-manager",
tenant_id=current_user.tenant_id,
employee_no="E-MEMORY-MANAGER",
name="记忆测试经理",
email="employee-memory-manager@example.com",
)
employee = Employee(
id=current_user.employee_id,
tenant_id=current_user.tenant_id,
employee_no=current_user.employee_no,
name=current_user.name,
email=current_user.username,
manager=manager,
)
db.add_all([manager, employee])
db.flush()
_seed_active_memory(db, current_user=current_user)
preview = ExpenseApplicationPreviewWorkflow(db).issue(
ExpenseApplicationPreviewDecisionCreate(
message=(
"申请时间2026-07-20 至 2026-07-22\n"
"地点:上海\n事由:客户现场实施\n天数3天"
"申请时间2026-07-20 至 2026-07-22\n地点:上海\n事由:客户现场实施\n天数3天"
),
conversation_id="conversation-memory-preview",
request_id="request-memory-preview",
@@ -685,9 +702,7 @@ def test_preview_and_orchestrator_apply_active_memory() -> None:
)
assert outcome is not None
assert outcome.result["application_preview"]["fields"]["transportMode"] == "火车"
assert outcome.result["application_preview"]["memoryApplications"][0][
"status"
] == "applied"
assert outcome.result["application_preview"]["memoryApplications"][0]["status"] == "applied"
assert "补充出行方式" not in str(outcome.result)
edited_fields = dict(preview.application_preview["fields"])
@@ -749,10 +764,13 @@ def test_memory_api_enforces_owner_and_allows_owner_revoke() -> None:
"/api/v1/expense-application-memories/me",
headers=other_headers,
).json() == {"items": []}
assert client.delete(
f"/api/v1/expense-application-memories/{memory_id}",
headers=other_headers,
).status_code == 404
assert (
client.delete(
f"/api/v1/expense-application-memories/{memory_id}",
headers=other_headers,
).status_code
== 404
)
response = client.delete(
f"/api/v1/expense-application-memories/{memory_id}",