from __future__ import annotations from datetime import UTC, datetime, timedelta from typing import Any from sqlalchemy import or_, select from app.models.financial_record import ExpenseClaim from app.services.expense_claim_constants import ( AI_REVIEW_LOOKBACK_DAYS, AI_REVIEW_REPEAT_RISK_BLOCK_COUNT, AI_REVIEW_REPEAT_RISK_WARNING_COUNT, ) from app.services.expense_claim_item_sync import ExpenseClaimItemSyncMixin from app.services.expense_claim_platform_risk import ExpenseClaimPlatformRiskMixin from app.services.expense_claim_policy_review import ExpenseClaimPolicyReviewMixin class ExpenseClaimRiskReviewMixin( ExpenseClaimPlatformRiskMixin, ExpenseClaimPolicyReviewMixin, ExpenseClaimItemSyncMixin, ): def _run_ai_submission_review(self, claim: ExpenseClaim) -> dict[str, Any]: base_flags = list(claim.risk_flags_json or []) attachment_flags = [ flag for flag in base_flags if isinstance(flag, dict) and str(flag.get("source") or "").strip() == "attachment_analysis" ] preserved_flags = [ flag for flag in base_flags if not (isinstance(flag, dict) and str(flag.get("source") or "").strip() == "submission_review") ] review_flags: list[dict[str, Any]] = [] attention_reasons: list[str] = [] high_attachment_flags = [ flag for flag in attachment_flags if str(flag.get("severity") or "").strip().lower() == "high" ] medium_attachment_flags = [ flag for flag in attachment_flags if str(flag.get("severity") or "").strip().lower() == "medium" ] if high_attachment_flags: attention_reasons.append("存在高风险票据,需审批人重点复核。") review_flags.append( { "source": "submission_review", "severity": "high", "label": "AI预审重点复核", "message": ( f"AI预审发现 {len(high_attachment_flags)} 条高风险附件," "已随单流转给审批人重点复核。" ), } ) elif medium_attachment_flags: review_flags.append( { "source": "submission_review", "severity": "medium", "label": "AI预审提醒", "message": f"AI预审发现 {len(medium_attachment_flags)} 条中风险附件,已随单流转给审批人复核。", } ) manager_name = self._resolve_claim_manager_name(claim) if not manager_name: attention_reasons.append("未识别到该员工的直属领导,需审批环节补充分配。") review_flags.append( { "source": "submission_review", "severity": "medium", "label": "审批链待分配", "message": "AI预审发现直属领导缺失,已提交到审批环节等待分配或复核。", } ) historical_risk_count = self._count_recent_risky_claims(claim) if historical_risk_count >= AI_REVIEW_REPEAT_RISK_BLOCK_COUNT: review_flags.append( { "source": "submission_review", "severity": "medium", "label": "历史风险偏高", "message": ( f"近 {AI_REVIEW_LOOKBACK_DAYS} 天内该员工已有 {historical_risk_count} 笔带风险标记的报销," "本次已追加到审批链重点关注。" ), } ) elif historical_risk_count >= AI_REVIEW_REPEAT_RISK_WARNING_COUNT: review_flags.append( { "source": "submission_review", "severity": "low", "label": "历史风险提醒", "message": ( f"近 {AI_REVIEW_LOOKBACK_DAYS} 天内该员工已有 {historical_risk_count} 笔带风险标记的报销," "建议直属领导重点复核。" ), } ) travel_review = self._run_travel_policy_review(claim) attention_reasons.extend(travel_review["blocking_reasons"]) review_flags.extend(travel_review["flags"]) scene_policy_review = self._run_scene_policy_review(claim) attention_reasons.extend(scene_policy_review["blocking_reasons"]) review_flags.extend(scene_policy_review["flags"]) platform_risk_review = self.evaluate_platform_risk_rules(claim) attention_reasons.extend(platform_risk_review["blocking_reasons"]) review_flags.extend(platform_risk_review["flags"]) if attention_reasons: summary_message = "AI预审发现需审批重点关注事项:" + ";".join( dict.fromkeys(attention_reasons) ) review_flags.insert( 0, { "source": "submission_review", "severity": "medium", "label": "AI预审重点复核", "message": summary_message, }, ) return { "status": "submitted", "approval_stage": "直属领导审批", "risk_flags": preserved_flags + review_flags, "message": ( f"报销单 {claim.claim_no} 已完成 AI预审," f"现已提交给直属领导 {manager_name or '审批人'} 审批。" ), "passed": True, } @staticmethod def _resolve_claim_manager_name(claim: ExpenseClaim) -> str: if claim.employee is not None: if claim.employee.manager is not None and claim.employee.manager.name: return str(claim.employee.manager.name).strip() if claim.employee.organization_unit is not None and claim.employee.organization_unit.manager_name: return str(claim.employee.organization_unit.manager_name).strip() return "" def _count_recent_risky_claims(self, claim: ExpenseClaim) -> int: filters = [] if claim.employee_id: filters.append(ExpenseClaim.employee_id == claim.employee_id) elif claim.employee_name: filters.append(ExpenseClaim.employee_name == claim.employee_name) if not filters: return 0 since = datetime.now(UTC) - timedelta(days=AI_REVIEW_LOOKBACK_DAYS) stmt = ( select(ExpenseClaim) .where(or_(*filters)) .where(ExpenseClaim.id != claim.id) .where(ExpenseClaim.occurred_at >= since) ) recent_claims = list(self.db.scalars(stmt).all()) return sum(1 for item in recent_claims if list(item.risk_flags_json or []))