From 5747e85acfc319ab968efdf9404edb4beb92b2f5 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Mon, 15 Jun 2026 20:20:55 +0800 Subject: [PATCH] fix(risk): restore upload-time rule center review --- .../services/risk_rule_template_executor.py | 11 ++ server/tests/test_expense_claim_service.py | 148 ++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/server/src/app/services/risk_rule_template_executor.py b/server/src/app/services/risk_rule_template_executor.py index 07e228f..ad69b1a 100644 --- a/server/src/app/services/risk_rule_template_executor.py +++ b/server/src/app/services/risk_rule_template_executor.py @@ -941,6 +941,8 @@ class RiskRuleTemplateExecutor: time_keys = ( "application_time", "applicationTime", + "application_business_time", + "applicationBusinessTime", "application_date", "applicationDate", "business_time", @@ -975,6 +977,15 @@ class RiskRuleTemplateExecutor: if isinstance(nested, dict): sources.append(nested) for source_dict in sources: + business_time_context = source_dict.get("business_time_context") + if isinstance(business_time_context, dict): + start_date = str(business_time_context.get("start_date") or "").strip() + end_date = str(business_time_context.get("end_date") or start_date).strip() + display_value = str(business_time_context.get("display_value") or "").strip() + if display_value: + values.append(display_value) + elif start_date: + values.append(f"{start_date} 至 {end_date or start_date}") for key in time_keys: value = source_dict.get(key) if value not in (None, ""): diff --git a/server/tests/test_expense_claim_service.py b/server/tests/test_expense_claim_service.py index 1d59ba6..4c2d5e7 100644 --- a/server/tests/test_expense_claim_service.py +++ b/server/tests/test_expense_claim_service.py @@ -2216,6 +2216,154 @@ def test_upload_attachment_response_includes_refreshed_rule_center_risk_flags( assert payload["claim_risk_flags"] == claim.risk_flags_json +def test_upload_attachment_runs_rule_center_city_risk_from_origin_destination_fields( + monkeypatch, + tmp_path, +) -> None: + current_user = CurrentUserContext( + username="emp-1", + name="张三", + role_codes=[], + is_admin=False, + ) + + def fake_recognize( + self, + files: list[tuple[str, bytes, str | None]], + ) -> OcrRecognizeBatchRead: + return OcrRecognizeBatchRead( + total_file_count=1, + success_count=1, + documents=[ + OcrRecognizeDocumentRead( + filename="train-ticket.png", + media_type="image/png", + text="铁路电子客票 出发城市 武汉 到达城市 北京 2026-02-20 票价354元", + summary="铁路电子客票,武汉至北京,票价 354 元。", + avg_score=0.98, + line_count=1, + page_count=1, + document_type="train_ticket", + document_type_label="火车/高铁票", + scene_code="travel", + scene_label="差旅票据", + document_fields=[ + {"key": "origin", "label": "出发城市", "value": "武汉"}, + {"key": "destination", "label": "到达城市", "value": "北京"}, + {"key": "trip_date", "label": "列车出发时间", "value": "2026-02-20"}, + {"key": "fare", "label": "票价", "value": "354元"}, + ], + ) + ], + ) + + monkeypatch.setattr(OcrService, "recognize_files", fake_recognize) + monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path) + + with build_session() as db: + claim = build_claim(expense_type="travel", location="上海") + claim.reason = "支撑国网仿生产环境部署" + claim.items[0].item_location = "上海" + claim.items[0].invoice_id = None + db.add(claim) + db.commit() + + payload = ExpenseClaimService(db).upload_claim_item_attachment( + claim_id=claim.id, + item_id=claim.items[0].id, + filename="train-ticket.png", + content=b"fake-image-bytes", + media_type="image/png", + current_user=current_user, + ) + + flags = payload["claim_risk_flags"] + assert any( + isinstance(flag, dict) + and flag.get("rule_code") == "risk.travel.high.city_mismatch" + for flag in flags + ) + + +def test_upload_attachment_uses_linked_application_business_time_for_date_risk( + monkeypatch, + tmp_path, +) -> None: + current_user = CurrentUserContext( + username="emp-1", + name="张三", + role_codes=[], + is_admin=False, + ) + + def fake_recognize( + self, + files: list[tuple[str, bytes, str | None]], + ) -> OcrRecognizeBatchRead: + return OcrRecognizeBatchRead( + total_file_count=1, + success_count=1, + documents=[ + OcrRecognizeDocumentRead( + filename="late-train-ticket.png", + media_type="image/png", + text="铁路电子客票 武汉-上海 2026-03-01 票价354元", + summary="铁路电子客票,武汉至上海,2026-03-01 出发。", + avg_score=0.98, + line_count=1, + page_count=1, + document_type="train_ticket", + document_type_label="火车/高铁票", + scene_code="travel", + scene_label="差旅票据", + document_fields=[ + {"key": "route", "label": "行程", "value": "武汉-上海"}, + {"key": "trip_date", "label": "列车出发时间", "value": "2026-03-01"}, + {"key": "fare", "label": "票价", "value": "354元"}, + ], + ) + ], + ) + + monkeypatch.setattr(OcrService, "recognize_files", fake_recognize) + monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path) + + with build_session() as db: + claim = build_claim(expense_type="travel", location="上海") + claim.reason = "支撑国网仿生产环境部署" + claim.items[0].item_location = "上海" + claim.items[0].invoice_id = None + claim.risk_flags_json = [ + { + "source": "application_link", + "application_claim_id": "application-date-risk", + "application_claim_no": "AP-20260220-TEST", + "application_detail": { + "application_business_time": "2026-02-20 至 2026-02-23", + "application_location": "上海", + }, + } + ] + db.add(claim) + db.commit() + + payload = ExpenseClaimService(db).upload_claim_item_attachment( + claim_id=claim.id, + item_id=claim.items[0].id, + filename="late-train-ticket.png", + content=b"fake-image-bytes", + media_type="image/png", + current_user=current_user, + ) + + flags = payload["claim_risk_flags"] + assert any( + isinstance(flag, dict) + and flag.get("rule_code") == "risk.travel.high.date_outside_trip" + for flag in flags + ) + + def test_upload_hotel_attachment_audits_date_like_amount(monkeypatch, tmp_path) -> None: current_user = CurrentUserContext( username="emp-1",