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

@@ -347,6 +347,7 @@ def test_attachment_association_job_links_receipts_after_conversation_exit(
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -446,9 +447,7 @@ def test_attachment_association_job_links_receipts_after_conversation_exit(
"attachment_associated",
}
expense_case = db.scalar(
select(ExpenseCase).where(
ExpenseCase.id == receipt_links[0].expense_case_id
)
select(ExpenseCase).where(ExpenseCase.id == receipt_links[0].expense_case_id)
)
assert expense_case is not None
assert expense_case.current_stage == "claiming"
@@ -480,6 +479,7 @@ def test_attachment_association_keeps_receipt_folder_preview_and_fields_after_ca
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -497,7 +497,10 @@ def test_attachment_association_keeps_receipt_folder_preview_and_fields_after_ca
document=OcrRecognizeDocumentRead(
filename="2月20 武汉-上海.pdf",
media_type="application/pdf",
text="电子发票(铁路电子客票) 武汉站 G458 上海虹桥站 2026年02月20日 07:55开 二等座 票价 354.00",
text=(
"电子发票(铁路电子客票) 武汉站 G458 上海虹桥站 "
"2026年02月20日 07:55开 二等座 票价 354.00"
),
summary="铁路电子客票,武汉-上海,票价 354 元。",
avg_score=0.96,
line_count=1,
@@ -597,6 +600,7 @@ def test_attachment_meta_repairs_existing_pdf_fallback_from_source_receipt(
)
try:
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -617,7 +621,10 @@ def test_attachment_meta_repairs_existing_pdf_fallback_from_source_receipt(
document=OcrRecognizeDocumentRead(
filename="2月20 武汉-上海.pdf",
media_type="application/pdf",
text="电子发票(铁路电子客票) 武汉站 G458 上海虹桥站 2026年02月20日 07:55开 二等座 票价 354.00",
text=(
"电子发票(铁路电子客票) 武汉站 G458 上海虹桥站 "
"2026年02月20日 07:55开 二等座 票价 354.00"
),
summary="铁路电子客票,武汉-上海,票价 354 元。",
avg_score=0.96,
line_count=1,
@@ -708,6 +715,7 @@ def test_attachment_association_job_requests_confirmation_without_editable_claim
try:
client, _session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -766,6 +774,7 @@ def test_confirmation_job_is_re_evaluated_after_draft_is_created(
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -823,9 +832,7 @@ def test_confirmation_job_is_re_evaluated_after_draft_is_created(
with session_factory() as db:
jobs = list(
db.scalars(
select(AttachmentAssociationJob).order_by(
AttachmentAssociationJob.generation
)
select(AttachmentAssociationJob).order_by(AttachmentAssociationJob.generation)
).all()
)
assert [(job.generation, job.resolution) for job in jobs] == [
@@ -847,6 +854,7 @@ def test_attachment_association_job_returns_application_candidate_without_creati
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -905,6 +913,7 @@ def test_attachment_association_job_is_idempotent_for_same_receipt(monkeypatch,
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -980,6 +989,7 @@ def test_attachment_association_ambiguous_match_has_no_business_writes(
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1056,6 +1066,7 @@ def test_attachment_association_rolls_back_when_event_write_fails(monkeypatch, t
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1130,6 +1141,7 @@ def test_attachment_association_failure_keeps_preexisting_application_case(
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1294,6 +1306,7 @@ def test_confirmation_match_does_not_repair_or_commit_unrelated_claim(
db.add_all([employee, claim])
db.commit()
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1345,6 +1358,7 @@ def test_mixed_receipt_batch_requires_confirmation_without_writes(
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1422,6 +1436,7 @@ def test_attachment_write_failure_restores_previous_directory_and_business_state
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1494,6 +1509,7 @@ def test_concurrent_jobs_for_same_receipt_are_serialized(monkeypatch, tmp_path)
try:
_client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1592,6 +1608,7 @@ def test_concurrent_receipts_for_same_claim_are_serialized(monkeypatch, tmp_path
try:
_client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1657,9 +1674,7 @@ def test_concurrent_receipts_for_same_claim_are_serialized(monkeypatch, tmp_path
assert not thread.is_alive()
with session_factory() as db:
results = [
get_attachment_association_job(job.job_id, current_user, db) for job in jobs
]
results = [get_attachment_association_job(job.job_id, current_user, db) for job in jobs]
claim = db.scalar(
select(ExpenseClaim)
.options(selectinload(ExpenseClaim.items))
@@ -1672,9 +1687,7 @@ def test_concurrent_receipts_for_same_claim_are_serialized(monkeypatch, tmp_path
)
receipt_links = list(
db.scalars(
select(ExpenseCaseLink).where(
ExpenseCaseLink.resource_type == "receipt"
)
select(ExpenseCaseLink).where(ExpenseCaseLink.resource_type == "receipt")
).all()
)
@@ -1694,9 +1707,7 @@ def test_concurrent_receipts_for_same_claim_are_serialized(monkeypatch, tmp_path
)
assert len(events) == 4
assert len(receipt_links) == 2
assert {
(event.aggregate_id, event.event_type) for event in events
} == {
assert {(event.aggregate_id, event.event_type) for event in events} == {
(receipt_id, event_type)
for receipt_id in receipt_ids
for event_type in ("receipt_received", "attachment_associated")
@@ -1706,15 +1717,16 @@ def test_concurrent_receipts_for_same_claim_are_serialized(monkeypatch, tmp_path
for receipt_id in receipt_ids
]
assert all(receipt.status == "linked" for receipt in linked_receipts)
assert all(
receipt.linked_claim_id == "claim-bg-association" for receipt in linked_receipts
assert all(receipt.linked_claim_id == "claim-bg-association" for receipt in linked_receipts)
assert (
len(
{
str((receipt.raw_meta or {}).get("linked_item_id") or "")
for receipt in linked_receipts
}
)
== 2
)
assert len(
{
str((receipt.raw_meta or {}).get("linked_item_id") or "")
for receipt in linked_receipts
}
) == 2
finally:
clear_attachment_association_jobs_for_tests()
get_settings.cache_clear()
@@ -1731,6 +1743,7 @@ def test_persistent_job_resumes_after_process_state_is_cleared(monkeypatch, tmp_
try:
client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1789,6 +1802,7 @@ def test_expired_worker_cannot_overwrite_newer_job_attempt(monkeypatch) -> None:
try:
_client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1870,6 +1884,7 @@ def test_failed_job_creates_new_generation_without_rewriting_history(monkeypatch
try:
_client, session_factory = build_client(monkeypatch)
current_user = CurrentUserContext(
tenant_id="default",
username="zhangsan@example.com",
name="张三",
role_codes=["user"],
@@ -1895,9 +1910,7 @@ def test_failed_job_creates_new_generation_without_rewriting_history(monkeypatch
second = create_attachment_association_job(payload, current_user, db)
jobs = list(
db.scalars(
select(AttachmentAssociationJob).order_by(
AttachmentAssociationJob.generation
)
select(AttachmentAssociationJob).order_by(AttachmentAssociationJob.generation)
).all()
)