Automatically score requests and route them to appropriate approval levels based on risk factors.
// Define risk scoring rules
import { createRiskScorer } from "@/lib/hitl/risk"
export const paymentRiskScorer = createRiskScorer({
name: "payment-risk",
// Risk factors with weights
factors: [
{
name: "amount",
weight: 0.3, // 30% of total score
scoring: (ctx) => {
const amount = ctx.data.amount
if (amount < 100) return 0
if (amount < 1000) return 25
if (amount < 10000) return 50
if (amount < 100000) return 75
return 100
}
},
{
name: "vendor-trust",
weight: 0.2,
scoring: (ctx) => {
const vendor = ctx.data.vendor
if (vendor.verified && vendor.transactionCount > 100) return 0
if (vendor.verified) return 25
if (vendor.transactionCount > 10) return 50
return 100 // New or unverified vendor
}
},
{
name: "user-history",
weight: 0.15,
scoring: async (ctx) => {
const user = await db.users.findById(ctx.data.userId)
const recentApprovals = await db.approvals.countRecent(user.id, "30d")
const rejectionRate = user.rejectionRate || 0
if (rejectionRate > 0.1) return 100
if (recentApprovals > 50) return 50 // High volume
return 0
}
},
{
name: "anomaly-detection",
weight: 0.2,
scoring: async (ctx) => {
const isAnomaly = await ml.detectAnomaly({
userId: ctx.data.userId,
amount: ctx.data.amount,
category: ctx.data.category,
time: ctx.data.timestamp
})
return isAnomaly.score * 100
}
},
{
name: "compliance-flags",
weight: 0.15,
scoring: (ctx) => {
let score = 0
if (ctx.data.crossBorder) score += 30
if (ctx.data.category === "cryptocurrency") score += 40
if (ctx.data.rushRequest) score += 20
return Math.min(score, 100)
}
}
],
// Automatic adjustments
adjustments: [
{
name: "time-sensitive",
condition: (ctx) => ctx.data.deadline &&
new Date(ctx.data.deadline) < new Date(Date.now() + 24 * 60 * 60 * 1000),
adjustment: 10 // Add 10 points for urgent requests
},
{
name: "trusted-department",
condition: (ctx) => ["finance", "legal"].includes(ctx.data.department),
adjustment: -15 // Reduce score for trusted departments
}
]
})// Route approvals based on risk score
hitl({
id: "payment-approval",
riskAssessment: {
scorer: paymentRiskScorer,
// Route based on risk level
routing: {
low: { // 0-25
action: "auto-approve",
notify: ["requester"]
},
medium: { // 26-50
approvers: { roles: ["finance-manager"] },
minApprovals: 1,
timeout: "24h"
},
high: { // 51-75
approvers: { roles: ["senior-finance-manager", "controller"] },
minApprovals: 1,
timeout: "8h",
escalateAfter: "4h"
},
critical: { // 76-100
approvers: { roles: ["cfo", "controller"] },
minApprovals: 2,
timeout: "4h",
escalateAfter: "2h",
notifications: {
channels: ["email", "slack", "sms"],
urgent: true
}
}
},
// Override routing for specific conditions
overrides: [
{
condition: (ctx, riskScore) =>
ctx.data.category === "legal-settlement",
routing: {
approvers: { roles: ["ceo", "general-counsel"] },
minApprovals: 2
}
}
]
}
})- Transaction amount
- Cumulative daily/monthly totals
- Variance from budget
- Payment method risk
- Vendor/customer verification
- Transaction history
- Geographic location
- Industry/category
- User approval history
- Time of request
- Request velocity
- Pattern anomalies
- Regulatory requirements
- Cross-border transactions
- Sensitive categories
- Documentation completeness
// Machine learning enhanced risk scoring
import { createMLRiskScorer } from "@/lib/hitl/risk-ml"
export const mlRiskScorer = createMLRiskScorer({
name: "ml-payment-risk",
// Base rule-based factors
baseFactors: paymentRiskScorer.factors,
// ML model configuration
ml: {
model: "risk-classifier-v2",
// Features for ML model
features: [
"amount",
"vendor.age_days",
"vendor.transaction_count",
"user.tenure_days",
"user.rejection_rate",
"time_of_day",
"day_of_week",
"category_encoded",
"amount_vs_avg_ratio"
],
// How to combine ML and rules
combination: "weighted_average",
weights: {
rules: 0.4,
ml: 0.6
},
// Confidence threshold
minConfidence: 0.7, // Fall back to rules if ML confidence low
// Continuous learning
feedback: {
enabled: true,
// Track approval outcomes
trackOutcomes: true,
// Retrain periodically
retrainSchedule: "weekly"
}
}
})