removed KycRiskCalculator, added 3 new field for the database table data_pool_backend, added simple risk level and mapping funcion for these new risk factor
This commit is contained in:
1 parent
976af0f2e6
commit
85e4fce229
10 files changed
+1721
-101
No files matched your search
@@ -138,6 +138,9 @@ class SyncBackendDataPool implements ShouldQueue
|
|||||||
'status' => $record->status,
|
'status' => $record->status,
|
||||||
'created_at' => $record->created_at,
|
'created_at' => $record->created_at,
|
||||||
'last_modified_at' => $record->last_modified_at,
|
'last_modified_at' => $record->last_modified_at,
|
||||||
|
'risk_score' => $record->risk_score,
|
||||||
|
'risk_level' => $record->risk_level,
|
||||||
|
'risk_details_json' => $record->risk_details_json,
|
||||||
'prompt_id' => $record->prompt_id,
|
'prompt_id' => $record->prompt_id,
|
||||||
'output_key' => $record->output_key,
|
'output_key' => $record->output_key,
|
||||||
'content' => $record->content,
|
'content' => $record->content,
|
||||||
@@ -191,6 +194,9 @@ class SyncBackendDataPool implements ShouldQueue
|
|||||||
't.status',
|
't.status',
|
||||||
't.created_at',
|
't.created_at',
|
||||||
't.last_modified_at',
|
't.last_modified_at',
|
||||||
|
't.risk_score',
|
||||||
|
't.risk_level',
|
||||||
|
't.risk_details_json',
|
||||||
'tout.prompt_id',
|
'tout.prompt_id',
|
||||||
'tout.output_key',
|
'tout.output_key',
|
||||||
'tout.content',
|
'tout.content',
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace App\Jobs;
|
|||||||
|
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Transaction;
|
use App\Models\Transaction;
|
||||||
use App\Services\KycRiskCalculator;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
@@ -32,13 +31,12 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public ?int $batchSize = 100,
|
public ?int $batchSize = 100,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*/
|
*/
|
||||||
public function handle(KycRiskCalculator $kycCalculator): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$startTime = now();
|
$startTime = now();
|
||||||
|
|
||||||
@@ -47,8 +45,8 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DB::transaction(function () use ($kycCalculator) {
|
DB::transaction(function () {
|
||||||
$this->transformData($kycCalculator);
|
$this->transformData();
|
||||||
});
|
});
|
||||||
|
|
||||||
$duration = now()->diffInSeconds($startTime);
|
$duration = now()->diffInSeconds($startTime);
|
||||||
@@ -69,7 +67,7 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
/**
|
/**
|
||||||
* Transform data from backend_data_pool to companies and transactions
|
* Transform data from backend_data_pool to companies and transactions
|
||||||
*/
|
*/
|
||||||
private function transformData(KycRiskCalculator $kycCalculator): void
|
private function transformData(): void
|
||||||
{
|
{
|
||||||
// Get all unique transaction IDs from data pool
|
// Get all unique transaction IDs from data pool
|
||||||
$transactionIds = DB::table('backend_data_pool')
|
$transactionIds = DB::table('backend_data_pool')
|
||||||
@@ -97,7 +95,7 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
$firstOutput = $outputs->first();
|
$firstOutput = $outputs->first();
|
||||||
|
|
||||||
// 1. Create or update Company
|
// 1. Create or update Company
|
||||||
$company = $this->createOrUpdateCompany($firstOutput, $outputs, $kycCalculator);
|
$company = $this->createOrUpdateCompany($firstOutput, $outputs);
|
||||||
if ($company->wasRecentlyCreated) {
|
if ($company->wasRecentlyCreated) {
|
||||||
$processedCompanies++;
|
$processedCompanies++;
|
||||||
}
|
}
|
||||||
@@ -118,21 +116,23 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
/**
|
/**
|
||||||
* Create or update a Company from data pool outputs
|
* Create or update a Company from data pool outputs
|
||||||
*/
|
*/
|
||||||
private function createOrUpdateCompany($firstOutput, $outputs, KycRiskCalculator $kycCalculator): Company
|
private function createOrUpdateCompany($firstOutput, $outputs): Company
|
||||||
{
|
{
|
||||||
$corporateEntity = $firstOutput->corporate_entity;
|
$corporateEntity = $firstOutput->corporate_entity;
|
||||||
|
|
||||||
// Calculate KYC Risk Level
|
// Use backend risk_score and map to risk_level
|
||||||
$kycRiskLevel = $kycCalculator->calculateKycRiskLevel(collect([$outputs])->first());
|
$backendRiskScore = $firstOutput->risk_score ?? 50;
|
||||||
|
$kycRiskLevel = $this->mapBackendRiskScoreToLevel($backendRiskScore);
|
||||||
|
|
||||||
// Extract company data from outputs
|
// Extract company data from outputs
|
||||||
$companyData = [
|
$companyData = [
|
||||||
'country' => $this->extractCountry($firstOutput, $outputs),
|
'country' => $this->extractCountry($firstOutput, $outputs),
|
||||||
'kyc_risk_level' => $kycRiskLevel,
|
'kyc_risk_level' => $kycRiskLevel,
|
||||||
'sector' => $this->extractField($outputs, 'corporate_sector', 255),
|
'sector' => $this->extractLeanField($outputs, 'entity_corporate_sector', 255),
|
||||||
'headquarters' => $this->extractField($outputs, 'corporate_HQ', 255),
|
'headquarters' => $this->extractLeanField($outputs, 'entity_corporate_HQ', 255),
|
||||||
'summary' => $this->extractField($outputs, 'corporate_summary', 5000), // text field, longer OK
|
'summary' => $this->extractField($outputs, 'entity_corporate_summary', 5000), // text field, longer OK
|
||||||
'legal_name' => $this->extractField($outputs, 'corporate_name', 255),
|
'legal_name' => $this->extractLegalName($outputs, $corporateEntity),
|
||||||
|
'ticker' => null, // Ticker is not available in backend_data_pool
|
||||||
];
|
];
|
||||||
|
|
||||||
return Company::updateOrCreate(
|
return Company::updateOrCreate(
|
||||||
@@ -146,14 +146,27 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
private function createOrUpdateTransaction(Company $company, $firstOutput, $outputs): Transaction
|
private function createOrUpdateTransaction(Company $company, $firstOutput, $outputs): Transaction
|
||||||
{
|
{
|
||||||
$reference = 'MIGRATED-' . $firstOutput->transaction_id;
|
// Generate better reference number
|
||||||
|
$reference = $this->generateTransactionReference($firstOutput);
|
||||||
|
|
||||||
// Map KYC Risk Level to Transaction Status
|
// Use risk_score from backend (leading field)
|
||||||
$status = match ($company->kyc_risk_level) {
|
$backendRiskScore = $firstOutput->risk_score ?? 50; // Default to 50 (high risk) if not set
|
||||||
|
|
||||||
|
// Map backend risk_score to risk_level
|
||||||
|
// -100 to 20 = Critical
|
||||||
|
// 20 to 80 = High
|
||||||
|
// 80 to 100 = Low
|
||||||
|
$riskLevel = $this->mapBackendRiskScoreToLevel($backendRiskScore);
|
||||||
|
|
||||||
|
// Convert to 0-255 scale for frontend transactions table
|
||||||
|
$riskScore = $this->mapBackendRiskScoreTo255Scale($backendRiskScore);
|
||||||
|
|
||||||
|
// Map risk level to Transaction Status
|
||||||
|
$status = match ($riskLevel) {
|
||||||
'critical' => Transaction::STATUS_TRUE_POSITIVE,
|
'critical' => Transaction::STATUS_TRUE_POSITIVE,
|
||||||
'high' => Transaction::STATUS_FALSE_POSITIVE,
|
'high' => Transaction::STATUS_FALSE_POSITIVE,
|
||||||
'low' => Transaction::STATUS_CLEARED,
|
'low' => Transaction::STATUS_CLEARED,
|
||||||
default => Transaction::STATUS_FALSE_POSITIVE, // Default to high risk
|
default => Transaction::STATUS_FALSE_POSITIVE,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Core transaction data
|
// Core transaction data
|
||||||
@@ -162,13 +175,19 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
'amount' => $firstOutput->tx_amount,
|
'amount' => $firstOutput->tx_amount,
|
||||||
'currency' => $firstOutput->tx_currency ?? 'EUR',
|
'currency' => $firstOutput->tx_currency ?? 'EUR',
|
||||||
'counterparty' => $firstOutput->corporate_counterparty,
|
'counterparty' => $firstOutput->corporate_counterparty,
|
||||||
'counterparty_country' => $this->extractCountryCode($firstOutput->tx_country_incoming),
|
'counterparty_country' => $this->extractCountryCode($firstOutput->tx_country_incoming, $firstOutput->corporate_counterparty),
|
||||||
'channel' => $firstOutput->source_file,
|
'counterparty_kyc_risk_level' => $riskLevel,
|
||||||
|
'channel' => $this->determineTransactionType($firstOutput),
|
||||||
'executed_at' => $this->parseDate($firstOutput->tx_date),
|
'executed_at' => $this->parseDate($firstOutput->tx_date),
|
||||||
'risk_score' => $this->extractRiskScore($outputs),
|
'risk_score' => $riskScore,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'requires_review' => true,
|
'requires_review' => $this->shouldRequireReview($riskLevel),
|
||||||
'flagged_reason' => $firstOutput->tx_purpose,
|
'flagged_reason' => $firstOutput->tx_purpose,
|
||||||
|
// Store backend risk data
|
||||||
|
'entity_risk_breakdown' => $firstOutput->risk_details_json,
|
||||||
|
'counterparty_risk_breakdown' => null,
|
||||||
|
'transaction_plausibility_breakdown' => null,
|
||||||
|
'combined_risk_breakdown' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Map all output_keys to their respective columns
|
// Map all output_keys to their respective columns
|
||||||
@@ -218,15 +237,29 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract country code (convert to ISO 2-letter if needed)
|
* Extract country code (convert to ISO 2-letter if needed)
|
||||||
|
* Also checks counterparty name for country indicators
|
||||||
*/
|
*/
|
||||||
private function extractCountryCode(?string $country): ?string
|
private function extractCountryCode(?string $country, ?string $counterpartyName = null): ?string
|
||||||
{
|
{
|
||||||
|
// First, try to extract from counterparty name (highest priority for tax havens)
|
||||||
|
if (! empty($counterpartyName)) {
|
||||||
|
$extractedFromName = $this->extractCountryFromText($counterpartyName);
|
||||||
|
if ($extractedFromName) {
|
||||||
|
return $extractedFromName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($country)) {
|
if (empty($country)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already 2 letters, return uppercase
|
// If already 2 letters, check if it's a suspicious/wrong code
|
||||||
if (strlen($country) === 2) {
|
if (strlen($country) === 2) {
|
||||||
|
$suspiciousCodes = ['IO']; // British Indian Ocean Territory often wrong for Virgin Islands
|
||||||
|
if (in_array(strtoupper($country), $suspiciousCodes)) {
|
||||||
|
return null; // Reject suspicious codes
|
||||||
|
}
|
||||||
|
|
||||||
return strtoupper($country);
|
return strtoupper($country);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,26 +290,77 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract risk score from tranx_score output
|
* Extract country code from text (e.g., company name)
|
||||||
*/
|
*/
|
||||||
private function extractRiskScore($outputs): int
|
private function extractCountryFromText(string $text): ?string
|
||||||
{
|
{
|
||||||
$scoreOutput = $outputs->firstWhere('output_key', 'tranx_score');
|
$countryPatterns = [
|
||||||
|
'/\b(?:British )?Virgin Islands\b/i' => 'VG',
|
||||||
|
'/\bCayman Islands\b/i' => 'KY',
|
||||||
|
'/\bBermuda\b/i' => 'BM',
|
||||||
|
'/\bBahamas\b/i' => 'BS',
|
||||||
|
'/\bPanama\b/i' => 'PA',
|
||||||
|
'/\bLiechtenstein\b/i' => 'LI',
|
||||||
|
'/\bMonaco\b/i' => 'MC',
|
||||||
|
'/\bLuxembourg\b/i' => 'LU',
|
||||||
|
'/\bSwitzerland\b/i' => 'CH',
|
||||||
|
'/\bAzerbaijan\b/i' => 'AZ',
|
||||||
|
'/\bRussia\b/i' => 'RU',
|
||||||
|
'/\bChina\b/i' => 'CN',
|
||||||
|
'/\bGermany\b/i' => 'DE',
|
||||||
|
];
|
||||||
|
|
||||||
if (! $scoreOutput) {
|
foreach ($countryPatterns as $pattern => $code) {
|
||||||
return 128; // Default: Medium risk (255/2)
|
if (preg_match($pattern, $text)) {
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$scoreData = json_decode($scoreOutput->content, true);
|
return null;
|
||||||
|
|
||||||
if (is_array($scoreData) && isset($scoreData['score'])) {
|
|
||||||
// Assume score is 0-100, convert to 0-255
|
|
||||||
$score = (int) $scoreData['score'];
|
|
||||||
|
|
||||||
return (int) round(($score / 100) * 255);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 128;
|
/**
|
||||||
|
* Map backend risk_score (-100 to 100) to risk_level
|
||||||
|
* -100 to 20 = Critical
|
||||||
|
* 20 to 80 = High
|
||||||
|
* 80 to 100 = Low
|
||||||
|
*/
|
||||||
|
private function mapBackendRiskScoreToLevel(int $backendRiskScore): string
|
||||||
|
{
|
||||||
|
if ($backendRiskScore <= 20) {
|
||||||
|
return 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($backendRiskScore <= 80) {
|
||||||
|
return 'high';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map backend risk_score (-100 to 100) to 0-255 scale
|
||||||
|
*/
|
||||||
|
private function mapBackendRiskScoreTo255Scale(int $backendRiskScore): int
|
||||||
|
{
|
||||||
|
// Normalize -100 to 100 → 0 to 255
|
||||||
|
// Formula: ((score + 100) / 200) * 255
|
||||||
|
$normalized = (($backendRiskScore + 100) / 200) * 255;
|
||||||
|
|
||||||
|
return (int) round(max(0, min(255, $normalized)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if transaction requires review based on risk level
|
||||||
|
*/
|
||||||
|
private function shouldRequireReview(string $riskLevel): bool
|
||||||
|
{
|
||||||
|
return match ($riskLevel) {
|
||||||
|
'critical' => true,
|
||||||
|
'high' => true,
|
||||||
|
'low' => false,
|
||||||
|
default => true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -313,6 +397,71 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
return $this->truncateString((string) $data, $maxLength);
|
return $this->truncateString((string) $data, $maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a lean/concise field from outputs (extracts first sentence only)
|
||||||
|
*/
|
||||||
|
private function extractLeanField($outputs, string $outputKey, int $maxLength = 255): mixed
|
||||||
|
{
|
||||||
|
$fullText = $this->extractField($outputs, $outputKey, 5000);
|
||||||
|
|
||||||
|
if (! $fullText) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract first sentence or up to first period
|
||||||
|
$firstSentence = preg_split('/\.(?:\s|$)/', $fullText, 2)[0];
|
||||||
|
|
||||||
|
if ($firstSentence) {
|
||||||
|
$firstSentence = trim($firstSentence).'.';
|
||||||
|
|
||||||
|
return $this->truncateString($firstSentence, $maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->truncateString($fullText, $maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract legal name from corporate name field
|
||||||
|
*/
|
||||||
|
private function extractLegalName($outputs, string $fallbackName): ?string
|
||||||
|
{
|
||||||
|
$fullText = $this->extractField($outputs, 'entity_corporate_name', 5000);
|
||||||
|
|
||||||
|
if (! $fullText) {
|
||||||
|
return $fallbackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 1: "is exactly as shown in an official register: [NAME]"
|
||||||
|
if (preg_match('/is exactly as shown in an official register:\s*([A-Z][^.,]+(?:AG|SE|GmbH|Inc\.|Ltd\.|Corp\.|N\.V\.|S\.A\.|Aktiengesellschaft))/i', $fullText, $matches)) {
|
||||||
|
return trim($matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 2: "is [NAME]" - matches various patterns like "legal name is NAME", "company is NAME"
|
||||||
|
if (preg_match('/(?:legal name|name|company) (?:of .+ )?is\s+([A-Z][^.,]+(?:AG|SE|GmbH|Inc\.|Ltd\.|Corp\.|N\.V\.|S\.A\.|Aktiengesellschaft))/i', $fullText, $matches)) {
|
||||||
|
$name = trim($matches[1]);
|
||||||
|
// Remove trailing explanatory text
|
||||||
|
$name = preg_replace('/\s+(?:is exactly|exactly as shown|with former|former names).*$/i', '', $name);
|
||||||
|
|
||||||
|
return trim($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 3: "of [NAME]" - matches "legal name of Deutsche Bank AG"
|
||||||
|
if (preg_match('/legal name of\s+([A-Z][^.,]+(?:AG|SE|GmbH|Inc\.|Ltd\.|Corp\.|N\.V\.|S\.A\.|Aktiengesellschaft))/i', $fullText, $matches)) {
|
||||||
|
$name = trim($matches[1]);
|
||||||
|
$name = preg_replace('/\s+(?:is exactly|with former).*$/i', '', $name);
|
||||||
|
|
||||||
|
return trim($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 4: Extract company name with suffix at start of sentence
|
||||||
|
if (preg_match('/^(?:The current legal name (?:of|is) )?([A-Z][^.,]+(?:AG|SE|GmbH|Inc\.|Ltd\.|Corp\.|N\.V\.|S\.A\.|Aktiengesellschaft))/i', $fullText, $matches)) {
|
||||||
|
return trim($matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no pattern matches, use the fallback name
|
||||||
|
return $fallbackName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate string to max length
|
* Truncate string to max length
|
||||||
*/
|
*/
|
||||||
@@ -343,6 +492,98 @@ class TransformDataPoolToProduction implements ShouldQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a meaningful transaction reference
|
||||||
|
*/
|
||||||
|
private function generateTransactionReference($firstOutput): string
|
||||||
|
{
|
||||||
|
// Try to extract reference from raw_payload
|
||||||
|
if (! empty($firstOutput->raw_payload)) {
|
||||||
|
$payload = json_decode($firstOutput->raw_payload, true);
|
||||||
|
|
||||||
|
// Look for common reference fields
|
||||||
|
$referenceFields = [
|
||||||
|
'Transaction Reference',
|
||||||
|
'Reference',
|
||||||
|
'Transaction ID',
|
||||||
|
'Payment Reference',
|
||||||
|
'Transfer Reference',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($referenceFields as $field) {
|
||||||
|
if (isset($payload[$field]) && ! empty($payload[$field])) {
|
||||||
|
return (string) $payload[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a date-based reference if no reference found
|
||||||
|
$date = $this->parseDate($firstOutput->tx_date);
|
||||||
|
$dateStr = $date ? $date->format('Ymd') : date('Ymd');
|
||||||
|
|
||||||
|
return sprintf('TXN-%s-%05d', $dateStr, $firstOutput->transaction_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine transaction type from available data
|
||||||
|
*/
|
||||||
|
private function determineTransactionType($firstOutput): string
|
||||||
|
{
|
||||||
|
// Try to determine from raw_payload first
|
||||||
|
if (! empty($firstOutput->raw_payload)) {
|
||||||
|
$payload = json_decode($firstOutput->raw_payload, true);
|
||||||
|
|
||||||
|
// Check for explicit transaction type field
|
||||||
|
if (isset($payload['Transaction Type']) && ! empty($payload['Transaction Type'])) {
|
||||||
|
return $payload['Transaction Type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($payload['Payment Type']) && ! empty($payload['Payment Type'])) {
|
||||||
|
return $payload['Payment Type'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine based on amount and countries
|
||||||
|
$amount = (float) $firstOutput->tx_amount;
|
||||||
|
$isInternational = $firstOutput->tx_country_outgoing !== $firstOutput->tx_country_incoming;
|
||||||
|
|
||||||
|
// Large international transactions are likely SWIFT
|
||||||
|
if ($isInternational && $amount > 100000) {
|
||||||
|
return 'SWIFT Transfer';
|
||||||
|
}
|
||||||
|
|
||||||
|
// European transfers
|
||||||
|
if ($isInternational && $this->isEuropeanCountry($firstOutput->tx_country_incoming)) {
|
||||||
|
return 'SEPA Transfer';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domestic high-value
|
||||||
|
if (! $isInternational && $amount > 50000) {
|
||||||
|
return 'Wire Transfer';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default
|
||||||
|
return 'Bank Transfer';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if country is in Europe
|
||||||
|
*/
|
||||||
|
private function isEuropeanCountry(?string $countryCode): bool
|
||||||
|
{
|
||||||
|
if (empty($countryCode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$europeanCountries = [
|
||||||
|
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
|
||||||
|
'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
|
||||||
|
'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'CH', 'NO', 'IS',
|
||||||
|
];
|
||||||
|
|
||||||
|
return in_array(strtoupper($countryCode), $europeanCountries);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if column exists in transactions table
|
* Check if column exists in transactions table
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,49 +12,138 @@ class KycRiskCalculator
|
|||||||
* KYC Risk Level Schwellenwerte (3 Levels)
|
* KYC Risk Level Schwellenwerte (3 Levels)
|
||||||
*/
|
*/
|
||||||
private const RISK_THRESHOLDS = [
|
private const RISK_THRESHOLDS = [
|
||||||
'low' => 40, // 0-40 = Geringes Risiko
|
'low' => 20, // 0-20 = Geringes Risiko (nur saubere EU/OECD Transaktionen ohne Red Flags)
|
||||||
'high' => 70, // 41-70 = Hohes Risiko
|
'high' => 50, // 20-50 = Hohes Risiko (Tax Havens, High-Risk Countries, PEP, Corruption)
|
||||||
'critical' => 100, // 71-100 = Kritisches Risiko
|
'critical' => 100, // 50-100 = Kritisches Risiko (Sanctions, Multiple Red Flags, Structuring)
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gewichtung der verschiedenen Risk-Faktoren
|
* Gewichtung der verschiedenen Risk-Faktoren
|
||||||
*/
|
*/
|
||||||
private const RISK_WEIGHTS = [
|
private const RISK_WEIGHTS = [
|
||||||
'transaction_score' => 0.40, // 40% Gewichtung
|
'transaction_score' => 0.15, // 15% Gewichtung (reduziert für mehr Raum bei kritischen Faktoren)
|
||||||
'sanctions' => 0.25, // 25% Gewichtung
|
'transaction_analysis' => 0.15, // 15% Gewichtung (duplicate, structuring, fake purpose)
|
||||||
'country_risk' => 0.15, // 15% Gewichtung
|
'sanctions' => 0.25, // 25% Gewichtung (ERHÖHT - sehr kritisch!)
|
||||||
'pep_adverse' => 0.10, // 10% Gewichtung
|
'country_risk' => 0.15, // 15% Gewichtung (ERHÖHT - wichtiger Indikator)
|
||||||
'corruption' => 0.10, // 10% Gewichtung
|
'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
|
* Berechne KYC Risk Level basierend auf Transaction Outputs (Legacy - verwendet Entity)
|
||||||
*
|
*
|
||||||
* @param Collection $outputs Collection von backend_data_pool records für eine Transaction
|
* @param Collection $outputs Collection von backend_data_pool records für eine Transaction
|
||||||
* @return string 'low', 'high', or 'critical'
|
* @return string 'low', 'high', or 'critical'
|
||||||
*/
|
*/
|
||||||
public function calculateKycRiskLevel(Collection $outputs): string
|
public function calculateKycRiskLevel(Collection $outputs): string
|
||||||
{
|
{
|
||||||
// Wenn keine Outputs vorhanden: Default = high (vorsichtig)
|
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()) {
|
if ($outputs->isEmpty()) {
|
||||||
return 'high';
|
return [
|
||||||
|
'risk_level' => 'high',
|
||||||
|
'weighted_score' => 70.0,
|
||||||
|
'scores' => [],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konvertiere Collection zu Key-Value Array für einfacheren Zugriff
|
|
||||||
$outputsArray = $outputs->pluck('content', 'output_key')->toArray();
|
$outputsArray = $outputs->pluck('content', 'output_key')->toArray();
|
||||||
|
|
||||||
$scores = [
|
$scores = [
|
||||||
'transaction_score' => $this->calculateTransactionScore($outputsArray),
|
'transaction_score' => $this->calculateTransactionScore($outputsArray),
|
||||||
'sanctions' => $this->calculateSanctionsScore($outputsArray),
|
'transaction_analysis' => $this->calculateTransactionAnalysisScore($outputsArray),
|
||||||
'country_risk' => $this->calculateCountryRiskScore($outputsArray),
|
'sanctions' => $this->calculateSanctionsScore($outputsArray, 'entity_'),
|
||||||
'pep_adverse' => $this->calculatePepAdverseScore($outputsArray),
|
'country_risk' => $this->calculateCountryRiskScore($outputsArray, 'entity_'),
|
||||||
'corruption' => $this->calculateCorruptionScore($outputsArray),
|
'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);
|
$weightedScore = $this->calculateWeightedScore($scores);
|
||||||
|
$riskLevel = $this->determineRiskLevel($weightedScore);
|
||||||
|
|
||||||
return $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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,13 +178,13 @@ class KycRiskCalculator
|
|||||||
/**
|
/**
|
||||||
* Berechne Sanctions Score (0-100)
|
* Berechne Sanctions Score (0-100)
|
||||||
*/
|
*/
|
||||||
private function calculateSanctionsScore(array $outputs): int
|
private function calculateSanctionsScore(array $outputs, string $prefix = ''): int
|
||||||
{
|
{
|
||||||
$sanctionFlags = [
|
$sanctionFlags = [
|
||||||
'corporate_eusanctions',
|
$prefix.'corporate_eusanctions',
|
||||||
'corporate_ofacsanctions',
|
$prefix.'corporate_ofacsanctions',
|
||||||
'corporate_uksanctions',
|
$prefix.'corporate_uksanctions',
|
||||||
'sanctions_circumvention',
|
$prefix.'sanctions_circumvention',
|
||||||
];
|
];
|
||||||
|
|
||||||
$score = 0;
|
$score = 0;
|
||||||
@@ -136,42 +225,239 @@ class KycRiskCalculator
|
|||||||
/**
|
/**
|
||||||
* Berechne Country Risk Score (0-100)
|
* Berechne Country Risk Score (0-100)
|
||||||
*/
|
*/
|
||||||
private function calculateCountryRiskScore(array $outputs): int
|
private function calculateCountryRiskScore(array $outputs, string $prefix = ''): int
|
||||||
{
|
{
|
||||||
if (! isset($outputs['country_risk'])) {
|
$key = $prefix.'country_risk';
|
||||||
return 40; // Default: Geringes Risiko
|
|
||||||
}
|
|
||||||
|
|
||||||
$countryRiskData = json_decode($outputs['country_risk'], true);
|
// Wenn Country Risk Daten vorhanden sind, diese verwenden
|
||||||
|
if (isset($outputs[$key])) {
|
||||||
|
$countryRiskData = json_decode($outputs[$key], true);
|
||||||
|
|
||||||
if (is_array($countryRiskData) && isset($countryRiskData['score'])) {
|
// 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']));
|
return min(100, max(0, (int) $countryRiskData['score']));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($countryRiskData) && isset($countryRiskData['level'])) {
|
if (isset($countryRiskData['level'])) {
|
||||||
return $this->mapRiskLevelToScore($countryRiskData['level']);
|
return $this->mapRiskLevelToScore($countryRiskData['level']);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (is_bool($countryRiskData)) {
|
|
||||||
return $countryRiskData ? 80 : 20;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 40;
|
// 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)
|
* Berechne PEP & Adverse Media Score (0-100)
|
||||||
*/
|
*/
|
||||||
private function calculatePepAdverseScore(array $outputs): int
|
private function calculatePepAdverseScore(array $outputs, string $prefix = ''): int
|
||||||
{
|
{
|
||||||
$score = 0;
|
$score = 0;
|
||||||
$count = 0;
|
$count = 0;
|
||||||
|
|
||||||
$pepFields = [
|
$pepFields = [
|
||||||
'corporate_pepexposure' => 75,
|
$prefix.'corporate_pepexposure' => 75,
|
||||||
'corporate_pep' => 80,
|
$prefix.'corporate_pep' => 80,
|
||||||
'corporate_adverse' => 70,
|
$prefix.'corporate_adverse' => 70,
|
||||||
'corporate_AMLexposure' => 85,
|
$prefix.'corporate_AMLexposure' => 85,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($pepFields as $field => $defaultScore) {
|
foreach ($pepFields as $field => $defaultScore) {
|
||||||
@@ -181,9 +467,59 @@ class KycRiskCalculator
|
|||||||
|
|
||||||
$data = json_decode($outputs[$field], true);
|
$data = json_decode($outputs[$field], true);
|
||||||
|
|
||||||
|
// Prüfe zuerst auf flag=true
|
||||||
if ($this->hasPositiveMatch($data)) {
|
if ($this->hasPositiveMatch($data)) {
|
||||||
$score += $this->extractScoreFromData($data, $defaultScore);
|
$score += $this->extractScoreFromData($data, $defaultScore);
|
||||||
$count++;
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,13 +529,13 @@ class KycRiskCalculator
|
|||||||
/**
|
/**
|
||||||
* Berechne Corruption Score (0-100)
|
* Berechne Corruption Score (0-100)
|
||||||
*/
|
*/
|
||||||
private function calculateCorruptionScore(array $outputs): int
|
private function calculateCorruptionScore(array $outputs, string $prefix = ''): int
|
||||||
{
|
{
|
||||||
$corruptionFields = [
|
$corruptionFields = [
|
||||||
'corruption_sector',
|
$prefix.'corruption_sector',
|
||||||
'corruption_country',
|
$prefix.'corruption_country',
|
||||||
'corruption_relationship',
|
$prefix.'corruption_relationship',
|
||||||
'corporate_corruptionexposure',
|
$prefix.'corporate_corruptionexposure',
|
||||||
];
|
];
|
||||||
|
|
||||||
$totalScore = 0;
|
$totalScore = 0;
|
||||||
@@ -283,7 +619,8 @@ class KycRiskCalculator
|
|||||||
|
|
||||||
return (isset($data['matches']) && ! empty($data['matches']))
|
return (isset($data['matches']) && ! empty($data['matches']))
|
||||||
|| (isset($data['found']) && $data['found'] === true)
|
|| (isset($data['found']) && $data['found'] === true)
|
||||||
|| (isset($data['sanctioned']) && $data['sanctioned'] === true);
|
|| (isset($data['sanctioned']) && $data['sanctioned'] === true)
|
||||||
|
|| (isset($data['flag']) && $data['flag'] === true); // WICHTIG: Flag-Check hinzugefügt
|
||||||
}
|
}
|
||||||
|
|
||||||
private function hasPositiveMatch($data): bool
|
private function hasPositiveMatch($data): bool
|
||||||
@@ -292,7 +629,8 @@ class KycRiskCalculator
|
|||||||
return (isset($data['match']) && $data['match'] === true)
|
return (isset($data['match']) && $data['match'] === true)
|
||||||
|| (isset($data['found']) && $data['found'] === true)
|
|| (isset($data['found']) && $data['found'] === true)
|
||||||
|| (isset($data['matches']) && ! empty($data['matches']))
|
|| (isset($data['matches']) && ! empty($data['matches']))
|
||||||
|| (isset($data['exposure']) && $data['exposure'] === true);
|
|| (isset($data['exposure']) && $data['exposure'] === true)
|
||||||
|
|| (isset($data['flag']) && $data['flag'] === true); // WICHTIG: Flag-Check hinzugefügt
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_bool($data)) {
|
if (is_bool($data)) {
|
||||||
@@ -315,6 +653,256 @@ class KycRiskCalculator
|
|||||||
return $default;
|
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
|
* Berechne aggregierten Risk Level für eine Company basierend auf allen Transaktionen
|
||||||
*/
|
*/
|
||||||
@@ -363,4 +951,221 @@ class KycRiskCalculator
|
|||||||
// 5. Default: Hohes Risiko (vorsichtig)
|
// 5. Default: Hohes Risiko (vorsichtig)
|
||||||
return 'high';
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+299
@@ -0,0 +1,299 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
// Entity Corporate Information
|
||||||
|
$table->jsonb('entity_corporate_summary')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_history')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_purpose')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_sector')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_nace')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_products')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_markets')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_supply')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_name')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_form')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_forum')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_HQ')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_locations')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_holding')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_shareholders')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_board')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_supervisory')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_powerofattorney')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_taxID')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_LEI')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_UBO')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_employeecount')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_turnover')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_EBIT')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_netprofits')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_balancesheet')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_auditfindings')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_insolvency')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_liquidation')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_adhoc')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_pressrelease')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_votes')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_directordealings')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_brands')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_website')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_domain')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_IBAN')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_solvency')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_rating')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_ESG')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_license')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_warnings')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_eusanctions')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_ofacsanctions')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_uksanctions')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_pepexposure')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_adversemediascanning')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_corruptionexposure')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_AMLexposure')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_CTYexposure')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_adverse')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_pep')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_adverse2')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_exportcontrol')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_mediamatch')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_haven')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_insiders')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_courtcases')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_manda')->nullable();
|
||||||
|
$table->jsonb('entity_corporate_pepdetail')->nullable();
|
||||||
|
|
||||||
|
// Counterparty Corporate Information
|
||||||
|
$table->jsonb('counterparty_corporate_summary')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_history')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_purpose')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_sector')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_nace')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_products')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_markets')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_supply')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_name')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_form')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_forum')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_HQ')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_locations')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_holding')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_shareholders')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_board')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_supervisory')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_powerofattorney')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_taxID')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_LEI')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_UBO')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_employeecount')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_turnover')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_EBIT')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_netprofits')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_balancesheet')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_auditfindings')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_insolvency')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_liquidation')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_adhoc')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_pressrelease')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_votes')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_directordealings')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_brands')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_website')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_domain')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_IBAN')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_solvency')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_rating')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_ESG')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_license')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_warnings')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_eusanctions')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_ofacsanctions')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_uksanctions')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_pepexposure')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_adversemediascanning')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_corruptionexposure')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_AMLexposure')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_CTYexposure')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_adverse')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_pep')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_adverse2')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_exportcontrol')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_mediamatch')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_haven')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_insiders')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_courtcases')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_manda')->nullable();
|
||||||
|
$table->jsonb('counterparty_corporate_pepdetail')->nullable();
|
||||||
|
|
||||||
|
// Entity Risk Assessment
|
||||||
|
$table->jsonb('entity_sanctions_circumvention')->nullable();
|
||||||
|
$table->jsonb('entity_corruption_sector')->nullable();
|
||||||
|
$table->jsonb('entity_corruption_country')->nullable();
|
||||||
|
$table->jsonb('entity_corruption_relationship')->nullable();
|
||||||
|
$table->jsonb('entity_country_risk')->nullable();
|
||||||
|
|
||||||
|
// Counterparty Risk Assessment
|
||||||
|
$table->jsonb('counterparty_sanctions_circumvention')->nullable();
|
||||||
|
$table->jsonb('counterparty_corruption_sector')->nullable();
|
||||||
|
$table->jsonb('counterparty_corruption_country')->nullable();
|
||||||
|
$table->jsonb('counterparty_corruption_relationship')->nullable();
|
||||||
|
$table->jsonb('counterparty_country_risk')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
// Entity Corporate Information
|
||||||
|
'entity_corporate_summary',
|
||||||
|
'entity_corporate_history',
|
||||||
|
'entity_corporate_purpose',
|
||||||
|
'entity_corporate_sector',
|
||||||
|
'entity_corporate_nace',
|
||||||
|
'entity_corporate_products',
|
||||||
|
'entity_corporate_markets',
|
||||||
|
'entity_corporate_supply',
|
||||||
|
'entity_corporate_name',
|
||||||
|
'entity_corporate_form',
|
||||||
|
'entity_corporate_forum',
|
||||||
|
'entity_corporate_HQ',
|
||||||
|
'entity_corporate_locations',
|
||||||
|
'entity_corporate_holding',
|
||||||
|
'entity_corporate_shareholders',
|
||||||
|
'entity_corporate_board',
|
||||||
|
'entity_corporate_supervisory',
|
||||||
|
'entity_corporate_powerofattorney',
|
||||||
|
'entity_corporate_taxID',
|
||||||
|
'entity_corporate_LEI',
|
||||||
|
'entity_corporate_UBO',
|
||||||
|
'entity_corporate_employeecount',
|
||||||
|
'entity_corporate_turnover',
|
||||||
|
'entity_corporate_EBIT',
|
||||||
|
'entity_corporate_netprofits',
|
||||||
|
'entity_corporate_balancesheet',
|
||||||
|
'entity_corporate_auditfindings',
|
||||||
|
'entity_corporate_insolvency',
|
||||||
|
'entity_corporate_liquidation',
|
||||||
|
'entity_corporate_adhoc',
|
||||||
|
'entity_corporate_pressrelease',
|
||||||
|
'entity_corporate_votes',
|
||||||
|
'entity_corporate_directordealings',
|
||||||
|
'entity_corporate_brands',
|
||||||
|
'entity_corporate_website',
|
||||||
|
'entity_corporate_domain',
|
||||||
|
'entity_corporate_IBAN',
|
||||||
|
'entity_corporate_solvency',
|
||||||
|
'entity_corporate_rating',
|
||||||
|
'entity_corporate_ESG',
|
||||||
|
'entity_corporate_license',
|
||||||
|
'entity_corporate_warnings',
|
||||||
|
'entity_corporate_eusanctions',
|
||||||
|
'entity_corporate_ofacsanctions',
|
||||||
|
'entity_corporate_uksanctions',
|
||||||
|
'entity_corporate_pepexposure',
|
||||||
|
'entity_corporate_adversemediascanning',
|
||||||
|
'entity_corporate_corruptionexposure',
|
||||||
|
'entity_corporate_AMLexposure',
|
||||||
|
'entity_corporate_CTYexposure',
|
||||||
|
'entity_corporate_adverse',
|
||||||
|
'entity_corporate_pep',
|
||||||
|
'entity_corporate_adverse2',
|
||||||
|
'entity_corporate_exportcontrol',
|
||||||
|
'entity_corporate_mediamatch',
|
||||||
|
'entity_corporate_haven',
|
||||||
|
'entity_corporate_insiders',
|
||||||
|
'entity_corporate_courtcases',
|
||||||
|
'entity_corporate_manda',
|
||||||
|
'entity_corporate_pepdetail',
|
||||||
|
// Counterparty Corporate Information
|
||||||
|
'counterparty_corporate_summary',
|
||||||
|
'counterparty_corporate_history',
|
||||||
|
'counterparty_corporate_purpose',
|
||||||
|
'counterparty_corporate_sector',
|
||||||
|
'counterparty_corporate_nace',
|
||||||
|
'counterparty_corporate_products',
|
||||||
|
'counterparty_corporate_markets',
|
||||||
|
'counterparty_corporate_supply',
|
||||||
|
'counterparty_corporate_name',
|
||||||
|
'counterparty_corporate_form',
|
||||||
|
'counterparty_corporate_forum',
|
||||||
|
'counterparty_corporate_HQ',
|
||||||
|
'counterparty_corporate_locations',
|
||||||
|
'counterparty_corporate_holding',
|
||||||
|
'counterparty_corporate_shareholders',
|
||||||
|
'counterparty_corporate_board',
|
||||||
|
'counterparty_corporate_supervisory',
|
||||||
|
'counterparty_corporate_powerofattorney',
|
||||||
|
'counterparty_corporate_taxID',
|
||||||
|
'counterparty_corporate_LEI',
|
||||||
|
'counterparty_corporate_UBO',
|
||||||
|
'counterparty_corporate_employeecount',
|
||||||
|
'counterparty_corporate_turnover',
|
||||||
|
'counterparty_corporate_EBIT',
|
||||||
|
'counterparty_corporate_netprofits',
|
||||||
|
'counterparty_corporate_balancesheet',
|
||||||
|
'counterparty_corporate_auditfindings',
|
||||||
|
'counterparty_corporate_insolvency',
|
||||||
|
'counterparty_corporate_liquidation',
|
||||||
|
'counterparty_corporate_adhoc',
|
||||||
|
'counterparty_corporate_pressrelease',
|
||||||
|
'counterparty_corporate_votes',
|
||||||
|
'counterparty_corporate_directordealings',
|
||||||
|
'counterparty_corporate_brands',
|
||||||
|
'counterparty_corporate_website',
|
||||||
|
'counterparty_corporate_domain',
|
||||||
|
'counterparty_corporate_IBAN',
|
||||||
|
'counterparty_corporate_solvency',
|
||||||
|
'counterparty_corporate_rating',
|
||||||
|
'counterparty_corporate_ESG',
|
||||||
|
'counterparty_corporate_license',
|
||||||
|
'counterparty_corporate_warnings',
|
||||||
|
'counterparty_corporate_eusanctions',
|
||||||
|
'counterparty_corporate_ofacsanctions',
|
||||||
|
'counterparty_corporate_uksanctions',
|
||||||
|
'counterparty_corporate_pepexposure',
|
||||||
|
'counterparty_corporate_adversemediascanning',
|
||||||
|
'counterparty_corporate_corruptionexposure',
|
||||||
|
'counterparty_corporate_AMLexposure',
|
||||||
|
'counterparty_corporate_CTYexposure',
|
||||||
|
'counterparty_corporate_adverse',
|
||||||
|
'counterparty_corporate_pep',
|
||||||
|
'counterparty_corporate_adverse2',
|
||||||
|
'counterparty_corporate_exportcontrol',
|
||||||
|
'counterparty_corporate_mediamatch',
|
||||||
|
'counterparty_corporate_haven',
|
||||||
|
'counterparty_corporate_insiders',
|
||||||
|
'counterparty_corporate_courtcases',
|
||||||
|
'counterparty_corporate_manda',
|
||||||
|
'counterparty_corporate_pepdetail',
|
||||||
|
// Entity Risk Assessment
|
||||||
|
'entity_sanctions_circumvention',
|
||||||
|
'entity_corruption_sector',
|
||||||
|
'entity_corruption_country',
|
||||||
|
'entity_corruption_relationship',
|
||||||
|
'entity_country_risk',
|
||||||
|
// Counterparty Risk Assessment
|
||||||
|
'counterparty_sanctions_circumvention',
|
||||||
|
'counterparty_corruption_sector',
|
||||||
|
'counterparty_corruption_country',
|
||||||
|
'counterparty_corruption_relationship',
|
||||||
|
'counterparty_country_risk',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
$table->string('counterparty_kyc_risk_level', 32)->nullable()->after('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('counterparty_kyc_risk_level');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
// Entity risk breakdown (sender)
|
||||||
|
$table->jsonb('entity_risk_breakdown')->nullable()->after('counterparty_kyc_risk_level');
|
||||||
|
|
||||||
|
// Counterparty risk breakdown (receiver)
|
||||||
|
$table->jsonb('counterparty_risk_breakdown')->nullable()->after('entity_risk_breakdown');
|
||||||
|
|
||||||
|
// Transaction plausibility breakdown
|
||||||
|
$table->jsonb('transaction_plausibility_breakdown')->nullable()->after('counterparty_risk_breakdown');
|
||||||
|
|
||||||
|
// Combined risk breakdown
|
||||||
|
$table->jsonb('combined_risk_breakdown')->nullable()->after('transaction_plausibility_breakdown');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('transactions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'entity_risk_breakdown',
|
||||||
|
'counterparty_risk_breakdown',
|
||||||
|
'transaction_plausibility_breakdown',
|
||||||
|
'combined_risk_breakdown',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('backend_data_pool', function (Blueprint $table) {
|
||||||
|
// Add risk fields from backend.transactions
|
||||||
|
$table->integer('risk_score')->nullable()->after('last_modified_at');
|
||||||
|
$table->text('risk_level')->nullable()->after('risk_score');
|
||||||
|
$table->text('risk_details_json')->nullable()->after('risk_level');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('backend_data_pool', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['risk_score', 'risk_level', 'risk_details_json']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
"output_key"
|
||||||
|
"counterparty_corporate_summary"
|
||||||
|
"entity_corporate_summary"
|
||||||
|
"entity_corporate_history"
|
||||||
|
"counterparty_corporate_history"
|
||||||
|
"entity_corporate_purpose"
|
||||||
|
"counterparty_corporate_purpose"
|
||||||
|
"entity_corporate_sector"
|
||||||
|
"counterparty_corporate_sector"
|
||||||
|
"counterparty_corporate_nace"
|
||||||
|
"entity_corporate_nace"
|
||||||
|
"entity_corporate_products"
|
||||||
|
"counterparty_corporate_products"
|
||||||
|
"entity_corporate_markets"
|
||||||
|
"counterparty_corporate_markets"
|
||||||
|
"counterparty_corporate_supply"
|
||||||
|
"entity_corporate_supply"
|
||||||
|
"entity_corporate_name"
|
||||||
|
"counterparty_corporate_name"
|
||||||
|
"entity_corporate_form"
|
||||||
|
"counterparty_corporate_form"
|
||||||
|
"counterparty_corporate_forum"
|
||||||
|
"entity_corporate_forum"
|
||||||
|
"counterparty_corporate_HQ"
|
||||||
|
"entity_corporate_HQ"
|
||||||
|
"entity_corporate_locations"
|
||||||
|
"counterparty_corporate_locations"
|
||||||
|
"entity_corporate_holding"
|
||||||
|
"counterparty_corporate_holding"
|
||||||
|
"counterparty_corporate_shareholders"
|
||||||
|
"entity_corporate_shareholders"
|
||||||
|
"entity_corporate_board"
|
||||||
|
"counterparty_corporate_board"
|
||||||
|
"counterparty_corporate_supervisory"
|
||||||
|
"entity_corporate_supervisory"
|
||||||
|
"counterparty_corporate_powerofattorney"
|
||||||
|
"entity_corporate_powerofattorney"
|
||||||
|
"counterparty_corporate_taxID"
|
||||||
|
"entity_corporate_taxID"
|
||||||
|
"counterparty_corporate_LEI"
|
||||||
|
"entity_corporate_LEI"
|
||||||
|
"counterparty_corporate_UBO"
|
||||||
|
"entity_corporate_UBO"
|
||||||
|
"counterparty_corporate_employeecount"
|
||||||
|
"entity_corporate_employeecount"
|
||||||
|
"entity_corporate_turnover"
|
||||||
|
"counterparty_corporate_turnover"
|
||||||
|
"counterparty_corporate_EBIT"
|
||||||
|
"entity_corporate_EBIT"
|
||||||
|
"entity_corporate_netprofits"
|
||||||
|
"counterparty_corporate_netprofits"
|
||||||
|
"entity_corporate_balancesheet"
|
||||||
|
"counterparty_corporate_balancesheet"
|
||||||
|
"entity_corporate_auditfindings"
|
||||||
|
"counterparty_corporate_auditfindings"
|
||||||
|
"counterparty_corporate_insolvency"
|
||||||
|
"entity_corporate_insolvency"
|
||||||
|
"counterparty_corporate_liquidation"
|
||||||
|
"entity_corporate_liquidation"
|
||||||
|
"entity_corporate_adhoc"
|
||||||
|
"counterparty_corporate_adhoc"
|
||||||
|
"entity_corporate_pressrelease"
|
||||||
|
"counterparty_corporate_pressrelease"
|
||||||
|
"entity_corporate_votes"
|
||||||
|
"counterparty_corporate_votes"
|
||||||
|
"entity_corporate_directordealings"
|
||||||
|
"counterparty_corporate_directordealings"
|
||||||
|
"entity_corporate_brands"
|
||||||
|
"counterparty_corporate_brands"
|
||||||
|
"entity_corporate_website"
|
||||||
|
"counterparty_corporate_website"
|
||||||
|
"entity_corporate_domain"
|
||||||
|
"counterparty_corporate_domain"
|
||||||
|
"counterparty_corporate_IBAN"
|
||||||
|
"entity_corporate_IBAN"
|
||||||
|
"entity_corporate_solvency"
|
||||||
|
"counterparty_corporate_solvency"
|
||||||
|
"counterparty_corporate_rating"
|
||||||
|
"entity_corporate_rating"
|
||||||
|
"entity_corporate_ESG"
|
||||||
|
"counterparty_corporate_ESG"
|
||||||
|
"counterparty_corporate_license"
|
||||||
|
"entity_corporate_license"
|
||||||
|
"counterparty_corporate_warnings"
|
||||||
|
"entity_corporate_warnings"
|
||||||
|
"entity_corporate_eusanctions"
|
||||||
|
"counterparty_corporate_eusanctions"
|
||||||
|
"counterparty_corporate_ofacsanctions"
|
||||||
|
"entity_corporate_ofacsanctions"
|
||||||
|
"entity_corporate_uksanctions"
|
||||||
|
"counterparty_corporate_uksanctions"
|
||||||
|
"counterparty_corporate_pepexposure"
|
||||||
|
"entity_corporate_pepexposure"
|
||||||
|
"entity_corporate_adversemediascanning"
|
||||||
|
"counterparty_corporate_adversemediascanning"
|
||||||
|
"entity_corporate_corruptionexposure"
|
||||||
|
"counterparty_corporate_corruptionexposure"
|
||||||
|
"entity_corporate_AMLexposure"
|
||||||
|
"counterparty_corporate_AMLexposure"
|
||||||
|
"counterparty_corporate_CTYexposure"
|
||||||
|
"entity_corporate_CTYexposure"
|
||||||
|
"tranx_TX_AMOUNT"
|
||||||
|
"tranx_historical"
|
||||||
|
"tranx_TX_PURPOSE"
|
||||||
|
"tranx_counterpartyassessment"
|
||||||
|
"tranx_context"
|
||||||
|
"tranx_report"
|
||||||
|
"tranx_PURPOSEbase"
|
||||||
|
"tranx_performanceperiod"
|
||||||
|
"tranx_seasonality"
|
||||||
|
"tranx_resolution"
|
||||||
|
"trans_IBANvalidation"
|
||||||
|
"tranx_businesslogic"
|
||||||
|
"source_domaincheck"
|
||||||
|
"tranx_plausibilty"
|
||||||
|
"tranx_outliers"
|
||||||
|
"tranx_patterns"
|
||||||
|
"tranx_revenueimpact"
|
||||||
|
"tranx_profitimpact"
|
||||||
|
"tranx_marketimpact"
|
||||||
|
"source_check"
|
||||||
|
"source_approveuniqueID"
|
||||||
|
"counterparty_sanctions_circumvention"
|
||||||
|
"entity_sanctions_circumvention"
|
||||||
|
"entity_corruption_sector"
|
||||||
|
"counterparty_corruption_sector"
|
||||||
|
"counterparty_corruption_country"
|
||||||
|
"entity_corruption_country"
|
||||||
|
"counterparty_corruption_relationship"
|
||||||
|
"entity_corruption_relationship"
|
||||||
|
"counterparty_corporate_adverse"
|
||||||
|
"entity_corporate_adverse"
|
||||||
|
"counterparty_corporate_pep"
|
||||||
|
"entity_corporate_pep"
|
||||||
|
"entity_corporate_adverse2"
|
||||||
|
"counterparty_corporate_adverse2"
|
||||||
|
"tranx_duplicate"
|
||||||
|
"tranx_pattern2"
|
||||||
|
"counterparty_corporate_exportcontrol"
|
||||||
|
"entity_corporate_exportcontrol"
|
||||||
|
"counterparty_corporate_mediamatch"
|
||||||
|
"entity_corporate_mediamatch"
|
||||||
|
"tranx_contracttenor"
|
||||||
|
"entity_corporate_haven"
|
||||||
|
"counterparty_corporate_haven"
|
||||||
|
"tranx_mismatch"
|
||||||
|
"entity_country_risk"
|
||||||
|
"counterparty_country_risk"
|
||||||
|
"corporate_haven2"
|
||||||
|
"counterparty_corporate_insiders"
|
||||||
|
"entity_corporate_insiders"
|
||||||
|
"counterparty_corporate_courtcases"
|
||||||
|
"entity_corporate_courtcases"
|
||||||
|
"counterparty_corporate_manda"
|
||||||
|
"entity_corporate_manda"
|
||||||
|
"tranx_fakepurposecheck"
|
||||||
|
"tranx_structuring"
|
||||||
|
"tranx_newbankaccount"
|
||||||
|
"counterparty_corporate_pepdetail"
|
||||||
|
"entity_corporate_pepdetail"
|
||||||
|
"tranx_weekday"
|
||||||
|
"tranx_holdingobfuscation"
|
||||||
|
"source_randomplausibility"
|
||||||
|
"tranx_score"
|
||||||
|
"tranx_reasoning"
|
||||||
|
"tranx_recommendation"
|
||||||
|
"tranx_sourcerepository"
|
||||||
|
@@ -535,28 +535,28 @@ Transaction::STATUS_CLEARED => [
|
|||||||
<div class="rounded-3xl border border-zinc-200/60 bg-white/95 p-6 shadow-sm backdrop-blur dark:border-zinc-700/70 dark:bg-zinc-900/90">
|
<div class="rounded-3xl border border-zinc-200/60 bg-white/95 p-6 shadow-sm backdrop-blur dark:border-zinc-700/70 dark:bg-zinc-900/90">
|
||||||
<h4 class="text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Kerndaten Unternehmen</h4>
|
<h4 class="text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Kerndaten Unternehmen</h4>
|
||||||
<ul class="mt-4 space-y-3 text-sm text-zinc-600 dark:text-zinc-300">
|
<ul class="mt-4 space-y-3 text-sm text-zinc-600 dark:text-zinc-300">
|
||||||
<li class="flex items-center justify-between">
|
<li class="flex items-start justify-between gap-3">
|
||||||
<span>Rechtlicher Name</span>
|
<span class="flex-shrink-0">Rechtlicher Name</span>
|
||||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->legal_name }}</span>
|
<span class="font-medium text-right text-zinc-900 dark:text-zinc-100">{{ $selected->company->legal_name }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-center justify-between">
|
<!-- <li class="flex items-start justify-between gap-3">
|
||||||
<span>Ticker</span>
|
<span class="flex-shrink-0">Ticker</span>
|
||||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->ticker }}</span>
|
<span class="font-medium text-right text-zinc-900 dark:text-zinc-100">{{ $selected->company->ticker }}</span>
|
||||||
|
</li> -->
|
||||||
|
<li class="flex items-start justify-between gap-3">
|
||||||
|
<span class="flex-shrink-0">Sektor</span>
|
||||||
|
<span class="font-medium text-right text-zinc-900 dark:text-zinc-100">{{ $selected->company->sector }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-center justify-between">
|
<li class="flex items-start justify-between gap-3">
|
||||||
<span>Sektor</span>
|
<span class="flex-shrink-0">Hauptsitz</span>
|
||||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->sector }}</span>
|
<span class="font-medium text-right text-zinc-900 dark:text-zinc-100">{{ $selected->company->headquarters }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-center justify-between">
|
<li class="flex items-start justify-between gap-3">
|
||||||
<span>Hauptsitz</span>
|
<span class="flex-shrink-0">Land</span>
|
||||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->headquarters }}</span>
|
<span class="font-medium text-right text-zinc-900 dark:text-zinc-100">{{ $selected->company->country }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-center justify-between">
|
<!-- <li class="flex items-start justify-between gap-3">
|
||||||
<span>Land</span>
|
<span class="flex-shrink-0">KYC-Risikostufe</span>
|
||||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->country }}</span>
|
|
||||||
</li>
|
|
||||||
<!-- <li class="flex items-center justify-between">
|
|
||||||
<span>KYC-Risikostufe</span>
|
|
||||||
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold uppercase tracking-wide text-slate-700 dark:bg-slate-800 dark:text-slate-200">
|
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold uppercase tracking-wide text-slate-700 dark:bg-slate-800 dark:text-slate-200">
|
||||||
{{ Str::title($selected->company->kyc_risk_level) }}
|
{{ Str::title($selected->company->kyc_risk_level) }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in new issue
Block a user