fix(expense): narrow travel route risk indicators

This commit is contained in:
caoxiaozhu
2026-06-17 09:36:24 +08:00
parent 9f7b8b46a3
commit 470f343b29
10 changed files with 1040 additions and 368 deletions

View File

@@ -160,6 +160,52 @@ def _add_vague_goods_rule_asset(
)
def _add_multi_city_reason_rule_asset(
db: Session,
manager: AgentAssetRuleLibraryManager,
) -> None:
rule_code = "risk.travel.medium.multi_city_no_reason"
file_name = f"{rule_code}.json"
payload = {
"schema_version": "2.0",
"rule_code": rule_code,
"name": "多城市行程缺少说明中风险",
"evaluator": "multi_city_reason_required",
"enabled": True,
"requires_attachment": True,
"applies_to": {
"domains": ["expense", "travel"],
"expense_types": ["travel"],
"business_stages": ["reimbursement"],
},
"outcomes": {"fail": {"severity": "medium", "action": "manual_review"}},
}
manager.write_rule_library_json(
library=RISK_RULES_LIBRARY,
file_name=file_name,
payload=payload,
)
db.add(
AgentAsset(
asset_type=AgentAssetType.RULE.value,
code=rule_code,
name="多城市行程缺少说明中风险",
description="",
domain=AgentAssetDomain.EXPENSE.value,
scenario_json=["差旅费"],
owner="pytest",
status=AgentAssetStatus.ACTIVE.value,
current_version="v1.0.0",
published_version="v1.0.0",
config_json={
"detail_mode": "json_risk",
"rule_library": RISK_RULES_LIBRARY,
"rule_document": {"file_name": file_name},
},
)
)
def _write_attachment_meta(storage_root, invoice_id: str, meta: dict[str, Any]) -> None:
file_path = storage_root / invoice_id
file_path.parent.mkdir(parents=True, exist_ok=True)
@@ -520,3 +566,78 @@ def test_vague_ticket_content_still_flags_unclear_goods_name(
assert len(rule_flags) == 1
assert rule_flags[0]["severity"] == "low"
assert rule_flags[0]["evidence"]["matched_keywords"] == ["服务费"]
def test_multi_city_reason_risk_marks_only_abnormal_route_items(
tmp_path,
monkeypatch,
) -> None:
with build_session() as db:
manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
storage_root = tmp_path / "attachments"
_patch_rule_manager(monkeypatch, manager)
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: storage_root)
_add_multi_city_reason_rule_asset(db, manager)
claim = _build_claim(claim_no="RE-MULTI-CITY-001", expense_type="travel")
claim.location = "上海"
claim.reason = "支撑国网仿生产环境部署"
routes = [
("outbound", "武汉-上海", date(2026, 2, 20), Decimal("354.00")),
("extra-out", "上海-深圳", date(2026, 2, 21), Decimal("438.00")),
("extra-back", "深圳-上海", date(2026, 2, 22), Decimal("388.00")),
("return", "上海-武汉", date(2026, 2, 23), Decimal("354.00")),
]
claim.items = [
ExpenseClaimItem(
item_date=item_date,
item_type="train_ticket",
item_reason=route,
item_location=route,
item_amount=amount,
invoice_id=f"claim-multi-city/{suffix}.pdf",
)
for suffix, route, item_date, amount in routes
]
db.add(claim)
db.commit()
for suffix, route, _, _ in routes:
_write_attachment_meta(
storage_root,
f"claim-multi-city/{suffix}.pdf",
{
"document_info": {
"document_type": "train_ticket",
"document_type_label": "火车票",
"scene_code": "travel",
"scene_label": "交通票据",
"fields": [
{"key": "route", "label": "路线", "value": route},
],
},
"ocr_summary": f"火车票;{route}",
"ocr_text": f"旅客行程为 {route}",
},
)
review = ExpenseClaimService(db).evaluate_platform_risk_rules(
claim,
business_stage="reimbursement",
)
rule_flags = [
flag
for flag in review["flags"]
if isinstance(flag, dict)
and flag.get("rule_code") == "risk.travel.medium.multi_city_no_reason"
]
assert len(rule_flags) == 1
flagged_item_ids = set(rule_flags[0]["item_ids"])
route_item_ids = {item.item_reason: item.id for item in claim.items}
assert flagged_item_ids == {
route_item_ids["上海-深圳"],
route_item_ids["深圳-上海"],
}
assert route_item_ids["武汉-上海"] not in flagged_item_ids
assert route_item_ids["上海-武汉"] not in flagged_item_ids