validateInput($month, $year); $this->initializeDate($month, $year); $this->initFrom = $initFrom; $this->forceLiveCalculation = $forceLiveCalculation; // Dependency Injection mit Fallback $this->repository = $repository ?? new BusinessUserRepository($month, $year); $this->renderer = $renderer ?? new TreeHtmlRenderer($initFrom, $forceLiveCalculation); $this->logger = $logger ?? app(LoggerInterface::class); } /** * Initialisiert die Business-Struktur für Admin-Ansicht * * @param bool $check Prüft auf gespeicherte Struktur * @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten */ public function initStructureAdmin(bool $check = true, bool $forceLiveCalculation = false): void { try { $this->forceLiveCalculation = $forceLiveCalculation; if ($forceLiveCalculation) { $this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year} with forced live calculation"); $this->buildFreshStructure(); return; } $storedStructure = null; if ($check) { $storedStructure = $this->repository->getStoredStructure(); } if ($storedStructure) { $this->logger->info("Loading stored business structure for {$this->date->month}/{$this->date->year}"); $this->loadStoredStructure($storedStructure); return; } else { $this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year}"); $this->buildFreshStructure(); return; } } catch (\Exception $e) { $this->logger->error("Error initializing admin structure: " . $e->getMessage()); throw $e; } } /** * Initialisiert die Struktur für einen spezifischen User * * @param int $userId Die User-ID * @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten */ public function initStructureUser(int $userId, bool $forceLiveCalculation = false): void { try { $this->forceLiveCalculation = $forceLiveCalculation; if ($forceLiveCalculation) { $this->logger->info("Initializing structure for user: {$userId} with forced live calculation"); } else { $this->logger->info("Initializing structure for user: {$userId}"); } $user = $this->repository->getUserWithRelations($userId); if (!$user) { $this->logger->warning("User not found: {$userId}"); return; } $businessUserItem = new BusinessUserItemOptimized($this->date, $this); $businessUserItem->makeUserFromModel($user); // Erst User-Model laden, ohne forceLiveCalculation $this->addUserIdToProcessed($userId); $this->businessUsers[] = $businessUserItem; $this->logger->info("Created businessUserItem for user {$userId}, total businessUsers: " . count($this->businessUsers)); // Prüfe gespeicherte Struktur nur, wenn Live-Berechnung nicht erzwungen wird $storedStructure = null; if (!$forceLiveCalculation) { $storedStructure = $this->repository->getStoredStructure(); $this->logger->info("Stored structure " . ($storedStructure ? "found" : "not found")); } if ($storedStructure && !$forceLiveCalculation) { $this->loadStoredParentsUsers($storedStructure); if (isset($this->businessUsers[0]) && $this->businessUsers[0]->sponsor) { $this->loadStoredSponsorUser($this->businessUsers[0]->sponsor->user_id); } } else { if ($forceLiveCalculation) { $this->logger->info("Forcing live calculation - skipping stored structure for user {$userId}"); } $this->loadParentsUsers(); $this->loadSponsorUser($userId); $totalSubUsers = 0; foreach ($this->businessUsers as $businessUser) { $totalSubUsers += count($businessUser->businessUserItems); } $this->logger->info("After loadParentsUsers: {$totalSubUsers} total sub-users loaded across " . count($this->businessUsers) . " business users"); // WICHTIG: calcQualPP() erst NACH loadParentsUsers() aufrufen, da Points benötigt werden if ($forceLiveCalculation) { $this->logger->info("Calculating qualification levels for all business users"); foreach ($this->businessUsers as $businessUser) { $businessUser->calcQualPP(); } //wird nicht benötigt, da hier nur die Points berechnet werden //$this->calculateQualPPForAllUsers(); // Auch für alle Sub-User } } } catch (\Exception $e) { $this->logger->error("Error initializing user structure for {$userId}: " . $e->getMessage()); throw $e; } } /** * Initialisiert detaillierte Business-User-Informationen * * @param User $user Das User-Model * @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten */ public function initBusinesslUserDetail(User $user, bool $forceLiveCalculation = false): void { try { $this->logger->info("Initializing business user details for: {$user->id}"); $this->businessUser = new BusinessUserItemOptimized($this->date, $this); $this->businessUser->makeUserFromModel($user, $forceLiveCalculation); // ✅ Nutzt bereits User-Objekt $this->businessUser->checkSponsor($user); // Führe vollständige Berechnung durch, wenn: // 1. Daten nicht gespeichert sind ODER // 2. Live-Berechnung erzwungen wird if (!$this->businessUser->isSave() || $forceLiveCalculation) { if ($forceLiveCalculation) { $this->logger->info("Forcing live calculation for user {$user->id}"); } // Aufbau der Struktur für den User in die unendliche Tiefe $this->businessUser->readParentsBusinessUsers($forceLiveCalculation); // Calculate Points in Lines (optimiert für Memory-Effizienz) if (count($this->businessUser->businessUserItems) > 0) { $this->calculateUserPointsOptimized($this->businessUser->businessUserItems, 1, $this->businessUser); } // Qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints) $this->businessUser->calcQualPP(); } } catch (\Exception $e) { $this->logger->error("Error initializing business user details for {$user->id}: " . $e->getMessage()); throw $e; } } /** * Gibt Growth Bonus zurück (ab Linie 6) * Erweitert um Array/Object-Kompatibilität für business_lines */ public function getGrowthBonus(): array { if (!$this->businessUser || !$this->businessUser->business_lines) { return []; } if (count($this->businessUser->business_lines) > 6) { // Handle both array and object types (JSON deserialization inconsistency) if (is_array($this->businessUser->business_lines)) { $bLines = $this->businessUser->business_lines; } else { $bLines = $this->businessUser->business_lines->toArray(); } return array_slice($bLines, 6); } return []; } /** * Gibt Wert für spezifische Linie zurück */ public function getKeybyLine(int $line, string $key) { if (!$this->businessUser || !$this->businessUser->business_lines) { return 0; } $bLines = $this->businessUser->business_lines; if (!isset($bLines[$line])) { return 0; } $lineData = $bLines[$line]; if ($lineData instanceof stdClass) { return $lineData->{$key} ?? 0; } if (is_array($lineData)) { return $lineData[$key] ?? 0; } return 0; } /** * HTML-Rendering Methoden (Delegation an Renderer) */ public function makeHtmlTree(): string { return $this->renderer->renderTree($this->businessUsers); } public function makeParentlessHtml(): string { return $this->renderer->renderParentless($this->parentless); } public function makeSponsorHtml(): string { return $this->renderer->renderSponsor($this->sponsor); } /** * Getter-Methoden (Rückwärtskompatibilität) */ public function getItems(): array { return $this->businessUsers; } /** * Getter-Methoden (Rückwärtskompatibilität) */ public function getItem(): object { return $this->businessUser; } /** * Zählt die Gesamtanzahl aller User in der Struktur (rekursiv) */ public function getTotalUserCount(): int { $totalCount = 0; // Zähle alle Root-User $totalCount += count($this->businessUsers); // Zähle alle Unter-User rekursiv foreach ($this->businessUsers as $businessUser) { $totalCount += $this->countBusinessUserItems($businessUser); } // Zähle parentless User $totalCount += count($this->parentless); return $totalCount; } /** * Zählt BusinessUserItems rekursiv */ private function countBusinessUserItems($businessUserItem): int { $count = 0; if (isset($businessUserItem->businessUserItems) && is_array($businessUserItem->businessUserItems)) { $count += count($businessUserItem->businessUserItems); // Rekursiv durch alle Unter-Items zählen foreach ($businessUserItem->businessUserItems as $subItem) { $count += $this->countBusinessUserItems($subItem); } } return $count; } public function isParentless(): bool { return !empty($this->parentless); } /** * Static Methoden (Rückwärtskompatibilität) */ public static function isFromStored(int $month, int $year): ?UserBusinessStructure { $structure = UserBusinessStructure::where('year', $year) ->where('month', $month) ->first(); return ($structure && $structure->completed) ? $structure : null; } /** * Öffentliche Methode zum Hinzufügen einer User ID zu den verarbeiteten IDs */ public function addProcessedUserId(int $id): void { $this->addUserIdToProcessed($id); } public static function addUserID(int $id): void { // Deprecated: Statische Methode kann nicht auf Instanz-Variable zugreifen // Verwende stattdessen die Instanz-Methode addProcessedUserId() throw new \BadMethodCallException('addUserID ist deprecated. Verwende Instanz-Methode addProcessedUserId() stattdessen.'); } // ===== Private Methoden ===== /** * Validiert Eingabeparameter */ private function validateInput(int $month, int $year): void { if ($month < 1 || $month > 12) { throw new \InvalidArgumentException("Invalid month: {$month}"); } $currentYear = (int) date('Y'); if ($year < 2020 || $year > $currentYear + 1) { throw new \InvalidArgumentException("Invalid year: {$year}"); } } /** * Initialisiert Datums-Objekt */ private function initializeDate(int $month, int $year): void { $this->date = new stdClass(); $date = Carbon::parse($year . '-' . $month . '-1'); $this->date->month = $month; $this->date->year = $year; $this->date->start_date = $date->format('Y-m-d H:i:s'); $this->date->end_date = $date->endOfMonth()->format('Y-m-d H:i:s'); } /** * Lädt gespeicherte Struktur */ private function loadStoredStructure(UserBusinessStructure $structure): void { $this->loadStoredRootUsers($structure); $this->loadStoredParentsUsers($structure); $this->loadStoredParentlessUsers($structure); // Prüfe ob gespeicherte Daten vollständig sind, ansonsten berechne neu $this->validateAndRecalculateIfNeeded(); $this->validateAndRecalculateParentlessIfNeeded(); } /** * Baut frische Struktur auf */ private function buildFreshStructure(): void { $this->loadRootUsers(); $this->loadParentsUsers(); $this->loadParentlessUsers(); // WICHTIG: Berechne Punkte und Qualifikationen für alle Business-Users $this->calculateAllBusinessUsers(); $this->calculateAllParentlessUsers(); } /** * Lädt Root-Users (optimiert mit Memory-Monitoring) */ private function loadRootUsers(): void { $startMemory = memory_get_usage(); $users = $this->repository->getRootUsers(); foreach ($users as $user) { // Memory-Check vor jeder User-Verarbeitung $this->checkMemoryUsage('loadRootUsers', $user->id); $businessUserItem = new BusinessUserItemOptimized($this->date, $this); $businessUserItem->makeUserFromModel($user, $this->forceLiveCalculation); // ✅ Nutzt bereits geladene Relations mit forceLiveCalculation $this->addUserIdToProcessed($user->id); $this->businessUsers[] = $businessUserItem; } $endMemory = memory_get_usage(); $memoryUsed = $this->formatBytes($endMemory - $startMemory); $this->logger->info("Loaded " . count($users) . " root users with optimized relations. Memory used: {$memoryUsed}"); } /** * Lädt Parent-Users für alle Business-Users */ private function loadParentsUsers(): void { $this->logger->info("Loading parent users for " . count($this->businessUsers) . " business users"); foreach ($this->businessUsers as $businessUser) { $businessUser->readParentsBusinessUsers($this->forceLiveCalculation); $this->logger->debug("Loaded " . count($businessUser->businessUserItems) . " parent users for user " . ($businessUser->b_user->user_id ?? 'unknown')); } } /** * Lädt parentlose Users (Memory-optimiert) */ private function loadParentlessUsers(): void { $count = 0; $excludeIds = array_keys($this->processedUserIds); foreach ($this->repository->getParentlessUsers($excludeIds) as $user) { $businessUserItem = new BusinessUserItemOptimized($this->date, $this); $businessUserItem->makeUserFromModel($user, $this->forceLiveCalculation); // ✅ Nutzt bereits geladene Relations mit forceLiveCalculation $this->parentless[] = $businessUserItem; $count++; } $this->logger->info("Loaded {$count} parentless users with optimized relations"); } /** * Berechnet Punkte und Qualifikationen für alle Business-Users */ private function calculateAllBusinessUsers(): void { $startTime = microtime(true); $this->logger->info("Starting calculation for " . count($this->businessUsers) . " business users"); foreach ($this->businessUsers as $businessUser) { try { // Berechne Punkte in Linien (wie bei initBusinesslUserDetail) if (count($businessUser->businessUserItems) > 0) { $this->calculateUserPointsOptimized($businessUser->businessUserItems, 1, $businessUser); } // Qualifikation nach qual_kp und qual_pp berechnen $businessUser->calcQualPP(); } catch (\Exception $e) { $this->logger->error("Error calculating business user {$businessUser->__get('user_id')}: " . $e->getMessage()); // Weiter mit dem nächsten User, nicht abbrechen continue; } } $endTime = microtime(true); $executionTime = round(($endTime - $startTime) * 1000, 2); $this->logger->info("Completed calculations for all business users in {$executionTime}ms"); } /** * Berechnet Punkte und Qualifikationen für alle Parentless-Users */ private function calculateAllParentlessUsers(): void { if (empty($this->parentless)) { return; } $startTime = microtime(true); $this->logger->info("Starting calculation for " . count($this->parentless) . " parentless users"); foreach ($this->parentless as $parentlessUser) { try { // Berechne Punkte in Linien if (count($parentlessUser->businessUserItems) > 0) { $this->calculateUserPointsOptimized($parentlessUser->businessUserItems, 1, $parentlessUser); } // Qualifikation berechnen $parentlessUser->calcQualPP(); } catch (\Exception $e) { $this->logger->error("Error calculating parentless user {$parentlessUser->__get('user_id')}: " . $e->getMessage()); continue; } } $endTime = microtime(true); $executionTime = round(($endTime - $startTime) * 1000, 2); $this->logger->info("Completed calculations for all parentless users in {$executionTime}ms"); } /** * Validiert gespeicherte Daten und berechnet bei Bedarf neu */ private function validateAndRecalculateIfNeeded(): void { $incompleteUsers = 0; foreach ($this->businessUsers as $businessUser) { // Prüfe ob grundlegende Berechnungen vorhanden sind if ($this->isBusinessUserIncomplete($businessUser)) { $incompleteUsers++; try { // Führe fehlende Berechnungen durch if (count($businessUser->businessUserItems) > 0) { $this->calculateUserPointsOptimized($businessUser->businessUserItems, 1, $businessUser); } $businessUser->calcQualPP(); } catch (\Exception $e) { $this->logger->error("Error recalculating business user {$businessUser->__get('user_id')}: " . $e->getMessage()); } } } if ($incompleteUsers > 0) { $this->logger->info("Recalculated {$incompleteUsers} incomplete business users from stored data"); } } /** * Prüft ob ein BusinessUser unvollständige Daten hat * Erweitert um Level-Qualifikationsdaten für Struktur-Ansicht */ private function isBusinessUserIncomplete($businessUser): bool { // Prüfe grundlegende Felder die nach Berechnungen vorhanden sein sollten $salesVolumeSum = $businessUser->__get('sales_volume_points_sum'); $qualKp = $businessUser->__get('qual_kp'); // Prüfe Level-Qualifikationsdaten für Struktur-Ansicht $nextQualUserLevel = $businessUser->__get('next_qual_user_level'); $nextCanUserLevel = $businessUser->__get('next_can_user_level'); $hasLevelQualificationData = !empty($nextQualUserLevel) || !empty($nextCanUserLevel); // User ist unvollständig wenn: // 1. Grundlegende berechnete Werte fehlen ODER // 2. Level-Qualifikationsdaten fehlen (wichtig für Struktur-Ansicht mit grünen Pfeilen) $missingBasicData = ($salesVolumeSum === null || $salesVolumeSum === 0) && ($qualKp === null || $qualKp === 0); $missingLevelData = !$hasLevelQualificationData; return $missingBasicData || $missingLevelData; } /** * Validiert und berechnet parentless Users bei Bedarf neu */ private function validateAndRecalculateParentlessIfNeeded(): void { if (empty($this->parentless)) { return; } $incompleteUsers = 0; foreach ($this->parentless as $parentlessUser) { if ($this->isBusinessUserIncomplete($parentlessUser)) { $incompleteUsers++; try { if (count($parentlessUser->businessUserItems) > 0) { $this->calculateUserPointsOptimized($parentlessUser->businessUserItems, 1, $parentlessUser); } $parentlessUser->calcQualPP(); } catch (\Exception $e) { $this->logger->error("Error recalculating parentless user {$parentlessUser->__get('user_id')}: " . $e->getMessage()); } } } if ($incompleteUsers > 0) { $this->logger->info("Recalculated {$incompleteUsers} incomplete parentless users from stored data"); } } /** * Lädt Sponsor für User */ private function loadSponsorUser(int $userId): void { try { $sponsorUser = $this->repository->getSponsorForUser($userId); if ($sponsorUser) { $this->sponsor = new BusinessUserItemOptimized($this->date, $this); $this->sponsor->makeUser($sponsorUser->id); $this->logger->info("Loaded sponsor {$sponsorUser->id} for user {$userId}"); } } catch (\Exception $e) { $this->logger->warning("Could not load sponsor for user {$userId}: " . $e->getMessage()); } } /** * Gespeicherte Root-Users laden */ private function loadStoredRootUsers(UserBusinessStructure $structure): void { if (!$structure->structure) { return; } foreach ($structure->structure as $obj) { $businessUserItem = new BusinessUserItemOptimized($this->date, $this); $businessUserItem->makeUser($obj->user_id); $this->addUserIdToProcessed($obj->user_id); $this->businessUsers[] = $businessUserItem; } } /** * Gespeicherte Parent-Users laden */ private function loadStoredParentsUsers(UserBusinessStructure $structure): void { foreach ($this->businessUsers as $businessUser) { $businessUser->readStoredParentsBusinessUsers($structure->structure); } } /** * Gespeicherte parentlose Users laden */ private function loadStoredParentlessUsers(UserBusinessStructure $structure): void { if (!$structure->parentless) { return; } foreach ($structure->parentless as $obj) { if (!isset($this->processedUserIds[$obj->user_id])) { $businessUserItem = new BusinessUserItemOptimized($this->date, $this); $businessUserItem->makeUser($obj->user_id); $this->parentless[] = $businessUserItem; } } } /** * Gespeicherten Sponsor laden */ private function loadStoredSponsorUser(int $userId): void { $this->sponsor = new BusinessUserItemOptimized($this->date, $this); $this->sponsor->makeUser($userId); } /** * Optimierte Punkte-Berechnung (Stack-basiert mit korrekter Depth-First Reihenfolge) * * KRITISCH: Stack muss gleiche Reihenfolge wie Original-Rekursion produzieren * Original: Depth-First Traversierung (erst tief, dann Punkte addieren) * Stack: Muss umgekehrt arbeiten - erst alle Kinder sammeln, dann von tief zu flach verarbeiten */ private function calculateUserPointsOptimized(array $businessUserItems, int $startLine, BusinessUserItemOptimized $businessUserToUpdate): void { $processingStack = []; $collectionStack = []; // Sammelt Items in korrekter Reihenfolge // Phase 1: Sammle alle Items in Depth-First Reihenfolge foreach ($businessUserItems as $item) { $collectionStack[] = ['item' => $item, 'line' => $startLine, 'depth' => 0]; } // Expandiere alle Kinder (Depth-First) $processedItems = []; while (!empty($collectionStack)) { $current = array_shift($collectionStack); // FIFO für Breadth-First Sammlung $item = $current['item']; $line = $current['line']; $depth = $current['depth']; // Markiere für Verarbeitung (mit Tiefe für spätere Sortierung) $processingStack[] = [ 'item' => $item, 'line' => $line, 'depth' => $depth, 'id' => $item->user_id ?? uniqid() ]; // Füge Kinder hinzu (werden später verarbeitet = Depth-First) if (isset($item->businessUserItems) && count($item->businessUserItems) > 0) { // Kinder in umgekehrter Reihenfolge hinzufügen für korrekte Stack-Verarbeitung $children = array_reverse($item->businessUserItems); foreach ($children as $childItem) { array_unshift($collectionStack, [ 'item' => $childItem, 'line' => $line + 1, 'depth' => $depth + 1 ]); } } } // Phase 2: Sortiere nach Tiefe (tiefste zuerst, wie bei Rekursion) usort($processingStack, function ($a, $b) { return $b['depth'] <=> $a['depth']; // Tiefste zuerst }); // ===================================================================== // PHASE 1: Punkte aggregieren (von tief zu flach) // // WICHTIG: Die DB speichert nur EIGENE Punkte (sales_volume_points_TP_sum). // Für die Upline brauchen wir die AGGREGIERTEN Punkte (eigene + Team). // Diese Aggregation muss zur Laufzeit passieren. // ===================================================================== foreach ($processingStack as $current) { $item = $current['item']; try { // Aggregiere: Eigene Punkte + Summe aller Kinder if (isset($item->businessUserItems) && count($item->businessUserItems) > 0) { $childrenTotal = 0.0; foreach ($item->businessUserItems as $child) { // Kinder haben bereits aggregierte Punkte (von tief zu flach) $childrenTotal += (float) ($child->sales_volume_points_TP_sum ?? 0); } // Eigene Punkte + Kinder = Gesamt-Team-Punkte $ownPoints = $this->getOwnSalesVolumePoints($item); $item->sales_volume_points_TP_sum = $ownPoints + $childrenTotal; // Auch im b_user aktualisieren $bUser = $item->getBUser(); if ($bUser) { $bUser->sales_volume_points_TP_sum = $item->sales_volume_points_TP_sum; } } } catch (\Exception $e) { $this->logger->error("Error aggregating points for {$current['id']}: " . $e->getMessage()); } } // ===================================================================== // PHASE 1b: business_lines für ROOT aus DIREKTEN Kindern aufbauen // Jetzt haben alle Kinder ihre aggregierten sales_volume_points_TP_sum // ===================================================================== $lineNumber = 1; foreach ($businessUserItems as $directChild) { $childPoints = (float) ($directChild->sales_volume_points_TP_sum ?? 0); $obj = new stdClass(); $obj->points = $childPoints; $obj->user_id = $directChild->user_id ?? null; $businessUserToUpdate->addBusinessLineToUser($lineNumber, $obj); $businessUserToUpdate->addTotalTP($childPoints); $lineNumber++; } // ===================================================================== // PHASE 2: Qualifikationen berechnen (von tief zu flach) // Jetzt haben alle User ihre aggregierten sales_volume_points_TP_sum // // WICHTIG: Kinder, die aus der DB geladen wurden (isSave=true), // haben bereits korrekte qual_user_level. Diese NICHT überschreiben! // ===================================================================== foreach ($processingStack as $current) { $item = $current['item']; try { // business_lines für diesen User aufbauen (aus direkten Kindern) if (isset($item->businessUserItems) && count($item->businessUserItems) > 0) { $this->buildBusinessLinesForUser($item); } // Qualifikation NUR berechnen wenn: // - Noch nicht berechnet UND // - Keine gespeicherten Daten vorhanden (sonst würden wir korrekte Daten überschreiben) if (!$item->isQualificationCalculated() && !$item->isSave()) { $item->calcQualPP(false, true); } } catch (\Exception $e) { $this->logger->error("Error calculating qualification for {$current['id']}: " . $e->getMessage()); } } // ===================================================================== // PHASE 3: Provisionen berechnen (von tief zu flach) // Jetzt haben alle User ihre qual_user_level und active_growth_bonus // ===================================================================== foreach ($processingStack as $current) { $item = $current['item']; try { $item->calculateCommissionsOnly(); } catch (\Exception $e) { $this->logger->error("Error calculating commissions for {$current['id']}: " . $e->getMessage()); } } $this->logger->info("Processed " . count($processingStack) . " business user items in depth-first order"); } /** * Baut die business_lines für einen User aus seinen direkten Kindern auf. * * Jeder direkte Partner (Child) bildet eine eigene Linie. * Die Punkte der Linie = sales_volume_points_TP_sum des Partners (inkl. dessen Team) * * WICHTIG: Dies muss VOR calcQualPP() aufgerufen werden, da die Qualifikation * die Payline-Punkte aus den business_lines berechnet. * * @param BusinessUserItemOptimized $user Der User, dessen business_lines aufgebaut werden */ private function buildBusinessLinesForUser(BusinessUserItemOptimized $user): void { if (!isset($user->businessUserItems) || count($user->businessUserItems) === 0) { return; } // Initialisiere business_lines über b_user falls nötig $bUser = $user->getBUser(); if (!$bUser) { return; } if (!isset($bUser->business_lines) || !is_array($bUser->business_lines)) { $bUser->business_lines = []; } $lineNumber = 1; foreach ($user->businessUserItems as $childItem) { // Jedes Kind bildet eine eigene Linie $childPoints = (float) ($childItem->sales_volume_points_TP_sum ?? 0); $obj = new stdClass(); $obj->points = $childPoints; $obj->user_id = $childItem->user_id ?? null; // Nutze die existierende Methode die auf b_user->business_lines arbeitet $user->addBusinessLineToUser($lineNumber, $obj); $this->logger->debug("BuildBusinessLines: User {$user->user_id} Line {$lineNumber} = {$childPoints} points (from child {$obj->user_id})"); $lineNumber++; } } /** * Holt die EIGENEN Team-Punkte eines Users (ohne bereits aggregierte Kinder-Punkte) * * Wenn ein User aus der DB geladen wurde, enthält sales_volume_points_TP_sum * möglicherweise bereits aggregierte Werte von einer vorherigen Berechnung. * Diese Methode holt die "echten" eigenen Punkte aus user_sales_volumes. */ /** * Holt die EIGENEN Team-Punkte eines Users (ohne Kinder-Punkte) * * Diese Methode holt die "echten" eigenen Punkte aus verschiedenen Quellen: * 1. user_sales_volumes.sales_volume_points_TP_sum (primär) * 2. user_businesses.sales_volume_TP_points (Fallback) * 3. b_user->sales_volume_TP_points (letzter Fallback) */ private function getOwnSalesVolumePoints(BusinessUserItemOptimized $item): float { $bUser = $item->getBUser(); if (!$bUser || !$bUser->user_id) { return 0.0; } // Versuch 1: Hole aus user_sales_volumes $salesVolume = \App\Models\UserSalesVolume::where('user_id', $bUser->user_id) ->where('month', $this->date->month) ->where('year', $this->date->year) ->first(); if ($salesVolume && $salesVolume->sales_volume_points_TP_sum > 0) { return (float) $salesVolume->sales_volume_points_TP_sum; } // Versuch 2: Hole aus user_businesses (gespeicherte eigene Punkte) // WICHTIG: Das ist sales_volume_TP_points, nicht sales_volume_points_TP_sum // sales_volume_TP_points sind die EIGENEN Punkte // sales_volume_points_TP_sum KANN bereits aggregierte Punkte enthalten (von vorheriger Berechnung) $userBusiness = \App\Models\UserBusiness::where('user_id', $bUser->user_id) ->where('month', $this->date->month) ->where('year', $this->date->year) ->first(); if ($userBusiness && $userBusiness->sales_volume_TP_points > 0) { return (float) $userBusiness->sales_volume_TP_points; } // Versuch 3: Fallback auf b_user return (float) ($bUser->sales_volume_TP_points ?? 0); } /** * User-ID zu verarbeiteten IDs hinzufügen */ private function addUserIdToProcessed(int $id): void { $this->processedUserIds[$id] = true; } /** * Prüft ob User bereits verarbeitet wurde (Public für BusinessUserItemOptimized) */ public function isUserProcessed(int $id): bool { return isset($this->processedUserIds[$id]); } /** * Memory-Monitoring Methoden */ private function checkMemoryUsage(string $operation, $identifier = null): void { $currentMemory = memory_get_usage(); $memoryLimit = $this->parseMemoryLimit(ini_get('memory_limit')); $memoryPercent = ($currentMemory / $memoryLimit) * 100; if ($memoryPercent > 80) { $currentFormatted = $this->formatBytes($currentMemory); $limitFormatted = $this->formatBytes($memoryLimit); $this->logger->warning("High memory usage detected in {$operation}", [ 'identifier' => $identifier, 'current_memory' => $currentFormatted, 'memory_limit' => $limitFormatted, 'usage_percent' => round($memoryPercent, 2) ]); // Garbage Collection bei hohem Memory-Verbrauch if ($memoryPercent > 90) { $this->logger->warning("Critical memory usage - forcing garbage collection"); gc_collect_cycles(); } } } private function parseMemoryLimit(string $limit): int { $limit = trim($limit); $last = strtolower($limit[strlen($limit) - 1]); $number = (int) $limit; switch ($last) { case 'g': $number *= 1024; case 'm': $number *= 1024; case 'k': $number *= 1024; } return $number; } private function formatBytes(int $bytes, int $precision = 2): string { $units = array('B', 'KB', 'MB', 'GB', 'TB'); for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { $bytes /= 1024; } return round($bytes, $precision) . ' ' . $units[$i]; } /** * Public Properties für Rückwärtskompatibilität */ public function __get(string $name) { switch ($name) { case 'date': return $this->date; case 'business_user': return $this->businessUser; case 'business_users': return $this->businessUsers; case 'parentless': return $this->parentless; default: throw new \InvalidArgumentException("Property {$name} does not exist"); } } /** * Berechnet calcQualPP() für alle BusinessUsers rekursiv * Muss NACH loadParentsUsers() aufgerufen werden, da Points benötigt werden */ private function calculateQualPPForAllUsers(): void { $this->logger->info("Starting recursive calcQualPP for all users"); $totalCalculated = 0; foreach ($this->businessUsers as $businessUser) { $totalCalculated += $this->calculateQualPPRecursive($businessUser); } $this->logger->info("Completed calcQualPP for {$totalCalculated} users"); } /** * Rekursive Hilfsmethode für calcQualPP */ private function calculateQualPPRecursive($businessUser): int { $calculated = 0; if (isset($businessUser->businessUserItems) && is_array($businessUser->businessUserItems)) { foreach ($businessUser->businessUserItems as $subBusinessUser) { if ($subBusinessUser->b_user && $subBusinessUser->b_user->user_id) { try { $subBusinessUser->calcQualPP(); $calculated++; $this->logger->debug("Calculated calcQualPP for user " . $subBusinessUser->b_user->user_id); } catch (\Exception $e) { $this->logger->warning("Error calculating calcQualPP for user " . $subBusinessUser->b_user->user_id . ": " . $e->getMessage()); } // Rekursiver Aufruf $calculated += $this->calculateQualPPRecursive($subBusinessUser); } } } return $calculated; } public function __set(string $name, $value) { switch ($name) { case 'business_users': $this->businessUsers = $value; break; case 'parentless': $this->parentless = $value; break; default: throw new \InvalidArgumentException("Property {$name} cannot be set"); } } }