20, // 0-20 = Geringes Risiko (nur saubere EU/OECD Transaktionen ohne Red Flags) 'high' => 50, // 20-50 = Hohes Risiko (Tax Havens, High-Risk Countries, PEP, Corruption) 'critical' => 100, // 50-100 = Kritisches Risiko (Sanctions, Multiple Red Flags, Structuring) ]; /** * Gewichtung der verschiedenen Risk-Faktoren */ private const RISK_WEIGHTS = [ 'transaction_score' => 0.15, // 15% Gewichtung (reduziert für mehr Raum bei kritischen Faktoren) 'transaction_analysis' => 0.15, // 15% Gewichtung (duplicate, structuring, fake purpose) 'sanctions' => 0.25, // 25% Gewichtung (ERHÖHT - sehr kritisch!) 'country_risk' => 0.15, // 15% Gewichtung (ERHÖHT - wichtiger Indikator) 'pep_adverse' => 0.10, // 10% Gewichtung (ERHÖHT - staatliche Verbindungen) 'corruption' => 0.10, // 10% Gewichtung (ERHÖHT - Korruption ist kritisch) 'financial_health' => 0.07, // 7% Gewichtung (insolvency, liquidation, rating) 'legal_issues' => 0.03, // 3% Gewichtung (court cases, tax haven, export violations) ]; /** * Berechne KYC Risk Level basierend auf Transaction Outputs (Legacy - verwendet Entity) * * @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 { return $this->calculateEntityKycRiskLevel($outputs); } /** * Berechne Entity KYC Risk Level (sendende Partei) * * @param Collection $outputs Collection von backend_data_pool records * @return string 'low', 'high', or 'critical' */ public function calculateEntityKycRiskLevel(Collection $outputs): string { $breakdown = $this->calculateEntityKycRiskBreakdown($outputs); return $breakdown['risk_level']; } /** * Berechne Entity KYC Risk Level mit detailliertem Breakdown * * @param Collection $outputs Collection von backend_data_pool records * @return array Breakdown mit scores und risk_level */ public function calculateEntityKycRiskBreakdown(Collection $outputs): array { if ($outputs->isEmpty()) { return [ 'risk_level' => 'high', 'weighted_score' => 70.0, 'scores' => [], ]; } $outputsArray = $outputs->pluck('content', 'output_key')->toArray(); $scores = [ 'transaction_score' => $this->calculateTransactionScore($outputsArray), 'transaction_analysis' => $this->calculateTransactionAnalysisScore($outputsArray), 'sanctions' => $this->calculateSanctionsScore($outputsArray, 'entity_'), 'country_risk' => $this->calculateCountryRiskScore($outputsArray, 'entity_'), 'pep_adverse' => $this->calculatePepAdverseScore($outputsArray, 'entity_'), 'corruption' => $this->calculateCorruptionScore($outputsArray, 'entity_'), 'financial_health' => $this->calculateFinancialHealthScore($outputsArray, 'entity_'), 'legal_issues' => $this->calculateLegalIssuesScore($outputsArray, 'entity_'), ]; $weightedScore = $this->calculateWeightedScore($scores); $riskLevel = $this->determineRiskLevel($weightedScore); return [ 'risk_level' => $riskLevel, 'weighted_score' => round($weightedScore, 2), 'scores' => $scores, ]; } /** * Berechne Counterparty KYC Risk Level (empfangende Partei) * * @param Collection $outputs Collection von backend_data_pool records * @return string 'low', 'high', or 'critical' */ public function calculateCounterpartyKycRiskLevel(Collection $outputs): string { $breakdown = $this->calculateCounterpartyKycRiskBreakdown($outputs); return $breakdown['risk_level']; } /** * Berechne Counterparty KYC Risk Level mit detailliertem Breakdown * * @param Collection $outputs Collection von backend_data_pool records * @return array Breakdown mit scores und risk_level */ public function calculateCounterpartyKycRiskBreakdown(Collection $outputs): array { if ($outputs->isEmpty()) { return [ 'risk_level' => 'high', 'weighted_score' => 70.0, 'scores' => [], ]; } $outputsArray = $outputs->pluck('content', 'output_key')->toArray(); $scores = [ 'transaction_score' => $this->calculateTransactionScore($outputsArray), 'transaction_analysis' => $this->calculateTransactionAnalysisScore($outputsArray), 'sanctions' => $this->calculateSanctionsScore($outputsArray, 'counterparty_'), 'country_risk' => $this->calculateCountryRiskScore($outputsArray, 'counterparty_'), 'pep_adverse' => $this->calculatePepAdverseScore($outputsArray, 'counterparty_'), 'corruption' => $this->calculateCorruptionScore($outputsArray, 'counterparty_'), 'financial_health' => $this->calculateFinancialHealthScore($outputsArray, 'counterparty_'), 'legal_issues' => $this->calculateLegalIssuesScore($outputsArray, 'counterparty_'), ]; $weightedScore = $this->calculateWeightedScore($scores); $riskLevel = $this->determineRiskLevel($weightedScore); return [ 'risk_level' => $riskLevel, 'weighted_score' => round($weightedScore, 2), 'scores' => $scores, ]; } /** * 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, string $prefix = ''): int { $sanctionFlags = [ $prefix.'corporate_eusanctions', $prefix.'corporate_ofacsanctions', $prefix.'corporate_uksanctions', $prefix.'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, string $prefix = ''): int { $key = $prefix.'country_risk'; // Wenn Country Risk Daten vorhanden sind, diese verwenden if (isset($outputs[$key])) { $countryRiskData = json_decode($outputs[$key], true); // Prüfe ob flag=true ist (valide Daten vorhanden) if (is_array($countryRiskData) && isset($countryRiskData['flag']) && $countryRiskData['flag'] === true) { if (isset($countryRiskData['score'])) { return min(100, max(0, (int) $countryRiskData['score'])); } if (isset($countryRiskData['level'])) { return $this->mapRiskLevelToScore($countryRiskData['level']); } } } // FALLBACK: Versuche Land aus HQ oder anderen Feldern zu extrahieren $countryCode = $this->extractCountryCodeFromOutputs($outputs, $prefix); if ($countryCode) { return $this->getCountryRiskScoreByCode($countryCode); } return 40; // Default: Mittleres Risiko } /** * Extrahiere Ländercode aus verfügbaren Daten */ private function extractCountryCodeFromOutputs(array $outputs, string $prefix): ?string { // Bekannte Länder-Mappings (WICHTIG: Zuerst Länder-Namen matchen!) $countryPatterns = [ '/\bAzerbaijan\b/i' => 'AZ', '/\bGermany\b/i' => 'DE', '/\bDeutschland\b/i' => 'DE', '/\bRussia\b/i' => 'RU', '/\bChina\b/i' => 'CN', '/\bUnited States\b/i' => 'US', '/\bKazakhstan\b/i' => 'KZ', '/\bTurkmenistan\b/i' => 'TM', '/\bVenezuela\b/i' => 'VE', '/\bIran\b/i' => 'IR', '/\bNorth Korea\b/i' => 'KP', '/\bBelarus\b/i' => 'BY', '/\bCuba\b/i' => 'CU', '/\bMyanmar\b/i' => 'MM', '/\bSyria\b/i' => 'SY', '/\bAfghanistan\b/i' => 'AF', '/\bYemen\b/i' => 'YE', '/\bSomalia\b/i' => 'SO', '/\bSudan\b/i' => 'SD', '/\b(?:British )?Virgin Islands\b/i' => 'VG', // British Virgin Islands (Tax Haven) '/\bCayman Islands\b/i' => 'KY', // Cayman Islands (Tax Haven) '/\bBermuda\b/i' => 'BM', // Bermuda (Tax Haven) '/\bBahamas\b/i' => 'BS', // Bahamas (Tax Haven) '/\bPanama\b/i' => 'PA', // Panama (Tax Haven) '/\bLiechtenstein\b/i' => 'LI', // Liechtenstein (Tax Haven) '/\bMonaco\b/i' => 'MC', // Monaco (Tax Haven) '/\bLuxembourg\b/i' => 'LU', // Luxembourg (Tax Haven) ]; // HIGHEST PRIORITY: Check corporate_name field (company name often contains country) $nameKey = $prefix.'corporate_name'; if (isset($outputs[$nameKey])) { $nameData = json_decode($outputs[$nameKey], true); if (is_array($nameData) && isset($nameData['answer'])) { foreach ($countryPatterns as $pattern => $code) { if (preg_match($pattern, $nameData['answer'])) { return $code; } } } } // Versuche aus summary zu extrahieren (zweite Priorität) $summaryKey = $prefix.'corporate_summary'; if (isset($outputs[$summaryKey])) { $summaryData = json_decode($outputs[$summaryKey], true); if (is_array($summaryData) && isset($summaryData['answer'])) { $answer = $summaryData['answer']; foreach ($countryPatterns as $pattern => $code) { if (preg_match($pattern, $answer)) { return $code; } } } } // Versuche aus HQ zu extrahieren $hqKey = $prefix.'corporate_HQ'; if (isset($outputs[$hqKey])) { $hqData = json_decode($outputs[$hqKey], true); if (is_array($hqData) && isset($hqData['answer'])) { $answer = $hqData['answer']; // Erst Länder-Namen matchen foreach ($countryPatterns as $pattern => $code) { if (preg_match($pattern, $answer)) { return $code; } } // Falls keine Länder-Namen gefunden: Suche nach 2-Buchstaben Ländercodes // ABER: Exclude common false positives wie "AG" (Aktiengesellschaft) $excludePatterns = ['AG', 'SA', 'AB', 'CO', 'LT', 'LP', 'PC', 'UK']; // UK ist OK, aber später if (preg_match('/\b([A-Z]{2})\b/', $answer, $matches)) { $potentialCode = $matches[1]; if (! in_array($potentialCode, $excludePatterns)) { return $potentialCode; } } } } return null; } /** * Bestimme Country Risk Score basierend auf Ländercode (Fallback) */ private function getCountryRiskScoreByCode(string $countryCode): int { // Kritische Hochrisikoländer (Sanktionen, Terror) $criticalRiskCountries = [ 'KP' => 95, // Nordkorea 'IR' => 95, // Iran 'SY' => 95, // Syrien 'AF' => 90, // Afghanistan 'YE' => 90, // Jemen 'SO' => 90, // Somalia 'SD' => 85, // Sudan ]; // Hochrisikoländer (Sanktionen, Korruption, Geldwäsche) $highRiskCountries = [ 'RU' => 85, // Russland 'BY' => 85, // Belarus 'VE' => 85, // Venezuela 'CU' => 80, // Kuba 'MM' => 80, // Myanmar 'ZW' => 80, // Simbabwe 'LY' => 80, // Libyen 'IQ' => 75, // Irak ]; // Erhöhtes Risiko (Korruption, schwache Governance, Öl-Staaten) $mediumHighRiskCountries = [ 'AZ' => 70, // Aserbaidschan (Öl, Korruption) ← WICHTIG! 'KZ' => 70, // Kasachstan 'TM' => 70, // Turkmenistan 'UZ' => 65, // Usbekistan 'TJ' => 65, // Tadschikistan 'PK' => 65, // Pakistan 'BD' => 60, // Bangladesch 'NG' => 65, // Nigeria 'CD' => 70, // DR Kongo 'GQ' => 75, // Äquatorialguinea 'AO' => 65, // Angola 'GN' => 65, // Guinea 'HT' => 70, // Haiti 'NI' => 65, // Nicaragua 'ER' => 75, // Eritrea 'LA' => 60, // Laos 'KH' => 60, // Kambodscha 'CN' => 55, // China (Governance, Menschenrechte) ]; // Steueroasen (ERHÖHT für strengere Bewertung - Tax Havens sind hohes AML-Risiko!) $taxHavens = [ 'KY' => 75, // Cayman Islands (stark erhöht) 'BM' => 75, // Bermuda (stark erhöht) 'VG' => 80, // British Virgin Islands (stark erhöht - sehr häufig für Briefkastenfirmen) 'BS' => 75, // Bahamas (stark erhöht) 'PA' => 80, // Panama (stark erhöht - Panama Papers) 'LI' => 70, // Liechtenstein 'MC' => 65, // Monaco 'LU' => 60, // Luxembourg (trotz EU - Steueroase) ]; // Prüfe Kritisch if (isset($criticalRiskCountries[$countryCode])) { return $criticalRiskCountries[$countryCode]; } // Prüfe Hoch if (isset($highRiskCountries[$countryCode])) { return $highRiskCountries[$countryCode]; } // Prüfe Mittel-Hoch if (isset($mediumHighRiskCountries[$countryCode])) { return $mediumHighRiskCountries[$countryCode]; } // Prüfe Steueroasen if (isset($taxHavens[$countryCode])) { return $taxHavens[$countryCode]; } // EU & OECD Länder = Geringes Risiko $lowRiskCountries = [ 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'CH', 'SE', 'NO', 'DK', 'FI', 'GB', 'IE', 'PT', 'GR', 'PL', 'CZ', 'HU', 'RO', 'BG', 'SK', 'SI', 'HR', 'LT', 'LV', 'EE', 'CY', 'MT', 'LU', 'IS', 'US', 'CA', 'AU', 'NZ', 'JP', 'KR', 'SG', ]; if (in_array($countryCode, $lowRiskCountries)) { return 25; // Geringes Risiko } return 40; // Default: Mittleres Risiko } /** * Berechne PEP & Adverse Media Score (0-100) */ private function calculatePepAdverseScore(array $outputs, string $prefix = ''): int { $score = 0; $count = 0; $pepFields = [ $prefix.'corporate_pepexposure' => 75, $prefix.'corporate_pep' => 80, $prefix.'corporate_adverse' => 70, $prefix.'corporate_AMLexposure' => 85, ]; foreach ($pepFields as $field => $defaultScore) { if (! isset($outputs[$field])) { continue; } $data = json_decode($outputs[$field], true); // Prüfe zuerst auf flag=true if ($this->hasPositiveMatch($data)) { $score += $this->extractScoreFromData($data, $defaultScore); $count++; } elseif (is_array($data) && isset($data['answer'])) { // FALLBACK: Wenn flag=false, aber Answer enthält PEP-Keywords $answer = strtolower($data['answer']); // PEP-Keywords (state-owned, government, politically exposed, etc.) $pepKeywords = [ 'politically exposed person', 'state-owned', 'state owned', 'government official', 'government-owned', 'public official', 'may be considered pep', 'pep proximity', 'close associate', ]; // Adverse Media Keywords $adverseKeywords = [ 'bribery', 'corruption', 'scandal', 'allegation', 'investigation', 'money laundering', 'fraud', 'sanctions', ]; // Prüfe auf Keywords foreach ($pepKeywords as $keyword) { if (stripos($answer, $keyword) !== false) { // PEP-Indikator gefunden, aber mit reduziertem Score (da flag=false) $score += $defaultScore * 0.7; // 70% des Default Scores $count++; break; // Nur einmal pro Feld zählen } } // Wenn noch nicht gezählt, prüfe auf Adverse Keywords if ($count === 0 || ! isset($scored)) { foreach ($adverseKeywords as $keyword) { if (stripos($answer, $keyword) !== false) { $score += $defaultScore * 0.6; // 60% bei Adverse Keywords $count++; break; } } } } } return $count > 0 ? (int) round($score / $count) : 0; } /** * Berechne Corruption Score (0-100) */ private function calculateCorruptionScore(array $outputs, string $prefix = ''): int { $corruptionFields = [ $prefix.'corruption_sector', $prefix.'corruption_country', $prefix.'corruption_relationship', $prefix.'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) || (isset($data['flag']) && $data['flag'] === true); // WICHTIG: Flag-Check hinzugefügt } 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) || (isset($data['flag']) && $data['flag'] === true); // WICHTIG: Flag-Check hinzugefügt } 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 Transaction Plausibility Score (0-100) * Analysiert die Plausibilität der Transaktion basierend auf: * - Betragshöhe * - Länder-Kombination * - Zweck * - Währung */ public function calculateTransactionPlausibilityScore(array $transactionData): int { $riskFactors = []; $score = 0; // Start bei 0 = kein Risiko // 1. Betragshöhe analysieren $amount = (float) ($transactionData['tx_amount'] ?? 0); if ($amount > 10000000) { // > 10 Mio $riskFactors[] = 40; // Sehr hohe Beträge = erhöhtes Risiko } elseif ($amount > 1000000) { // > 1 Mio $riskFactors[] = 25; } elseif ($amount > 100000) { // > 100k $riskFactors[] = 15; } else { $riskFactors[] = 5; // Kleine Beträge = geringes Risiko } // 2. Länder-Kombination analysieren $outgoing = $transactionData['tx_country_outgoing'] ?? ''; $incoming = $transactionData['tx_country_incoming'] ?? ''; // Kritische Hochrisikoländer (Sanktionen, Terror, schwere Korruption) $criticalRiskCountries = [ 'KP', // Nordkorea 'IR', // Iran 'SY', // Syrien 'AF', // Afghanistan (Taliban) 'YE', // Jemen 'SO', // Somalia 'SD', // Sudan ]; // Hochrisikoländer (Sanktionen, Korruption, Geldwäsche) $highRiskCountries = [ 'RU', // Russland 'BY', // Belarus 'VE', // Venezuela 'CU', // Kuba 'MM', // Myanmar 'ZW', // Simbabwe 'LY', // Libyen 'IQ', // Irak ]; // Erhöhtes Risiko (Korruption, schwache Governance, Öl-Staaten) $mediumHighRiskCountries = [ 'AZ', // Aserbaidschan (Öl, Korruption) 'KZ', // Kasachstan (Öl, Korruption) 'TM', // Turkmenistan (Öl, Diktatur) 'UZ', // Usbekistan 'TJ', // Tadschikistan 'PK', // Pakistan 'BD', // Bangladesch 'NG', // Nigeria (Korruption, Öl) 'CD', // DR Kongo 'GQ', // Äquatorialguinea (Öl, Diktatur) 'AO', // Angola (Öl, Korruption) 'GN', // Guinea 'HT', // Haiti 'NI', // Nicaragua 'ER', // Eritrea 'LA', // Laos 'KH', // Kambodscha ]; // Steueroasen & Offshore-Zentren $taxHavens = [ 'KY', // Cayman Islands 'BM', // Bermuda 'VG', // British Virgin Islands 'BS', // Bahamas 'PA', // Panama 'LI', // Liechtenstein 'MC', // Monaco 'AN', // Niederländische Antillen 'AG', // Antigua und Barbuda 'BZ', // Belize 'SC', // Seychellen 'MU', // Mauritius 'WS', // Samoa 'CK', // Cook Islands 'IM', // Isle of Man 'JE', // Jersey 'GG', // Guernsey 'MT', // Malta (teilweise) 'CY', // Zypern (teilweise) ]; // China separat bewerten (wegen Größe und wirtschaftlicher Bedeutung) $chinaRelated = ['CN', 'HK', 'MO']; // China, Hong Kong, Macau // Risiko-Bewertung nach Priorität if (in_array($outgoing, $criticalRiskCountries) || in_array($incoming, $criticalRiskCountries)) { $riskFactors[] = 70; // Kritisches Risiko } elseif (in_array($outgoing, $highRiskCountries) || in_array($incoming, $highRiskCountries)) { $riskFactors[] = 50; // Hohes Risiko } elseif (in_array($outgoing, $taxHavens) || in_array($incoming, $taxHavens)) { $riskFactors[] = 45; // Steueroase } elseif (in_array($outgoing, $mediumHighRiskCountries) || in_array($incoming, $mediumHighRiskCountries)) { $riskFactors[] = 35; // Erhöhtes Risiko (Korruption, schwache Governance) } elseif (in_array($outgoing, $chinaRelated) || in_array($incoming, $chinaRelated)) { $riskFactors[] = 30; // China: Moderates Risiko (Kapitalkontrolle, Transparenz) } else { $riskFactors[] = 10; // Normale Länder (EU, USA, entwickelte Demokratien) } // 3. Zweck analysieren (Verdächtige Keywords) $purpose = strtolower($transactionData['tx_purpose'] ?? ''); $suspiciousKeywords = ['cash', 'loan', 'investment', 'consulting', 'service fee', 'commission']; $suspiciousCount = 0; foreach ($suspiciousKeywords as $keyword) { if (str_contains($purpose, $keyword)) { $suspiciousCount++; } } if ($suspiciousCount >= 2) { $riskFactors[] = 30; // Mehrere verdächtige Keywords } elseif ($suspiciousCount === 1) { $riskFactors[] = 15; } else { $riskFactors[] = 5; } // 4. Währungs-Plausibilität $currency = $transactionData['tx_currency'] ?? 'EUR'; if (! in_array($currency, ['EUR', 'USD', 'GBP', 'CHF', 'JPY'])) { $riskFactors[] = 20; // Unübliche Währung } else { $riskFactors[] = 5; } // Berechne Durchschnitt aller Risikofaktoren $score = (int) round(array_sum($riskFactors) / count($riskFactors)); return min(100, max(0, $score)); } /** * Berechne kombinierten Risk Score mit Entity, Counterparty und Transaction Plausibility * KATEGORISCHER ANSATZ: Höheres Risiko schlägt niedrigeres * - Wenn IRGENDEIN Faktor CRITICAL → Gesamt CRITICAL * - Wenn IRGENDEIN Faktor HIGH → Gesamt HIGH * - Sonst → LOW * * @return array ['entity_risk_level', 'counterparty_risk_level', 'plausibility_score', 'plausibility_level', 'combined_score', 'combined_risk_level', 'entity_risk_breakdown', 'counterparty_risk_breakdown', 'transaction_plausibility_breakdown', 'combined_risk_breakdown'] */ public function calculateCombinedRisk(Collection $outputs, array $transactionData): array { // Berechne Entity Risk mit Breakdown $entityBreakdown = $this->calculateEntityKycRiskBreakdown($outputs); $entityRiskLevel = $entityBreakdown['risk_level']; // Berechne Counterparty Risk mit Breakdown $counterpartyBreakdown = $this->calculateCounterpartyKycRiskBreakdown($outputs); $counterpartyRiskLevel = $counterpartyBreakdown['risk_level']; // Berechne Transaction Plausibility Score und konvertiere zu Level $plausibilityScore = $this->calculateTransactionPlausibilityScore($transactionData); $plausibilityLevel = $this->determineRiskLevel($plausibilityScore); // KATEGORISCHER ANSATZ: Critical schlägt High schlägt Low $combinedRiskLevel = $this->determineCategoricalRiskLevel([ $entityRiskLevel, $counterpartyRiskLevel, $plausibilityLevel, ]); // Konvertiere finales Risk Level zu Score für die Datenbank $combinedScore = $this->riskLevelToNumericScore($combinedRiskLevel); // Transaction Plausibility Breakdown $transactionPlausibilityBreakdown = [ 'score' => $plausibilityScore, 'risk_level' => $plausibilityLevel, 'transaction_data' => $transactionData, ]; // Combined Risk Breakdown $combinedRiskBreakdown = [ 'combined_risk_level' => $combinedRiskLevel, 'combined_score' => $combinedScore, 'method' => 'categorical', 'contributing_factors' => [ 'entity_risk_level' => $entityRiskLevel, 'counterparty_risk_level' => $counterpartyRiskLevel, 'plausibility_level' => $plausibilityLevel, ], ]; return [ 'entity_risk_level' => $entityRiskLevel, 'counterparty_risk_level' => $counterpartyRiskLevel, 'plausibility_score' => $plausibilityScore, 'plausibility_level' => $plausibilityLevel, 'combined_score' => $combinedScore, 'combined_risk_level' => $combinedRiskLevel, // Neue Breakdown-Daten 'entity_risk_breakdown' => $entityBreakdown, 'counterparty_risk_breakdown' => $counterpartyBreakdown, 'transaction_plausibility_breakdown' => $transactionPlausibilityBreakdown, 'combined_risk_breakdown' => $combinedRiskBreakdown, ]; } /** * Bestimme kategorisches Risk Level: Höchstes Risiko gewinnt * CRITICAL > HIGH > LOW * * @param array $riskLevels Array von Risk Levels ['low', 'high', 'critical'] * @return string 'low', 'high', or 'critical' */ private function determineCategoricalRiskLevel(array $riskLevels): string { // Wenn IRGENDEIN Faktor CRITICAL → Gesamt CRITICAL if (in_array('critical', $riskLevels)) { return 'critical'; } // Wenn IRGENDEIN Faktor HIGH → Gesamt HIGH if (in_array('high', $riskLevels)) { return 'high'; } // Sonst → LOW return 'low'; } /** * Konvertiere Risk Level zu numerischem Score (0-100) */ private function riskLevelToNumericScore(string $riskLevel): int { return match ($riskLevel) { 'low' => 20, 'high' => 60, 'critical' => 90, default => 50, }; } /** * 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'; } /** * NEU: Berechne Transaction Analysis Score (0-100) * Analysiert: Duplikate, Structuring/Smurfing, Fake Purpose */ private function calculateTransactionAnalysisScore(array $outputs): int { $score = 0; $flagCount = 0; // 1. Duplikats-Erkennung if (isset($outputs['tranx_duplicate'])) { $data = json_decode($outputs['tranx_duplicate'], true); if ($this->hasPositiveMatch($data)) { $score += 85; // Duplikate = sehr hohes Risiko $flagCount++; } } // 2. Structuring/Smurfing Detection (Geldwäsche-Indikator) if (isset($outputs['tranx_structuring'])) { $data = json_decode($outputs['tranx_structuring'], true); if ($this->hasPositiveMatch($data)) { $score += 95; // Structuring = kritisches Risiko $flagCount++; } } // 3. Fake Purpose Check if (isset($outputs['tranx_fakepurposecheck'])) { $data = json_decode($outputs['tranx_fakepurposecheck'], true); if ($this->hasPositiveMatch($data)) { $score += 80; // Fake Purpose = hohes Risiko $flagCount++; } } // 4. Round Amount (Rundbeträge können auf Geldwäsche hindeuten) if (isset($outputs['tranx_roundamount'])) { $data = json_decode($outputs['tranx_roundamount'], true); if ($this->hasPositiveMatch($data)) { $score += 40; // Rundbeträge = mittleres Risiko $flagCount++; } } // 5. Velocity Check (zu schnelle/viele Transaktionen) if (isset($outputs['tranx_velocitycheck'])) { $data = json_decode($outputs['tranx_velocitycheck'], true); if ($this->hasPositiveMatch($data)) { $score += 70; // Velocity = hohes Risiko $flagCount++; } } // 6. Unusual Pattern if (isset($outputs['tranx_unusualpattern'])) { $data = json_decode($outputs['tranx_unusualpattern'], true); if ($this->hasPositiveMatch($data)) { $score += 60; // Ungewöhnliche Muster = erhöhtes Risiko $flagCount++; } } if ($flagCount === 0) { return 0; // Keine verdächtigen Muster gefunden } // Durchschnitt berechnen return (int) round($score / $flagCount); } /** * NEU: Berechne Financial Health Score (0-100) * Analysiert: Insolvenz, Liquidation, Rating, Solvency */ private function calculateFinancialHealthScore(array $outputs, string $prefix = ''): int { $score = 0; $flagCount = 0; // 1. Insolvenz (kritisch) if (isset($outputs[$prefix.'corporate_insolvency'])) { $data = json_decode($outputs[$prefix.'corporate_insolvency'], true); if ($this->hasPositiveMatch($data)) { $score += 95; // Insolvenz = kritisches Risiko $flagCount++; } } // 2. Liquidation (kritisch) if (isset($outputs[$prefix.'corporate_liquidation'])) { $data = json_decode($outputs[$prefix.'corporate_liquidation'], true); if ($this->hasPositiveMatch($data)) { $score += 95; // Liquidation = kritisches Risiko $flagCount++; } } // 3. Rating (falls vorhanden) if (isset($outputs[$prefix.'corporate_rating'])) { $data = json_decode($outputs[$prefix.'corporate_rating'], true); if (is_array($data) && isset($data['rating'])) { $rating = strtoupper($data['rating']); // Rating-basierte Scores if (in_array($rating, ['D', 'C', 'CC', 'CCC'])) { $score += 90; // Sehr schlechtes Rating $flagCount++; } elseif (in_array($rating, ['B', 'BB', 'BBB'])) { $score += 60; // Mittleres Rating $flagCount++; } elseif (in_array($rating, ['A', 'AA', 'AAA'])) { $score += 10; // Gutes Rating (geringes Risiko) $flagCount++; } } } // 4. Solvency (Zahlungsfähigkeit) if (isset($outputs[$prefix.'corporate_solvency'])) { $data = json_decode($outputs[$prefix.'corporate_solvency'], true); if (is_array($data) && isset($data['solvent']) && $data['solvent'] === false) { $score += 85; // Nicht zahlungsfähig = hohes Risiko $flagCount++; } } // 5. Audit Findings (Prüfungsfeststellungen) if (isset($outputs[$prefix.'corporate_auditfindings'])) { $data = json_decode($outputs[$prefix.'corporate_auditfindings'], true); if (is_array($data) && isset($data['critical_findings']) && $data['critical_findings'] === true) { $score += 70; // Kritische Prüfungsfeststellungen $flagCount++; } } if ($flagCount === 0) { return 0; // Keine finanziellen Probleme gefunden } return (int) round($score / $flagCount); } /** * NEU: Berechne Legal Issues Score (0-100) * Analysiert: Gerichtsverfahren, Steueroasen, Exportverstöße, Warnungen */ private function calculateLegalIssuesScore(array $outputs, string $prefix = ''): int { $score = 0; $flagCount = 0; // 1. Court Cases (Gerichtsverfahren) if (isset($outputs[$prefix.'corporate_courtcases'])) { $data = json_decode($outputs[$prefix.'corporate_courtcases'], true); if (is_array($data) && isset($data['active_cases']) && $data['active_cases'] > 0) { $caseCount = (int) $data['active_cases']; if ($caseCount >= 5) { $score += 80; // Viele Verfahren = hohes Risiko } elseif ($caseCount >= 2) { $score += 60; // Einige Verfahren = mittleres Risiko } else { $score += 40; // Ein Verfahren = erhöhtes Risiko } $flagCount++; } } // 2. Tax Haven (Steueroasen-Nutzung) if (isset($outputs[$prefix.'corporate_haven'])) { $data = json_decode($outputs[$prefix.'corporate_haven'], true); if ($this->hasPositiveMatch($data)) { $score += 75; // Steueroasen = hohes Risiko $flagCount++; } } // 3. Export Control Violations (Exportverstöße) if (isset($outputs[$prefix.'corporate_exportcontrol'])) { $data = json_decode($outputs[$prefix.'corporate_exportcontrol'], true); if ($this->hasPositiveMatch($data)) { $score += 90; // Exportverstöße = kritisches Risiko $flagCount++; } } // 4. Warnings (Behördliche Warnungen) if (isset($outputs[$prefix.'corporate_warnings'])) { $data = json_decode($outputs[$prefix.'corporate_warnings'], true); if ($this->hasPositiveMatch($data)) { $score += 70; // Warnungen = hohes Risiko $flagCount++; } } // 5. License Issues (Lizenzprobleme) if (isset($outputs[$prefix.'corporate_license'])) { $data = json_decode($outputs[$prefix.'corporate_license'], true); if (is_array($data) && isset($data['license_valid']) && $data['license_valid'] === false) { $score += 65; // Ungültige/fehlende Lizenz = erhöhtes Risiko $flagCount++; } } if ($flagCount === 0) { return 0; // Keine rechtlichen Probleme gefunden } return (int) round($score / $flagCount); } }