feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -9,11 +9,14 @@ from app.services.document_preview import DocumentPreviewAssets
|
||||
from app.services.receipt_folder import ReceiptFolderService
|
||||
|
||||
|
||||
def test_receipt_folder_train_ticket_uses_invoice_date_and_enriches_fields(monkeypatch, tmp_path) -> None:
|
||||
def test_receipt_folder_train_ticket_uses_invoice_date_and_enriches_fields(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path / "storage"))
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
@@ -77,6 +80,7 @@ def test_receipt_folder_pdf_save_eagerly_renders_image_preview(monkeypatch, tmp_
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
@@ -87,7 +91,9 @@ def test_receipt_folder_pdf_save_eagerly_renders_image_preview(monkeypatch, tmp_
|
||||
preview_path.write_bytes(b"rendered-preview")
|
||||
return preview_path
|
||||
|
||||
monkeypatch.setattr(DocumentPreviewAssets, "render_pdf_first_page", fake_render_pdf_first_page)
|
||||
monkeypatch.setattr(
|
||||
DocumentPreviewAssets, "render_pdf_first_page", fake_render_pdf_first_page
|
||||
)
|
||||
|
||||
service = ReceiptFolderService()
|
||||
receipt = service.save_receipt(
|
||||
@@ -121,11 +127,14 @@ def test_receipt_folder_pdf_save_eagerly_renders_image_preview(monkeypatch, tmp_
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_receipt_folder_persist_enriches_pdf_ocr_document_with_image_preview(monkeypatch, tmp_path) -> None:
|
||||
def test_receipt_folder_persist_enriches_pdf_ocr_document_with_image_preview(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path / "storage"))
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
@@ -136,7 +145,9 @@ def test_receipt_folder_persist_enriches_pdf_ocr_document_with_image_preview(mon
|
||||
preview_path.write_bytes(b"rendered-preview")
|
||||
return preview_path
|
||||
|
||||
monkeypatch.setattr(DocumentPreviewAssets, "render_pdf_first_page", fake_render_pdf_first_page)
|
||||
monkeypatch.setattr(
|
||||
DocumentPreviewAssets, "render_pdf_first_page", fake_render_pdf_first_page
|
||||
)
|
||||
|
||||
service = ReceiptFolderService()
|
||||
result = service.persist_ocr_batch(
|
||||
@@ -162,7 +173,9 @@ def test_receipt_folder_persist_enriches_pdf_ocr_document_with_image_preview(mon
|
||||
|
||||
document = result.documents[0]
|
||||
assert document.receipt_id
|
||||
assert document.receipt_preview_url.endswith(f"/receipt-folder/{document.receipt_id}/preview")
|
||||
assert document.receipt_preview_url.endswith(
|
||||
f"/receipt-folder/{document.receipt_id}/preview"
|
||||
)
|
||||
assert document.preview_kind == "image"
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
@@ -173,13 +186,16 @@ def test_receipt_folder_pdf_preview_regenerates_stale_cached_image(monkeypatch,
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
is_admin=False,
|
||||
)
|
||||
stale_preview = b"stale-preview"
|
||||
preview_data_url = f"data:image/png;base64,{base64.b64encode(stale_preview).decode('ascii')}"
|
||||
preview_data_url = (
|
||||
f"data:image/png;base64,{base64.b64encode(stale_preview).decode('ascii')}"
|
||||
)
|
||||
service = ReceiptFolderService()
|
||||
receipt = service.save_receipt(
|
||||
filename="2月20_武汉-上海.pdf",
|
||||
@@ -205,7 +221,9 @@ def test_receipt_folder_pdf_preview_regenerates_stale_cached_image(monkeypatch,
|
||||
preview_path.write_bytes(b"refreshed-preview")
|
||||
return preview_path
|
||||
|
||||
monkeypatch.setattr(DocumentPreviewAssets, "render_pdf_first_page", fake_render_pdf_first_page)
|
||||
monkeypatch.setattr(
|
||||
DocumentPreviewAssets, "render_pdf_first_page", fake_render_pdf_first_page
|
||||
)
|
||||
|
||||
resolved_path, media_type, file_name = service.resolve_preview(receipt.id, current_user)
|
||||
|
||||
@@ -227,13 +245,16 @@ def test_receipt_folder_pdf_preview_falls_back_to_source_when_render_fonts_missi
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
is_admin=False,
|
||||
)
|
||||
stale_preview = b"broken-preview"
|
||||
preview_data_url = f"data:image/png;base64,{base64.b64encode(stale_preview).decode('ascii')}"
|
||||
preview_data_url = (
|
||||
f"data:image/png;base64,{base64.b64encode(stale_preview).decode('ascii')}"
|
||||
)
|
||||
service = ReceiptFolderService()
|
||||
receipt = service.save_receipt(
|
||||
filename="2月20_武汉-上海.pdf",
|
||||
@@ -256,7 +277,9 @@ def test_receipt_folder_pdf_preview_falls_back_to_source_when_render_fonts_missi
|
||||
def fake_render_pdf_first_page(*, pdf_path, preview_path, timeout_seconds):
|
||||
raise RuntimeError("Missing language pack for 'Adobe-GB1' mapping")
|
||||
|
||||
monkeypatch.setattr(DocumentPreviewAssets, "render_pdf_first_page", fake_render_pdf_first_page)
|
||||
monkeypatch.setattr(
|
||||
DocumentPreviewAssets, "render_pdf_first_page", fake_render_pdf_first_page
|
||||
)
|
||||
|
||||
resolved_path, media_type, file_name = service.resolve_preview(receipt.id, current_user)
|
||||
|
||||
@@ -280,6 +303,7 @@ def test_receipt_folder_train_ticket_extracts_passenger_from_id_line_and_purchas
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
@@ -314,9 +338,13 @@ def test_receipt_folder_train_ticket_extracts_passenger_from_id_line_and_purchas
|
||||
scene_code="travel",
|
||||
scene_label="差旅票据",
|
||||
document_fields=[
|
||||
OcrRecognizeFieldRead(key="merchant_name", label="商户", value="电子发票(铁路"),
|
||||
OcrRecognizeFieldRead(
|
||||
key="merchant_name", label="商户", value="电子发票(铁路"
|
||||
),
|
||||
OcrRecognizeFieldRead(key="amount", label="金额", value="354元"),
|
||||
OcrRecognizeFieldRead(key="date", label="列车出发时间", value="2026-02-20 07:55"),
|
||||
OcrRecognizeFieldRead(
|
||||
key="date", label="列车出发时间", value="2026-02-20 07:55"
|
||||
),
|
||||
OcrRecognizeFieldRead(key="trip_no", label="车次", value="G458"),
|
||||
OcrRecognizeFieldRead(key="route", label="行程", value="武汉-上海"),
|
||||
],
|
||||
@@ -349,6 +377,7 @@ def test_receipt_folder_train_ticket_repairs_invalid_generated_fields_from_ocr_t
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
@@ -401,12 +430,20 @@ def test_receipt_folder_train_ticket_repairs_invalid_generated_fields_from_ocr_t
|
||||
scene_label="差旅票据",
|
||||
document_fields=[
|
||||
OcrRecognizeFieldRead(key="amount", label="金额", value="438元"),
|
||||
OcrRecognizeFieldRead(key="date", label="列车出发时间", value="2026-02-21 08:30"),
|
||||
OcrRecognizeFieldRead(key="invoice_number", label="票据号码", value="DEMO202602210001"),
|
||||
OcrRecognizeFieldRead(
|
||||
key="date", label="列车出发时间", value="2026-02-21 08:30"
|
||||
),
|
||||
OcrRecognizeFieldRead(
|
||||
key="invoice_number", label="票据号码", value="DEMO202602210001"
|
||||
),
|
||||
OcrRecognizeFieldRead(key="trip_no", label="车次", value="G999"),
|
||||
OcrRecognizeFieldRead(key="route", label="行程", value="上海-深圳"),
|
||||
OcrRecognizeFieldRead(key="departure_station", label="出发地点", value="二等座"),
|
||||
OcrRecognizeFieldRead(key="arrival_station", label="到达地点", value="扫码无效"),
|
||||
OcrRecognizeFieldRead(
|
||||
key="departure_station", label="出发地点", value="二等座"
|
||||
),
|
||||
OcrRecognizeFieldRead(
|
||||
key="arrival_station", label="到达地点", value="扫码无效"
|
||||
),
|
||||
OcrRecognizeFieldRead(key="passenger_name", label="乘车人", value="席别二等座"),
|
||||
],
|
||||
),
|
||||
@@ -431,6 +468,7 @@ def test_receipt_folder_delete_removes_duplicate_marker(monkeypatch, tmp_path) -
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
@@ -480,11 +518,14 @@ def test_receipt_folder_delete_removes_duplicate_marker(monkeypatch, tmp_path) -
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_receipt_folder_duplicate_uses_newer_ocr_when_existing_meta_is_weaker(monkeypatch, tmp_path) -> None:
|
||||
def test_receipt_folder_duplicate_uses_newer_ocr_when_existing_meta_is_weaker(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path / "storage"))
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
@@ -538,7 +579,9 @@ def test_receipt_folder_duplicate_uses_newer_ocr_when_existing_meta_is_weaker(mo
|
||||
assert document.receipt_id == stale_receipt.id
|
||||
assert document.document_type == "train_ticket"
|
||||
assert document.document_type_label == "火车/高铁票"
|
||||
assert any(field.label == "金额" and field.value == "354元" for field in document.document_fields)
|
||||
assert any(
|
||||
field.label == "金额" and field.value == "354元" for field in document.document_fields
|
||||
)
|
||||
assert any("重复上传" in warning for warning in document.warnings)
|
||||
|
||||
repaired = service.get_receipt(stale_receipt.id, current_user)
|
||||
@@ -549,11 +592,14 @@ def test_receipt_folder_duplicate_uses_newer_ocr_when_existing_meta_is_weaker(mo
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_receipt_folder_recovers_train_ticket_detail_from_other_english_ocr(monkeypatch, tmp_path) -> None:
|
||||
def test_receipt_folder_recovers_train_ticket_detail_from_other_english_ocr(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path / "storage"))
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
@@ -610,11 +656,14 @@ def test_receipt_folder_recovers_train_ticket_detail_from_other_english_ocr(monk
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_receipt_folder_unlink_receipts_for_claim_marks_linked_receipts_unlinked(monkeypatch, tmp_path) -> None:
|
||||
def test_receipt_folder_unlink_receipts_for_claim_marks_linked_receipts_unlinked(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path / "storage"))
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
current_user = CurrentUserContext(
|
||||
tenant_id="default",
|
||||
username="pytest",
|
||||
name="Py Test",
|
||||
role_codes=[],
|
||||
|
||||
Reference in New Issue
Block a user