Files
AFC-Demo/app/Services/KycRiskCalculator.php
T

367 lines
10 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Support\Collection;
class KycRiskCalculator
{
/**
* KYC Risk Level Schwellenwerte (3 Levels)
*/
private const RISK_THRESHOLDS = [
'low' => 40, // 0-40 = Geringes Risiko
'high' => 70, // 41-70 = Hohes Risiko
'critical' => 100, // 71-100 = Kritisches Risiko
];
/**
* Gewichtung der verschiedenen Risk-Faktoren
*/
private const RISK_WEIGHTS = [
'transaction_score' => 0.40, // 40% Gewichtung
'sanctions' => 0.25, // 25% Gewichtung
'country_risk' => 0.15, // 15% Gewichtung
'pep_adverse' => 0.10, // 10% Gewichtung
'corruption' => 0.10, // 10% Gewichtung
];
/**
* Berechne KYC Risk Level basierend auf Transaction Outputs
*
* @param Collection $outputs Collection von backend_data_pool records für eine Transaction
* @return string 'low', 'high', or 'critical'
*/
public function calculateKycRiskLevel(Collection $outputs): string
{
// Wenn keine Outputs vorhanden: Default = high (vorsichtig)
if ($outputs->isEmpty()) {
return 'high';
}
// Konvertiere Collection zu Key-Value Array für einfacheren Zugriff
$outputsArray = $outputs->pluck('content', 'output_key')->toArray();
$scores = [
'transaction_score' => $this->calculateTransactionScore($outputsArray),
'sanctions' => $this->calculateSanctionsScore($outputsArray),
'country_risk' => $this->calculateCountryRiskScore($outputsArray),
'pep_adverse' => $this->calculatePepAdverseScore($outputsArray),
'corruption' => $this->calculateCorruptionScore($outputsArray),
];
$weightedScore = $this->calculateWeightedScore($scores);
return $this->determineRiskLevel($weightedScore);
}
/**
* Berechne Transaction Score (0-100)
*/
private function calculateTransactionScore(array $outputs): int
{
if (! isset($outputs['tranx_score'])) {
return 60; // Default: Hohes Risiko wenn keine Daten
}
$scoreData = json_decode($outputs['tranx_score'], true);
// Format: {"score": 75}
if (is_array($scoreData) && isset($scoreData['score'])) {
return min(100, max(0, (int) $scoreData['score']));
}
// Format: {"risk_level": "high"}
if (is_array($scoreData) && isset($scoreData['risk_level'])) {
return $this->mapRiskLevelToScore($scoreData['risk_level']);
}
// Falls nur numerischer Wert
if (is_numeric($scoreData)) {
return min(100, max(0, (int) $scoreData));
}
return 60; // Default
}
/**
* Berechne Sanctions Score (0-100)
*/
private function calculateSanctionsScore(array $outputs): int
{
$sanctionFlags = [
'corporate_eusanctions',
'corporate_ofacsanctions',
'corporate_uksanctions',
'sanctions_circumvention',
];
$score = 0;
$foundSanctions = 0;
foreach ($sanctionFlags as $flag) {
if (! isset($outputs[$flag])) {
continue;
}
$data = json_decode($outputs[$flag], true);
if ($this->hasSanctionsMatch($data)) {
$foundSanctions++;
if (is_array($data) && isset($data['severity'])) {
$score += $this->mapSeverityToScore($data['severity']);
} else {
$score += 70; // Default: Hohes Risiko
}
}
}
if ($foundSanctions === 0) {
return 0;
}
$avgScore = $score / $foundSanctions;
// Mehrere Sanctions → Kritisches Risiko
if ($foundSanctions > 1) {
return min(100, (int) round($avgScore * 1.3));
}
return (int) round($avgScore);
}
/**
* Berechne Country Risk Score (0-100)
*/
private function calculateCountryRiskScore(array $outputs): int
{
if (! isset($outputs['country_risk'])) {
return 40; // Default: Geringes Risiko
}
$countryRiskData = json_decode($outputs['country_risk'], true);
if (is_array($countryRiskData) && isset($countryRiskData['score'])) {
return min(100, max(0, (int) $countryRiskData['score']));
}
if (is_array($countryRiskData) && isset($countryRiskData['level'])) {
return $this->mapRiskLevelToScore($countryRiskData['level']);
}
if (is_bool($countryRiskData)) {
return $countryRiskData ? 80 : 20;
}
return 40;
}
/**
* Berechne PEP & Adverse Media Score (0-100)
*/
private function calculatePepAdverseScore(array $outputs): int
{
$score = 0;
$count = 0;
$pepFields = [
'corporate_pepexposure' => 75,
'corporate_pep' => 80,
'corporate_adverse' => 70,
'corporate_AMLexposure' => 85,
];
foreach ($pepFields as $field => $defaultScore) {
if (! isset($outputs[$field])) {
continue;
}
$data = json_decode($outputs[$field], true);
if ($this->hasPositiveMatch($data)) {
$score += $this->extractScoreFromData($data, $defaultScore);
$count++;
}
}
return $count > 0 ? (int) round($score / $count) : 0;
}
/**
* Berechne Corruption Score (0-100)
*/
private function calculateCorruptionScore(array $outputs): int
{
$corruptionFields = [
'corruption_sector',
'corruption_country',
'corruption_relationship',
'corporate_corruptionexposure',
];
$totalScore = 0;
$count = 0;
foreach ($corruptionFields as $field) {
if (! isset($outputs[$field])) {
continue;
}
$data = json_decode($outputs[$field], true);
if ($this->hasPositiveMatch($data)) {
$totalScore += $this->extractScoreFromData($data, 70);
$count++;
}
}
return $count > 0 ? (int) round($totalScore / $count) : 0;
}
/**
* Berechne gewichteten Gesamt-Score
*/
private function calculateWeightedScore(array $scores): float
{
$totalScore = 0.0;
foreach ($scores as $category => $score) {
$weight = self::RISK_WEIGHTS[$category] ?? 0;
$totalScore += $score * $weight;
}
return $totalScore;
}
/**
* Bestimme Risk Level basierend auf Score (3 Levels)
*/
private function determineRiskLevel(float $score): string
{
if ($score <= self::RISK_THRESHOLDS['low']) {
return 'low'; // Geringes Risiko
}
if ($score <= self::RISK_THRESHOLDS['high']) {
return 'high'; // Hohes Risiko
}
return 'critical'; // Kritisches Risiko
}
/**
* Hilfsfunktionen
*/
private function mapRiskLevelToScore(string $level): int
{
return match (strtolower($level)) {
'low', 'green', 'gering', 'geringes risiko' => 25,
'high', 'yellow', 'orange', 'hoch', 'hohes risiko' => 60,
'critical', 'red', 'kritisch', 'kritisches risiko' => 85,
default => 60,
};
}
private function mapSeverityToScore(string $severity): int
{
return match (strtolower($severity)) {
'minor', 'low', 'gering' => 40,
'moderate', 'high', 'hoch' => 65,
'critical', 'severe', 'kritisch' => 90,
default => 65,
};
}
private function hasSanctionsMatch($data): bool
{
if (! is_array($data)) {
return false;
}
return (isset($data['matches']) && ! empty($data['matches']))
|| (isset($data['found']) && $data['found'] === true)
|| (isset($data['sanctioned']) && $data['sanctioned'] === true);
}
private function hasPositiveMatch($data): bool
{
if (is_array($data)) {
return (isset($data['match']) && $data['match'] === true)
|| (isset($data['found']) && $data['found'] === true)
|| (isset($data['matches']) && ! empty($data['matches']))
|| (isset($data['exposure']) && $data['exposure'] === true);
}
if (is_bool($data)) {
return $data;
}
return false;
}
private function extractScoreFromData($data, int $default): int
{
if (is_array($data) && isset($data['score'])) {
return min(100, max(0, (int) $data['score']));
}
if (is_array($data) && isset($data['risk_score'])) {
return min(100, max(0, (int) $data['risk_score']));
}
return $default;
}
/**
* Berechne aggregierten Risk Level für eine Company basierend auf allen Transaktionen
*/
public function calculateCompanyRiskLevel(Collection $transactions): string
{
if ($transactions->isEmpty()) {
return 'high'; // Default: Hohes Risiko wenn keine Daten
}
$riskScores = [
'low' => 0,
'high' => 0,
'critical' => 0,
];
// Zähle Risk Levels aller Transaktionen
foreach ($transactions as $transactionOutputs) {
$riskLevel = $this->calculateKycRiskLevel($transactionOutputs);
$riskScores[$riskLevel]++;
}
$totalTransactions = $transactions->count();
// Worst-Case-Prinzip:
// 1. Wenn EINE Transaktion kritisches Risiko hat → Company ist kritisch
if ($riskScores['critical'] > 0) {
return 'critical';
}
// 2. Wenn >30% der Transaktionen hohes Risiko haben → Company ist kritisch
$highRiskPercentage = $riskScores['high'] / $totalTransactions;
if ($highRiskPercentage > 0.3) {
return 'critical';
}
// 3. Wenn >10% hohes Risiko → Company ist hohes Risiko
if ($highRiskPercentage > 0.1) {
return 'high';
}
// 4. Wenn >80% geringes Risiko → Company ist geringes Risiko
if ($riskScores['low'] / $totalTransactions > 0.8) {
return 'low';
}
// 5. Default: Hohes Risiko (vorsichtig)
return 'high';
}
}