from __future__ import annotations from datetime import UTC, date, datetime, timedelta from decimal import Decimal from app.models.financial_record import ExpenseClaim, ExpenseClaimItem from app.models.risk_disposition import RiskDisposition from app.models.risk_observation import RiskObservation from app.services.approval_workbench import ApprovalWorkbenchService def _claim( *, claim_no: str = "RE-WORKBENCH-1", amount: str = "888.00", submitted_at: datetime, risk_flags: list[dict] | None = None, invoice_count: int = 1, ) -> ExpenseClaim: claim = ExpenseClaim( id=f"claim-{claim_no.lower()}", claim_no=claim_no, employee_id=None, employee_name="张三", department_id=None, department_name="市场部", project_code="PRJ-WORKBENCH", expense_type="travel", reason="客户现场差旅", location="上海", amount=Decimal(amount), currency="CNY", invoice_count=invoice_count, occurred_at=submitted_at, submitted_at=submitted_at, status="submitted", approval_stage="直属领导审批", risk_flags_json=list(risk_flags or []), created_at=submitted_at, updated_at=submitted_at, ) claim.items = [ ExpenseClaimItem( id=f"item-{claim_no.lower()}", claim_id=claim.id, item_date=date(2026, 7, 15), item_type="hotel", item_reason="住宿", item_location="上海", item_note="", item_amount=Decimal(amount), invoice_id="INV-WORKBENCH" if invoice_count else None, created_at=submitted_at, updated_at=submitted_at, ) ] return claim def test_priority_queue_explains_risk_budget_sla_amount_and_history() -> None: now = datetime(2026, 7, 16, 12, 0, tzinfo=UTC) claim = _claim( amount="60000.00", submitted_at=now - timedelta(hours=26), risk_flags=[ { "source": "ai_pre_review", "severity": "high", "disposition": "review", "resolution_status": "unresolved", "route_decision": {"budget_result": {"metrics": {"after_usage_rate": "96.5"}}}, "historical_case_evidence": [{"label": "confirmed", "sample_id": "must-not-leak"}], } ], ) item = ApprovalWorkbenchService.build_item(claim, now=now) assert item.priority_score >= 85 assert item.priority_tier == "urgent" assert item.risk_level == "high" assert item.sla_overdue is True assert item.budget_usage_rate == 96.5 assert item.suggestion.action == "manual_review" assert item.evidence.historical_labels == ["历史已确认,仅供复核"] assert "must-not-leak" not in repr(item.model_dump()) assert {reason.code for reason in item.priority_reasons} >= { "open_risk", "sla_overdue", "budget_pressure", "large_amount", } def test_application_evidence_does_not_require_invoice_and_resolved_risk_is_ignored() -> None: now = datetime(2026, 7, 16, 12, 0, tzinfo=UTC) claim = _claim( claim_no="AP-WORKBENCH-1", submitted_at=now - timedelta(hours=1), invoice_count=0, risk_flags=[ { "source": "ai_pre_review", "severity": "critical", "resolution_status": "resolved", } ], ) item = ApprovalWorkbenchService.build_item(claim, now=now) assert item.evidence.completeness == 1 assert item.evidence.missing_labels == [] assert item.risk_level == "low" assert item.open_risk_count == 0 assert item.suggestion.action == "approve_candidate" assert item.priority_score == 0 def test_persisted_disposition_is_authoritative_over_stale_claim_risk_flags() -> None: now = datetime(2026, 7, 16, 12, 0, tzinfo=UTC) claim = _claim( submitted_at=now - timedelta(hours=1), risk_flags=[ { "severity": "critical", "triggered": True, "observation_key": "risk:workbench:resolved", } ], ) observation = RiskObservation( id="risk-workbench-resolved", tenant_id="default", observation_key="risk:workbench:resolved", subject_type="expense_claim", subject_key=f"claim:{claim.id}", subject_label=claim.claim_no, claim_id=claim.id, claim_no=claim.claim_no, risk_type="duplicate_invoice", risk_signal="duplicate_invoice", title="重复票据风险", description="已复核完成。", risk_score=95, risk_level="critical", confidence_score=0.96, control_stage="reimbursement", control_mode="risk_observation", automation_mode="semi_auto_review", source="financial_risk_graph", algorithm_version="v1", status="resolved", feedback_status="confirmed", ) disposition = RiskDisposition( tenant_id="default", observation_id=observation.id, adjudication="confirmed", lifecycle_status="resolved", version=2, ) item = ApprovalWorkbenchService.build_item( claim, now=now, observation_rows=[(observation, disposition)], ) assert item.risk_level == "low" assert item.open_risk_count == 0 assert item.suggestion.action == "approve_candidate" def test_persisted_low_risk_does_not_hide_unmaterialized_raw_high_risk() -> None: now = datetime(2026, 7, 16, 12, 0, tzinfo=UTC) claim = _claim( submitted_at=now - timedelta(hours=1), risk_flags=[ { "source": "attachment_analysis", "severity": "high", "label": "票据金额异常", "triggered": True, } ], ) observation = RiskObservation( id="risk-workbench-low", tenant_id="default", observation_key="risk:workbench:low", subject_type="expense_claim", subject_key=f"claim:{claim.id}", subject_label=claim.claim_no, claim_id=claim.id, claim_no=claim.claim_no, risk_type="minor_notice", risk_signal="minor_notice", title="普通提醒", description="普通提醒。", risk_score=30, risk_level="low", confidence_score=0.8, control_stage="reimbursement", control_mode="risk_observation", automation_mode="manual_review", source="financial_risk_graph", algorithm_version="v1", status="pending_review", feedback_status="unreviewed", ) item = ApprovalWorkbenchService.build_item( claim, now=now, observation_rows=[(observation, None)], ) assert item.risk_level == "high" assert item.open_risk_count == 2 assert item.suggestion.action == "manual_review"