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: getQualifiedGrowthBonus() funktioniert sowohl für: // - Live-berechnete Daten (qualificationCalculated = true) // - Gespeicherte Daten aus DB (qual_user_level bereits vorhanden) $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) { // Differenz: Mein Anspruch - Schutz von oben $diffPercent = max(0, $myPercent - $incomingProtection); $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, int $depth = 0): array { // Schutz vor zu tiefer Rekursion (Performance) $maxDepth = 20; if ($depth > $maxDepth) { Log::warning("GrowthBonusCalculator: Max recursion depth reached for user {$item->user_id}"); return []; } // Bei Live-Berechnung: Qualifikation berechnen falls nötig // Bei gespeicherten Daten: qual_user_level ist bereits vorhanden 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: getQualifiedGrowthBonus() funktioniert sowohl für: // - Live-berechnete Daten (qualificationCalculated = true) // - Gespeicherte Daten aus DB (qual_user_level bereits vorhanden) $myProtectionPercent = $item->getQualifiedGrowthBonus(); // 3. Kinder laden falls nicht vorhanden (für gespeicherte Daten) if (empty($item->businessUserItems) && $item->user_id) { $this->loadChildrenFromDatabase($item, $depth); } // 4. Rekursive Aggregation der Kinder if (!empty($item->businessUserItems)) { foreach ($item->businessUserItems as $childItem) { // Rekursiver Aufruf $childVolumes = $this->getVolumeByProtectionLevel($childItem, $depth + 1); // 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; } /** * Lädt Kinder eines Users aus der Datenbank für die Growth Bonus Berechnung * * WICHTIG: Diese Methode wird nur aufgerufen, wenn businessUserItems leer ist * (typischerweise bei gespeicherten Daten) */ private function loadChildrenFromDatabase(BusinessUserItemOptimized $item, int $depth): void { // Lade Sponsor-Beziehungen aus der User-Tabelle $childIds = \App\User::where('m_sponsor', $item->user_id) ->where('deleted_at', null) ->pluck('id') ->toArray(); if (empty($childIds)) { return; } // Hole das Date-Objekt vom Item $date = $item->getDate(); if (!$date || !isset($date->month) || !isset($date->year)) { Log::warning("GrowthBonusCalculator: No valid date for loading children of user {$item->user_id}"); return; } // Lade UserBusiness-Daten für alle Kinder $childBusinesses = \App\Models\UserBusiness::whereIn('user_id', $childIds) ->where('month', $date->month) ->where('year', $date->year) ->get() ->keyBy('user_id'); foreach ($childIds as $childId) { $childBusiness = $childBusinesses->get($childId); // Nur Kinder mit Daten und Volumen berücksichtigen if (!$childBusiness) { continue; } $childTPSum = (float) ($childBusiness->sales_volume_points_TP_sum ?? 0); // Nur relevante Kinder (mit Volumen oder qualifiziertem Level) if ($childTPSum <= 0 && empty($childBusiness->qual_user_level)) { continue; } // Erstelle ein BusinessUserItem aus den gespeicherten Daten $childItem = new BusinessUserItemOptimized($date, null); $childItem->makeUser($childId, false); // Aus DB laden $childItem->addUserID(); $item->businessUserItems[] = $childItem; } } }