318 lines
13 KiB
PHP
318 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Services\BusinessPlan;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Service für die Berechnung des Growth Bonus (Tiefenbonus)
|
|
* Implementiert die Differenz-Bonus-Logik ab Ebene 1
|
|
*/
|
|
class GrowthBonusCalculator
|
|
{
|
|
/**
|
|
* Berechnet den Growth Bonus für einen BusinessUserItemOptimized
|
|
*
|
|
* @param BusinessUserItemOptimized $userItem Der User, für den der Bonus berechnet wird
|
|
* @param object $qualUserLevel Das Qualifikations-Level-Objekt des Users
|
|
* @return float Der berechnete Bonus
|
|
*/
|
|
public function calculate(BusinessUserItemOptimized $userItem, $qualUserLevel): float
|
|
{
|
|
// Basis-Check: Hat der User überhaupt Anspruch auf Growth Bonus?
|
|
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
// Falls keine direkte Downline-Struktur geladen ist, kann kein Growth Bonus berechnet werden
|
|
if (empty($userItem->businessUserItems) && !empty($userItem->business_lines)) {
|
|
Log::warning("GrowthBonusCalculator: Growth Bonus calculation requires loaded child structure (businessUserItems is empty for user {$userItem->user_id})");
|
|
return 0.0;
|
|
}
|
|
|
|
return $this->calculateRecursive($userItem, $qualUserLevel);
|
|
}
|
|
|
|
/**
|
|
* Führt die eigentliche Berechnung basierend auf der Differenz-Logik durch
|
|
*/
|
|
private function calculateRecursive(BusinessUserItemOptimized $userItem, $qualUserLevel): float
|
|
{
|
|
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
|
|
$totalGrowthBonus = 0.0;
|
|
|
|
// Iteriere über alle direkten Beine (Firstlines)
|
|
foreach ($userItem->businessUserItems as $childItem) {
|
|
|
|
// Hole die Volumen-Verteilung aus diesem Bein
|
|
// Array-Format: ['0.0' => 1000, '1.5' => 5000]
|
|
// Bedeutung: 1000 Punkte sind mit 0% geschützt, 5000 Punkte mit 1.5%
|
|
$volumeDistribution = $this->getVolumeByProtectionLevel($childItem);
|
|
|
|
foreach ($volumeDistribution as $protectedPercentStr => $volume) {
|
|
$alreadyDistributedPercent = (float) $protectedPercentStr;
|
|
|
|
// Differenz berechnen: Mein Anspruch MINUS was schon verteilt wurde
|
|
$mySharePercent = max(0, $myGrowthPercent - $alreadyDistributedPercent);
|
|
|
|
if ($mySharePercent > 0) {
|
|
$commission = round($volume / 100 * $mySharePercent, 2);
|
|
$totalGrowthBonus += $commission;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $totalGrowthBonus;
|
|
}
|
|
|
|
/**
|
|
* Liefert detaillierte Informationen zur Berechnung für die Anzeige
|
|
*
|
|
* @return array Detaillierte Aufschlüsselung pro Bein
|
|
*/
|
|
public function getCalculationDetails(BusinessUserItemOptimized $userItem, $qualUserLevel): array
|
|
{
|
|
$details = [];
|
|
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
|
|
return $details;
|
|
}
|
|
|
|
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
|
|
|
|
// Iteriere über alle direkten Beine (Firstlines)
|
|
foreach ($userItem->businessUserItems as $childItem) {
|
|
$legDetails = [
|
|
'user_id' => $childItem->user_id,
|
|
'first_name' => $childItem->first_name,
|
|
'last_name' => $childItem->last_name,
|
|
'level_name' => $childItem->user_level_name,
|
|
'volume_distribution' => [],
|
|
'total_commission' => 0.0,
|
|
'total_volume' => 0.0
|
|
];
|
|
|
|
$volumeDistribution = $this->getVolumeByProtectionLevel($childItem);
|
|
|
|
foreach ($volumeDistribution as $protectedPercentStr => $volume) {
|
|
$alreadyDistributedPercent = (float) $protectedPercentStr;
|
|
$mySharePercent = max(0, $myGrowthPercent - $alreadyDistributedPercent);
|
|
$commission = 0.0;
|
|
|
|
if ($mySharePercent > 0) {
|
|
$commission = round($volume / 100 * $mySharePercent, 2);
|
|
}
|
|
|
|
$legDetails['volume_distribution'][] = [
|
|
'protected_percent' => $alreadyDistributedPercent,
|
|
'volume' => $volume,
|
|
'my_share_percent' => $mySharePercent,
|
|
'commission' => $commission
|
|
];
|
|
|
|
$legDetails['total_commission'] += $commission;
|
|
$legDetails['total_volume'] += $volume;
|
|
}
|
|
|
|
// Sortiere nach Protection Level
|
|
usort($legDetails['volume_distribution'], function ($a, $b) {
|
|
return $a['protected_percent'] <=> $b['protected_percent'];
|
|
});
|
|
|
|
if ($legDetails['total_volume'] > 0) {
|
|
$details[] = $legDetails;
|
|
}
|
|
}
|
|
|
|
// Sortiere Beine nach höchster Provision
|
|
usort($details, function ($a, $b) {
|
|
return $b['total_commission'] <=> $a['total_commission'];
|
|
});
|
|
|
|
return $details;
|
|
}
|
|
|
|
/**
|
|
* Liefert eine Matrix-Sicht für die detaillierte Darstellung
|
|
* Zeilen = Beine (Legs), Spalten = Ebenen (Levels)
|
|
*/
|
|
public function getMatrixDetails(BusinessUserItemOptimized $userItem, $qualUserLevel): array
|
|
{
|
|
$details = [];
|
|
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
|
|
return $details;
|
|
}
|
|
|
|
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
|
|
|
|
foreach ($userItem->businessUserItems as $childItem) {
|
|
|
|
$legData = [
|
|
'user' => [
|
|
'id' => $childItem->user_id,
|
|
'name' => $childItem->first_name . ' ' . $childItem->last_name,
|
|
'level' => $childItem->user_level_name
|
|
],
|
|
'levels' => [],
|
|
'total_commission' => 0.0,
|
|
'total_volume' => 0.0
|
|
];
|
|
|
|
// Rekursiv die Ebenen dieses Beins einsammeln
|
|
// Start bei Ebene 1 (das ist das Kind selbst)
|
|
// Initial Protection ist 0 (vom Upline/Mir kommt kein Schutz, der relevant wäre, da ICH ja der Empfänger bin)
|
|
$this->collectLegLevels($childItem, 1, 0.0, $myGrowthPercent, $legData);
|
|
|
|
if (!empty($legData['levels'])) {
|
|
// Sortieren nach Ebenen-Index
|
|
ksort($legData['levels']);
|
|
$details[] = $legData;
|
|
}
|
|
}
|
|
|
|
// Sortieren nach Gesamt-Provision
|
|
usort($details, function ($a, $b) {
|
|
return $b['total_commission'] <=> $a['total_commission'];
|
|
});
|
|
|
|
return $details;
|
|
}
|
|
|
|
/**
|
|
* Rekursive Hilfsfunktion für Matrix-Daten
|
|
*/
|
|
private function collectLegLevels(BusinessUserItemOptimized $item, int $level, float $incomingProtection, float $myPercent, array &$legData)
|
|
{
|
|
// 1. Eigenen Status ermitteln (Schutz für Downline)
|
|
// WICHTIG: Nutze getQualifiedGrowthBonus() für das ERREICHTE Qualifikations-Level des Monats
|
|
// Nicht getActiveGrowthBonus() verwenden, da das das aktuelle Karriere-Level wäre!
|
|
$userProtection = $item->getQualifiedGrowthBonus();
|
|
$userLevelName = '';
|
|
|
|
if ($userProtection > 0) {
|
|
$qual = $item->getQualUserLevel();
|
|
$userLevelName = is_array($qual) ? ($qual['name'] ?? '') : ($qual->name ?? '');
|
|
}
|
|
|
|
// Berechnung für diesen User (Ebene)
|
|
$volume = (float) ($item->sales_volume_points_TP_sum ?? 0);
|
|
|
|
// Auch User ohne Volumen in die Matrix aufnehmen, wenn sie einen Status haben (Blocker sichtbar machen)
|
|
// Aber wir brauchen Volumen für die Relevanz. Wenn Volumen 0, dann ist der Block hier (noch) egal,
|
|
// wirkt aber auf die Ebenen darunter.
|
|
|
|
if ($volume > 0 || $userProtection > 0) {
|
|
// WICHTIG: Der effektive Schutz ist das MAXIMUM aus:
|
|
// - Schutz von oben ($incomingProtection)
|
|
// - Eigener Schutz des Users ($userProtection)
|
|
// Der User schützt sein EIGENES Volumen mit seinem eigenen Growth Bonus!
|
|
$effectiveProtection = max($incomingProtection, $userProtection);
|
|
$diffPercent = max(0, $myPercent - $effectiveProtection);
|
|
$commission = round($volume / 100 * $diffPercent, 2);
|
|
|
|
if (!isset($legData['levels'][$level])) {
|
|
$legData['levels'][$level] = [
|
|
'volume' => 0.0,
|
|
'commission' => 0.0,
|
|
'details' => [],
|
|
'has_blocker' => false, // Flag für UI
|
|
'blocker_name' => ''
|
|
];
|
|
}
|
|
|
|
$legData['levels'][$level]['volume'] += $volume;
|
|
$legData['levels'][$level]['commission'] += $commission;
|
|
|
|
// Markiere Blocker
|
|
if ($userProtection > 0) {
|
|
$legData['levels'][$level]['has_blocker'] = true;
|
|
$legData['levels'][$level]['blocker_name'] = $userLevelName . ' (' . $userProtection . '%)';
|
|
}
|
|
|
|
// Detail-Information für Hover/Debug
|
|
$legData['levels'][$level]['details'][] = [
|
|
'u' => $item->user_id,
|
|
'n' => $item->first_name . ' ' . $item->last_name, // Name für Tooltip
|
|
'v' => $volume,
|
|
'p_in' => $incomingProtection,
|
|
'p_own' => $userProtection,
|
|
'pct' => $diffPercent
|
|
];
|
|
|
|
$legData['total_volume'] += $volume;
|
|
$legData['total_commission'] += $commission;
|
|
}
|
|
|
|
// Protection für nächste Ebene: Maximum aus was von oben kam und was dieser User beansprucht
|
|
$nextProtection = max($incomingProtection, $userProtection);
|
|
|
|
// Rekursion (Begrenzt auf 30 Ebenen für Anzeige)
|
|
if ($level < 30 && !empty($item->businessUserItems)) {
|
|
foreach ($item->businessUserItems as $child) {
|
|
$this->collectLegLevels($child, $level + 1, $nextProtection, $myPercent, $legData);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Liefert das Volumen der Downline eines Users gruppiert nach dem "bereits verteilten Prozentsatz" (Protection Level).
|
|
* Rekursive Funktion, die die "Differenz-Logik" vorbereitet.
|
|
*
|
|
* @param BusinessUserItemOptimized $item
|
|
* @return array<string, float> Key = Protected Percent, Value = Volume Points
|
|
*/
|
|
public function getVolumeByProtectionLevel(BusinessUserItemOptimized $item): array
|
|
{
|
|
// WICHTIG: Nur calcQualPP aufrufen wenn KEINE gespeicherten Daten vorhanden sind
|
|
// Bei gespeicherten Daten ist qual_user_level bereits vorhanden, auch wenn qualificationCalculated=false
|
|
if (!$item->isQualificationCalculated() && !$item->isQualLevel()) {
|
|
$item->calcQualPP();
|
|
}
|
|
|
|
$volumes = [];
|
|
|
|
// 1. Eigenes Volumen (Unprotected / GAP)
|
|
// Man selbst schützt seinen eigenen Umsatz NICHT vor der Upline.
|
|
// Daher Start mit Protection Level 0.0
|
|
$ownVolume = (float) ($item->sales_volume_points_TP_sum ?? 0);
|
|
|
|
if ($ownVolume > 0) {
|
|
$key = '0.0';
|
|
$volumes[$key] = $ownVolume;
|
|
}
|
|
|
|
// 2. Mein Schutz-Level ermitteln (für das Volumen, das durch mich hindurch fließt)
|
|
// WICHTIG: Nutze getQualifiedGrowthBonus() für das ERREICHTE Qualifikations-Level des Monats
|
|
// Nicht getActiveGrowthBonus() verwenden, da das das aktuelle Karriere-Level wäre!
|
|
$myProtectionPercent = $item->getQualifiedGrowthBonus();
|
|
|
|
// Debug-Logging für Troubleshooting
|
|
Log::debug("GrowthBonusCalculator: User {$item->user_id} - qualifiedGrowthBonus={$myProtectionPercent}%, activeGrowthBonus={$item->getActiveGrowthBonus()}%, isQualLevel=" . ($item->isQualLevel() ? 'true' : 'false'));
|
|
|
|
// 3. Rekursive Aggregation der Kinder
|
|
if (!empty($item->businessUserItems)) {
|
|
foreach ($item->businessUserItems as $childItem) {
|
|
|
|
// Rekursiver Aufruf
|
|
$childVolumes = $this->getVolumeByProtectionLevel($childItem);
|
|
|
|
// 4. Schutz-Level anwenden und aggregieren
|
|
foreach ($childVolumes as $protectedPercentStr => $vol) {
|
|
$incomingProtection = (float) $protectedPercentStr;
|
|
|
|
// Das Volumen ist bereits mit $incomingProtection geschützt.
|
|
// Da es nun durch diesen User fließt, erhöht sich der Schutz auf dessen Level (falls höher).
|
|
$effectiveProtection = max($incomingProtection, $myProtectionPercent);
|
|
|
|
$newKey = (string) $effectiveProtection;
|
|
|
|
if (!isset($volumes[$newKey])) {
|
|
$volumes[$newKey] = 0.0;
|
|
}
|
|
$volumes[$newKey] += $vol;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $volumes;
|
|
}
|
|
}
|