feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记 - 完善管家意图规划器与模型计划构建器全链路 - 新增 OCR Worker 脚本、数据库会话管理与通知状态 - 优化文档中心、日志视图、预算中心与员工管理交互 - 增强工作台摘要、图标资源与全局主题样式 - 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, date, datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.config import SERVER_DIR
|
||||
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
|
||||
from app.services.risk_rule_dsl_examples import (
|
||||
get_risk_rule_dsl_example,
|
||||
@@ -166,6 +169,95 @@ def test_date_rule_uses_application_month_before_ticket_item_date() -> None:
|
||||
assert condition["outside_dates"] == ["2026-02-20"]
|
||||
|
||||
|
||||
def test_application_context_values_are_available_to_composite_rules() -> None:
|
||||
claim = _claim(amount=Decimal("3000.00"))
|
||||
claim.risk_flags_json = [
|
||||
{
|
||||
"source": "application_link",
|
||||
"application_claim_id": "application-ctx-1",
|
||||
"application_claim_no": "AP-202606-CTX",
|
||||
"application_detail": {
|
||||
"application_amount": "3000",
|
||||
"application_expense_type": "office",
|
||||
},
|
||||
}
|
||||
]
|
||||
manifest = {
|
||||
"template_key": COMPOSITE_RULE_TEMPLATE_KEY,
|
||||
"params": {
|
||||
"template_key": COMPOSITE_RULE_TEMPLATE_KEY,
|
||||
"conditions": [
|
||||
{
|
||||
"id": "application_present",
|
||||
"operator": "exists_any",
|
||||
"fields": ["application.id", "application.claim_no"],
|
||||
}
|
||||
],
|
||||
"hit_logic": "application_present",
|
||||
"condition_summary": "application exists",
|
||||
},
|
||||
}
|
||||
|
||||
result = RiskRuleTemplateExecutor().evaluate(manifest, claim=claim, contexts=[])
|
||||
|
||||
assert result is not None
|
||||
condition = result["evidence"]["conditions"][0]
|
||||
assert condition["values"] == ["application-ctx-1", "AP-202606-CTX"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("file_name", "expense_type", "amount"),
|
||||
[
|
||||
("risk.application.meal_high_value_without_preapproval.json", "meal", Decimal("501.00")),
|
||||
("risk.application.office_bulk_without_purchase.json", "office", Decimal("2001.00")),
|
||||
("risk.application.large_expense_without_preapproval.json", "software", Decimal("2001.00")),
|
||||
],
|
||||
)
|
||||
def test_preapproval_amount_rules_hit_without_linked_application(
|
||||
file_name: str,
|
||||
expense_type: str,
|
||||
amount: Decimal,
|
||||
) -> None:
|
||||
claim = _claim(amount=amount)
|
||||
claim.expense_type = expense_type
|
||||
manifest = _load_rule_manifest(file_name)
|
||||
|
||||
result = RiskRuleTemplateExecutor().evaluate(manifest, claim=claim, contexts=[])
|
||||
|
||||
assert result is not None
|
||||
assert result["evidence"]["condition_results"]["amount_exceeds_preapproval_threshold"] is True
|
||||
assert result["evidence"]["condition_results"]["application_present"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("file_name", "expense_type", "amount"),
|
||||
[
|
||||
("risk.application.meal_high_value_without_preapproval.json", "entertainment", Decimal("800.00")),
|
||||
("risk.application.office_bulk_without_purchase.json", "office", Decimal("2600.00")),
|
||||
("risk.application.large_expense_without_preapproval.json", "software", Decimal("2600.00")),
|
||||
],
|
||||
)
|
||||
def test_preapproval_amount_rules_skip_when_application_is_linked(
|
||||
file_name: str,
|
||||
expense_type: str,
|
||||
amount: Decimal,
|
||||
) -> None:
|
||||
claim = _claim(amount=amount)
|
||||
claim.expense_type = expense_type
|
||||
claim.risk_flags_json = [
|
||||
{
|
||||
"source": "application_link",
|
||||
"application_claim_id": "application-linked-ok",
|
||||
"application_claim_no": "AP-202606-OK",
|
||||
}
|
||||
]
|
||||
manifest = _load_rule_manifest(file_name)
|
||||
|
||||
result = RiskRuleTemplateExecutor().evaluate(manifest, claim=claim, contexts=[])
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def _claim(*, amount: Decimal = Decimal("1000.00")) -> ExpenseClaim:
|
||||
claim = ExpenseClaim(
|
||||
claim_no="TEST-RISK-RULE-DSL",
|
||||
@@ -193,3 +285,8 @@ def _claim(*, amount: Decimal = Decimal("1000.00")) -> ExpenseClaim:
|
||||
)
|
||||
]
|
||||
return claim
|
||||
|
||||
|
||||
def _load_rule_manifest(file_name: str) -> dict:
|
||||
path = Path(SERVER_DIR) / "rules" / "risk-rules" / file_name
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
Reference in New Issue
Block a user