feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user