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 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; } }