date = $date; $this->treeCalcBot = $treeCalcBot; $this->businessUserItems = []; // Initialize array return $this; } public function isQualificationCalculated(): bool { return $this->qualificationCalculated; } /** * Erstellt BusinessUser aus User-ID (Original-Methode für Rückwärtskompatibilität) * * @param int $user_id Die User-ID * @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten */ public function makeUser($user_id, bool $forceLiveCalculation = false): void { try { // Prüfe nur nach gespeicherten Daten, wenn keine Live-Berechnung erzwungen wird if (! $forceLiveCalculation) { $this->b_user = UserBusiness::where('user_id', $user_id) ->where('month', $this->date->month) ->where('year', $this->date->year) ->first(); if ($this->b_user !== null) { \Log::debug("BusinessUserItem: Using stored data for user {$user_id} ({$this->date->month}/{$this->date->year})"); // WICHTIG: Auch bei gespeicherten Daten User-Model laden für Grunddaten $user = User::with(['account', 'user_level'])->find($user_id); if ($user) { $this->enrichStoredDataWithUserModel($user); // Prüfe ob Level-Qualifikationsdaten nachberechnet werden müssen if ($this->needsQualificationRecalculation) { \Log::debug("BusinessUserItem: Triggering qualification recalculation for user {$user_id}"); $this->calcQualPP(); // Berechne fehlende Level-Qualifikationsdaten } } return; // Bereits gespeicherte Daten verwenden } } else { \Log::debug("BusinessUserItem: Force live calculation for user {$user_id} ({$this->date->month}/{$this->date->year})"); } // Lade User mit Relations (weniger effizient als makeUserFromModel) $user = User::with(['account', 'user_level'])->find($user_id); if (! $user) { \Log::warning("BusinessUserItem: User not found: {$user_id}"); return; } $this->initializeFromUserModel($user); // WICHTIG: Bei Live-Berechnung auch Level-Qualifikationsdaten berechnen // (nicht bei forceLiveCalculation=false, da dort gespeicherte Daten bevorzugt werden) if ($forceLiveCalculation) { $this->calcQualPP(); } } catch (\Exception $e) { \Log::error("BusinessUserItem: Error creating user {$user_id}: ".$e->getMessage()); throw $e; } } /** * NEUE OPTIMIERTE METHODE: Erstellt BusinessUser aus bereits geladenem User-Objekt * Konsistent zur ursprünglichen makeUser Logik - prüft explizit nach bereits berechneten Daten * * @param User $user Das User-Model * @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten */ public function makeUserFromModel(User $user, bool $forceLiveCalculation = false): void { \Log::debug("BusinessUserItemOptimized: makeUserFromModel for user {$user->id} ({$this->date->month}/{$this->date->year})"); try { if (! $user || ! $user->id) { throw new \InvalidArgumentException('Invalid user model provided'); } // Prüfe nur nach gespeicherten Daten, wenn keine Live-Berechnung erzwungen wird if (! $forceLiveCalculation) { $this->b_user = UserBusiness::where('user_id', $user->id) ->where('month', $this->date->month) ->where('year', $this->date->year) ->first(); if ($this->b_user !== null) { \Log::debug("BusinessUserItem: Using stored data for user {$user->id} ({$this->date->month}/{$this->date->year})"); // WICHTIG: Auch bei gespeicherten Daten User-Grunddaten anreichern $this->enrichStoredDataWithUserModel($user); // Prüfe ob Level-Qualifikationsdaten nachberechnet werden müssen if ($this->needsQualificationRecalculation) { \Log::debug("BusinessUserItem: Triggering qualification recalculation for user {$user->id}"); $this->calcQualPP(); // Berechne fehlende Level-Qualifikationsdaten } return; // Bereits berechnete Daten verwenden } } else { \Log::debug("BusinessUserItemOptimized: Force live calculation for user {$user->id} ({$this->date->month}/{$this->date->year})"); } // Erstelle neuen User und führe Live-Berechnung durch $this->initializeFromUserModel($user); // WICHTIG: Bei Live-Berechnung auch Level-Qualifikationsdaten berechnen // (nicht bei forceLiveCalculation=false, da dort gespeicherte Daten bevorzugt werden) if ($forceLiveCalculation) { // $this->calcQualPP(); } } catch (\Exception $e) { \Log::error("BusinessUserItemOptimized: Error creating user from model {$user->id}: ".$e->getMessage()); throw $e; } } /** * Initialisiert BusinessUser aus User-Model (gemeinsame Logik) */ private function initializeFromUserModel(User $user): void { // Nutze geladene Relations wenn verfügbar $user_level_active = null; if ($user->relationLoaded('user_level')) { $user_level_active = $user->user_level; } else { $user_level_active = $user->user_level; // Fallback auf Original-Relation } $this->user_level_active_pos = $user_level_active ? $user_level_active->pos : 0; // Neues UserBusiness Objekt erstellen $this->b_user = new UserBusiness; // Account-Daten (mit intelligentem Laden und Error-Handling) $account = $this->getAccountForUser($user); $fill = [ 'user_id' => $user->id, 'month' => $this->date->month, 'year' => $this->date->year, 'm_level_id' => $user->m_level, 'user_level_name' => $user_level_active ? $user_level_active->name : '', 'active_account' => $this->calculateActiveAccount($user), 'payment_account_date' => $user->payment_account ? $user->getPaymentAccountDateFormat(false) : null, 'active_date' => $user->active_date, // Account-Daten mit korrekten Fallback-Werten 'm_account' => $account ? ($account->m_account ?? null) : null, 'email' => $user->email, 'first_name' => $account ? ($account->first_name ?? '') : '', 'last_name' => $account ? ($account->last_name ?? '') : '', 'user_birthday' => $account ? $account->birthday : null, 'user_phone' => $account ? ($account->getPhoneNumber() ?? '') : '', // Sales Volume (mit Caching falls möglich) 'sales_volume_KP_points' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_KP_points'), 'sales_volume_TP_points' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_TP_points'), 'sales_volume_points_shop' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_points_shop'), 'sales_volume_points_KP_sum' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_points_KP_sum'), 'sales_volume_points_TP_sum' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_points_TP_sum'), 'sales_volume_total' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_total'), 'sales_volume_total_shop' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_total_shop'), 'sales_volume_total_sum' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_total_sum'), // Level-Daten mit Boundary-Checks 'margin' => $user_level_active ? max(0, $user_level_active->margin) : 0, 'margin_shop' => $user_level_active ? max(0, $user_level_active->margin_shop) : 0, 'qual_kp' => $user_level_active ? max(0, $user_level_active->qual_kp) : 0, 'qual_pp' => $user_level_active ? max(0, $user_level_active->qual_pp) : 0, 'active_growth_bonus' => $user_level_active ? (float) $user_level_active->growth_bonus : 0, 'growth_bonus_details' => null, // Initialisierung 'payline_points' => 0, 'commission_pp_total' => 0, 'commission_shop_sales' => 0, 'commission_growth_total' => 0, 'version' => 2, ]; $this->b_user->fill($fill); $this->b_user->business_lines = []; $this->b_user->user_items = []; // Shop-Provision berechnen (mit verbessertem Logging) $shopVolume = (float) $this->b_user->sales_volume_total_shop; $shopMargin = (float) $this->b_user->margin_shop; $calculatedCommission = round($shopVolume / 100 * $shopMargin, 2); $this->b_user->commission_shop_sales = $calculatedCommission; \Log::debug("BusinessUserItem: Created optimized user {$user->id} for {$this->date->month}/{$this->date->year} - Shop commission: {$calculatedCommission} (Volume: {$shopVolume}, Margin: {$shopMargin}%)"); \Log::debug('BusinessUserItemOptimized: b_user: '.json_encode($this->b_user)); } /** * Ergänzt gespeicherte UserBusiness-Daten mit aktuellen User-Grunddaten * Erweitert um Level-Qualifikationsdaten-Validierung für Struktur-Ansicht */ private function enrichStoredDataWithUserModel(User $user): void { try { $account = $this->getAccountForUser($user); // Ergänze fehlende User-Grunddaten in gespeicherten UserBusiness-Daten $this->b_user->user_id = $user->id; $this->b_user->email = $user->email; $this->b_user->first_name = $account ? ($account->first_name ?? '') : ''; $this->b_user->last_name = $account ? ($account->last_name ?? '') : ''; $this->b_user->user_birthday = $account ? $account->birthday : null; $this->b_user->user_phone = $account ? ($account->getPhoneNumber() ?? '') : ''; $this->b_user->m_account = $account ? ($account->m_account ?? null) : null; // Berechne aktiven Account-Status $this->b_user->active_account = $this->calculateActiveAccount($user); $this->b_user->payment_account_date = $user->payment_account; // User-Level Informationen $user_level_active = $user->user_level; if ($user_level_active) { $this->b_user->user_level_name = $user_level_active->name; $this->user_level_active_pos = $user_level_active->pos; } // WICHTIG: Validiere Level-Qualifikationsdaten für Struktur-Ansicht $this->validateLevelQualificationData(); // Prüfe ob Sales Volume Felder aktualisiert werden müssen $this->updateSalesVolumeFields($user); \Log::debug("BusinessUserItem: Enriched stored data for user {$user->id} with current user model data"); } catch (\Exception $e) { \Log::error("BusinessUserItem: Error enriching stored data for user {$user->id}: ".$e->getMessage()); } } /** * Aktualisiert Sales Volume und Commission Felder bei gespeicherten Daten */ private function updateSalesVolumeFields(User $user): void { try { // Prüfe ob Sales Volume Felder leer sind $fieldsToUpdate = [ 'sales_volume_KP_points', 'sales_volume_TP_points', 'sales_volume_points_shop', 'sales_volume_points_KP_sum', 'sales_volume_points_TP_sum', 'sales_volume_total', 'sales_volume_total_shop', 'sales_volume_total_sum', ]; $needsUpdate = false; foreach ($fieldsToUpdate as $field) { if (! isset($this->b_user->{$field}) || $this->b_user->{$field} === null || $this->b_user->{$field} === 0) { $newValue = $this->getUserSalesVolumeOptimized($user, $field); $this->b_user->{$field} = $newValue; if ($newValue > 0) { $needsUpdate = true; \Log::debug("BusinessUserItem: Updated {$field} for user {$user->id}: {$newValue}"); } } } // Aktualisiere Shop Commission falls nötig if (! isset($this->b_user->commission_shop_sales) || $this->b_user->commission_shop_sales === 0) { $shopVolume = (float) $this->b_user->sales_volume_total_shop; $shopMargin = (float) $this->b_user->margin_shop; if ($shopVolume > 0 && $shopMargin > 0) { $calculatedCommission = round($shopVolume / 100 * $shopMargin, 2); $this->b_user->commission_shop_sales = $calculatedCommission; $needsUpdate = true; \Log::debug("BusinessUserItem: Updated commission_shop_sales for user {$user->id}: {$calculatedCommission}"); } } if ($needsUpdate) { \Log::info("BusinessUserItem: Updated sales volume fields for user {$user->id}"); } } catch (\Exception $e) { \Log::error("BusinessUserItem: Error updating sales volume fields for user {$user->id}: ".$e->getMessage()); } } /** * Validiert und aktualisiert Level-Qualifikationsdaten wenn nötig * Stellt sicher, dass next_qual_user_level und next_can_user_level für Struktur-Ansicht verfügbar sind */ private function validateLevelQualificationData(): void { try { // Prüfe ob Level-Qualifikationsdaten vorhanden sind $hasNextQual = ! empty($this->b_user->next_qual_user_level); $hasNextCan = ! empty($this->b_user->next_can_user_level); $hasQualUserLevel = ! empty($this->b_user->qual_user_level); // Wenn Level-Qualifikationsdaten fehlen, führe Neuberechnung durch if (! $hasNextQual && ! $hasNextCan && ! $hasQualUserLevel) { \Log::debug("BusinessUserItem: Level qualification data missing for user {$this->b_user->user_id}, triggering recalculation"); // Setze Flag für notwendige Neuberechnung $this->needsQualificationRecalculation = true; } } catch (\Exception $e) { \Log::warning("BusinessUserItem: Error validating level qualification data for user {$this->b_user->user_id}: ".$e->getMessage()); } } /** * Berechnet ob Account aktiv ist (mit Error-Handling) */ private function calculateActiveAccount(User $user): bool { try { if (! $user->payment_account) { return false; } // Verwende aktuelles Datum, nicht das Berechnungs-Startdatum return Carbon::parse($user->payment_account)->gt(Carbon::now()); } catch (\Exception $e) { \Log::warning("BusinessUserItem: Error calculating active account for user {$user->id}: ".$e->getMessage()); return false; } } /** * Optimierte Sales Volume Abfrage mit detailliertem Logging */ private function getUserSalesVolumeOptimized(User $user, string $field) { try { // Direkter Aufruf mit detailliertem Logging $value = $user->getUserSalesVolumeBy($this->date->month, $this->date->year, $field); // Log nur bei ersten Aufruf für diesen User (Performance) static $loggedUsers = []; if (! isset($loggedUsers[$user->id])) { $loggedUsers[$user->id] = true; // Prüfe ob UserSalesVolume Daten existieren $userSalesVolume = $user->getUserSalesVolume($this->date->month, $this->date->year, 'first'); if (! $userSalesVolume) { \Log::info("BusinessUserItem: No UserSalesVolume found for user {$user->id} in {$this->date->month}/{$this->date->year}"); // Prüfe neueste verfügbare Daten $latestVolume = \App\Models\UserSalesVolume::where('user_id', $user->id) ->orderBy('year', 'desc') ->orderBy('month', 'desc') ->first(); if ($latestVolume) { \Log::info("BusinessUserItem: Latest UserSalesVolume for user {$user->id}: {$latestVolume->month}/{$latestVolume->year}"); } else { \Log::warning("BusinessUserItem: No UserSalesVolume records found for user {$user->id} at all"); } } else { \Log::debug("BusinessUserItem: UserSalesVolume found for user {$user->id} in {$this->date->month}/{$this->date->year}"); } } return $value; } catch (\Exception $e) { \Log::error("BusinessUserItem: Error getting sales volume {$field} for user {$user->id}: ".$e->getMessage()); return 0; // Sicherer Fallback } } // ===== ORIGINALE METHODEN (unverändert für Kompatibilität) ===== public function getSalesVolumeTotalMargin() { return $this->b_user->getSalesVolumeTotalMargin(); } public function addUserID() { if ($this->treeCalcBot) { $this->treeCalcBot->addProcessedUserId($this->b_user->user_id); } else { // Fallback für Rückwärtskompatibilität - sollte in Logs sichtbar sein \Log::warning('BusinessUserItemOptimized: TreeCalcBotOptimized Referenz fehlt für User ID: '.$this->b_user->user_id); } } public function getBUser() { return $this->b_user; } public function addBusinessLineToUser($line, $obj) { $this->b_user->business_lines[$line] = $obj; } /** * Initialisiert leere business_lines für diesen User */ public function initBusinessLines(): void { if (! isset($this->b_user->business_lines) || ! is_array($this->b_user->business_lines)) { $this->b_user->business_lines = []; } } /** * Prüft ob eine business_line existiert */ public function hasBusinessLine(int $line): bool { return isset($this->b_user->business_lines[$line]); } public function addBusinessLinePoints($line, $points) { if (! isset($this->b_user->business_lines[$line])) { \Log::warning("BusinessUserItem: Trying to add points to non-existent line {$line}"); return; } $obj = $this->b_user->business_lines[$line]; // Handle both array and object types (JSON deserialization inconsistency) if (is_array($obj)) { $obj['points'] = ($obj['points'] ?? 0) + (float) $points; } else { // Ensure it's an object if (! is_object($obj)) { $obj = (object) $obj; } $obj->points = ($obj->points ?? 0) + (float) $points; } $this->b_user->business_lines[$line] = $obj; } /** * Gibt Details zur Growth Bonus Berechnung zurück (für die View) * Nur für Monate ab November 2025 verfügbar (neue Logik) */ public function getGrowthBonusBreakdown(): array { // Prüfe ob Legacy-Monat (vor November 2025) $isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11); if ($isLegacy) { return []; } if (! $this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) { return []; } try { $calculator = new GrowthBonusCalculator; // Array zu Object konvertieren für Calculator $qualData = (object) $this->b_user->qual_user_level; return $calculator->getCalculationDetails($this, $qualData); } catch (\Exception $e) { \Log::error('BusinessUserItem: Error getting growth bonus breakdown: '.$e->getMessage()); return []; } } /** * Gibt Matrix-Details zur Growth Bonus Berechnung zurück (für die View) * Nur für Monate ab November 2025 verfügbar (neue Logik) */ public function getGrowthBonusMatrix(): array { // Prüfe ob Legacy-Monat (vor November 2025) $isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11); if ($isLegacy) { return []; } if (! $this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) { return []; } // Use stored details if available (avoid recalculation) if (! empty($this->b_user->growth_bonus_details)) { if (is_object($this->b_user->growth_bonus_details) && method_exists($this->b_user->growth_bonus_details, 'toArray')) { return $this->b_user->growth_bonus_details->toArray(); } if (is_array($this->b_user->growth_bonus_details)) { return $this->b_user->growth_bonus_details; } // Fallback for standard object if (is_object($this->b_user->growth_bonus_details)) { return json_decode(json_encode($this->b_user->growth_bonus_details), true); } } try { $calculator = new GrowthBonusCalculator; // Array zu Object konvertieren für Calculator $qualData = (object) $this->b_user->qual_user_level; return $calculator->getMatrixDetails($this, $qualData); } catch (\Exception $e) { \Log::error('BusinessUserItem: Error getting growth bonus matrix: '.$e->getMessage()); return []; } } public function addTotalTP($points) { $this->b_user->total_pp += (float) $points; // Type-Safety } public function isQualKP(): bool { return $this->b_user->sales_volume_points_KP_sum >= $this->b_user->qual_kp; } public function isQualLevel(): bool { return ! empty($this->b_user->qual_user_level); } /** * Methode für Zugriff auf qual_user_level (auch für GrowthBonusCalculator) */ public function getQualUserLevel() { return $this->b_user->qual_user_level ?? null; } public function getActiveGrowthBonus() { return $this->active_growth_bonus; } /** * Gibt das Date-Objekt zurück (für GrowthBonusCalculator) */ public function getDate() { return $this->date; } /** * Gibt den Growth Bonus basierend auf dem ERREICHTEN Qualifikations-Level zurück. * * WICHTIG: Diese Methode gibt den Growth Bonus nur zurück, wenn der Partner * in dem Monat tatsächlich das entsprechende Level qualifiziert hat. * Das ist entscheidend für die korrekte Differenz-Berechnung im GrowthBonusCalculator. * * Die Methode funktioniert sowohl für: * - Live-berechnete Daten (qualificationCalculated = true) * - Gespeicherte/geladene Daten aus UserBusiness (qual_user_level bereits vorhanden) * * @return float Der Growth Bonus des erreichten Qualifikations-Levels (0 wenn nicht qualifiziert) */ public function getQualifiedGrowthBonus(): float { // Prüfen ob b_user existiert if (empty($this->b_user)) { return 0.0; } // Prüfen ob ein Qualifikations-Level erreicht wurde // Dies funktioniert sowohl für live-berechnete als auch für gespeicherte Daten if (empty($this->b_user->qual_user_level)) { return 0.0; } // Handle array und object Zugriff (JSON-Deserialisierung kann beides liefern) $qualLevel = $this->b_user->qual_user_level; if (is_array($qualLevel)) { return (float) ($qualLevel['growth_bonus'] ?? 0.0); } if (is_object($qualLevel)) { return (float) ($qualLevel->growth_bonus ?? 0.0); } return 0.0; } public function isQualEqualLevel(): bool { if (! $this->b_user->qual_user_level) { return false; } return $this->b_user->m_level_id == $this->b_user->qual_user_level['id']; } public function getQualPaylines(): int { if (! $this->b_user->qual_user_level) { return 0; } return (int) $this->b_user->qual_user_level['paylines']; } public function getRestQualKP(): float { $ret = $this->b_user->sales_volume_points_KP_sum - $this->b_user->qual_kp; return max(0, $ret); // Boundary-Check } public function getCommissionTotal(): float { return round( $this->b_user->commission_shop_sales + $this->b_user->commission_pp_total + $this->b_user->commission_growth_total, 2 ); } // ===== PROVISIONSBERECHNUNG (Original-Logik) ===== public function calcQualPP($force = false): void { if ($this->qualificationCalculated && ! $force) { return; } // Mark as calculated immediately to prevent potential recursion loops $this->qualificationCalculated = true; try { $qualUserLevel = $this->calcuQualLevel(); \Log::debug("BusinessUserItemOptimized: calcQualPP for user {$this->b_user->user_id}: ".json_encode($qualUserLevel)); if ($qualUserLevel !== null) { // das erreichte level setzen $this->b_user->qual_user_level = $qualUserLevel->toArray(); // Wichtig: Setze die qual_kp und qual_pp des erreichten Levels im b_user Objekt // Diese Werte ändern sich je nach erreichtem Level und müssen hier aktualisiert werden $this->b_user->qual_kp = $qualUserLevel->qual_kp; $this->b_user->qual_pp = $qualUserLevel->qual_pp; \Log::debug("BusinessUserItemOptimized: Set qual_kp={$qualUserLevel->qual_kp}, qual_pp={$qualUserLevel->qual_pp} for user {$this->b_user->user_id}"); // next_qual_user_level nächster qualifizierten level $this->setNextUserLevel($force); // qual_user_level_next nächste Provisions-Stufe, $this->setQualNextLevel($force); // provisionen berechnen $this->calculateCommissions($qualUserLevel); } else { $this->setFirstQualLevel(); } } catch (\Exception $e) { \Log::error("BusinessUserItem: Error calculating qualifications for user {$this->b_user->user_id}: ".$e->getMessage()); } } /** * Berechnet Provisionen mit Error-Handling * Erweitert um Array/Object-Kompatibilität für business_lines */ private function calculateCommissions($qualUserLevel): void { $commission_pp_total = 0; $commission_growth_total = 0; // Payline-Provisionen for ($i = 1; $i <= $qualUserLevel->paylines; $i++) { if (isset($this->b_user->business_lines[$i])) { $object = $this->b_user->business_lines[$i]; $margin = (float) $this->b_user->qual_user_level['pr_line_'.$i]; // Handle both array and object types (JSON deserialization inconsistency) if (is_array($object)) { $points = (float) ($object['points'] ?? 0); $object['margin'] = $margin; $object['commission'] = round($points / 100 * $margin, 2); $object['payline'] = true; $commission_pp_total += $object['commission']; } else { $points = (float) ($object->points ?? 0); $object->margin = $margin; $object->commission = round($points / 100 * $margin, 2); $object->payline = true; $commission_pp_total += $object->commission; } $this->b_user->business_lines[$i] = $object; } } // Growth Bonus if (! empty($qualUserLevel->growth_bonus)) { // Fallback für alte Monate (vor November 2025) // Stichtag: 01.11.2025 - Alles davor nutzt die Legacy-Berechnung $isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11); if ($isLegacy) { $commission_growth_total = $this->calculateLegacyGrowthBonus($qualUserLevel); \Log::debug("BusinessUserItem: Used LEGACY growth bonus calculation for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year})"); } else { // Neue Logik ab Dezember 2025 - delegated to new Calculator service try { $growthCalculator = new GrowthBonusCalculator; $commission_growth_total = $growthCalculator->calculate($this, $qualUserLevel); // Calculate matrix details for storage and total sum // This ensures that the stored details match the calculated total exactly $matrixDetails = $growthCalculator->getMatrixDetails($this, $qualUserLevel); // Store details in the model so they can be retrieved later without recalculation $this->b_user->growth_bonus_details = $matrixDetails; } catch (\Exception $e) { \Log::error("BusinessUserItem: Error calculating growth bonus for user {$this->b_user->user_id}: ".$e->getMessage()); // Fallback to 0 if calculation fails $commission_growth_total = 0; $this->b_user->growth_bonus_details = null; } } } $this->b_user->commission_pp_total = $commission_pp_total; $this->b_user->commission_growth_total = $commission_growth_total; } /** * Alte Berechnungsmethode für Growth Bonus (Kompatibilität für vergangene Monate) * Berechnet pauschal ab einer bestimmten Ebene ohne Differenz-Prüfung */ private function calculateLegacyGrowthBonus($qualUserLevel): float { $commission_growth_total = 0; // Payline aus Level-Daten + 1 (Start des Bonus) $payline = (int) ($this->b_user->qual_user_level['paylines'] ?? 0) + 1; $maxlines = count($this->b_user->business_lines ?? []) + 1; $growth_bonus = (float) ($this->b_user->qual_user_level['growth_bonus'] ?? 0); for ($i = $payline; $i <= $maxlines; $i++) { if (isset($this->b_user->business_lines[$i])) { $object = $this->b_user->business_lines[$i]; // Handle both array and object types if (is_array($object)) { $points = (float) ($object['points'] ?? 0); $object['margin'] = $growth_bonus; $object['commission'] = round($points / 100 * $growth_bonus, 2); $object['growth_bonus'] = true; $commission_growth_total += $object['commission']; } else { if (! is_object($object)) { $object = (object) $object; } $points = (float) ($object->points ?? 0); $object->margin = $growth_bonus; $object->commission = round($points / 100 * $growth_bonus, 2); $object->growth_bonus = true; $commission_growth_total += $object->commission; } $this->b_user->business_lines[$i] = $object; } } return $commission_growth_total; } // ===== WEITERE ORIGINAL-METHODEN (gekürzt, vollständige Implementation in Original) ===== /** * Berechnet das aktuell erreichte Level * Durchläuft alle möglichen Levels (max. bis zur eigenen User-Level-Position) * und prüft dynamisch die Qualifikation basierend auf den spezifischen qual_kp und qual_pp des jeweiligen Levels */ public function calcuQualLevel() { \Log::debug("BusinessUserItemOptimized: calcuQualLevel for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year})"); // Hole alle möglichen Levels bis zur eigenen Position, sortiert nach Position absteigend // um vom höchsten zum niedrigsten zu prüfen $qualUserLevels = UserLevel::where('qual_kp', '<=', $this->b_user->sales_volume_points_KP_sum) ->where('pos', '<=', $this->user_level_active_pos) ->orderBy('pos', 'desc') // Sortiere nach Position DESC, um das höchste Level zuerst zu prüfen ->get(); foreach ($qualUserLevels as $qualUserLevel) { // Berechne die Payline-Punkte für die spezifischen Paylines dieses Levels $payline_points = $this->getPointsforPayline($qualUserLevel->paylines); \Log::debug('BusinessUserItemOptimized: payline_points: '.$payline_points); // WICHTIG: Berechne die Rest-KP basierend auf der qual_kp DES AKTUELL GEPRÜFTEN LEVELS // nicht der qual_kp des bereits gesetzten Levels (das war der Fehler!) $rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevel->qual_kp); $payline_points_qual_kp = $payline_points + $rest_kp; // Prüfe ob die Qualifikation für diesen spezifischen Level erfüllt ist if ($payline_points_qual_kp >= $qualUserLevel->qual_pp) { // Setze die berechneten Werte $this->b_user->calc_qual_kp = $rest_kp > 0 ? $qualUserLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum; $this->b_user->payline_points = $payline_points; $this->b_user->payline_points_qual_kp = $payline_points_qual_kp; $qualUserLevel->_calculated_qual_kp = $rest_kp > 0 ? $qualUserLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum; $qualUserLevel->_calculated_payline_points = $payline_points; $qualUserLevel->_calculated_payline_points_qual_kp = $payline_points_qual_kp; \Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} qualifies for level {$qualUserLevel->name} (pos: {$qualUserLevel->pos}) - Payline Points: {$payline_points}, Rest KP: {$rest_kp}, Total: {$payline_points_qual_kp} >= {$qualUserLevel->qual_pp}"); return $qualUserLevel; } } \Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not qualify for any level"); return null; } private function getPointsforPayline($paylines): float { \Log::debug("BusinessUserItemOptimized: getPointsforPayline for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year}) with paylines: ".$paylines.' and business_lines: '.json_encode($this->b_user->business_lines)); $payline_points = 0; for ($i = 1; $i <= $paylines; $i++) { if (isset($this->b_user->business_lines[$i])) { $line = $this->b_user->business_lines[$i]; // Handle both array and object types (JSON deserialization inconsistency) if (is_array($line)) { $payline_points += (float) ($line['points'] ?? 0); } else { $payline_points += (float) ($line->points ?? 0); } } } return $payline_points; } /** * Setzt das nächste Provision-Level * Wenn das aktuelle Level nicht erreicht ist, dann wird bei aktuelle Provisions-Stufe die erreichte level angezeigt und berechnet * Zur Info wird das nächste level angezeigt, der folgt, sonst leer */ private function setQualNextLevel($force = false): void { // ist der level nicht das aktuelle level, dann sucht es den nächsten level // isQualEqualLevel wenn das erreichte level das akutelle user level ist. if (! $this->isQualEqualLevel() && $this->b_user->qual_user_level['next_id'] != null) { $qualUserLevelNext = UserLevel::where('id', '=', $this->b_user->qual_user_level['next_id']) ->orderBy('qual_pp', 'asc') ->first(); if ($qualUserLevelNext) { // Berechne die spezifischen Werte für diesen Level $payline_points = $this->getPointsforPayline($qualUserLevelNext->paylines); $rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevelNext->qual_kp); $payline_points_qual_kp = $payline_points + $rest_kp; // Speichere Level-Daten mit berechneten Werten $levelData = $qualUserLevelNext->toArray(); $levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $qualUserLevelNext->qual_kp : $this->b_user->sales_volume_points_KP_sum; $levelData['_calculated_payline_points'] = $payline_points; $levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp; $this->b_user->qual_user_level_next = $levelData; } else { $this->b_user->qual_user_level_next = null; } } else { $this->b_user->qual_user_level_next = null; } } private function setNextUserLevel($force = false): void { // Hole nur den direkt nächsten Level (keine Level überspringen!) $nextLevel = UserLevel::where('pos', '=', $this->user_level_active_pos + 1) ->first(); // Wenn kein nächster Level existiert, beende if (! $nextLevel) { $this->b_user->next_qual_user_level = null; $this->b_user->next_can_user_level = null; \Log::debug("BusinessUserItemOptimized: No next level found for user {$this->b_user->user_id} (already at highest level)"); return; } // Berechne die Payline-Punkte für die spezifischen Paylines des nächsten Levels $payline_points = $this->getPointsforPayline($nextLevel->paylines); // Berechne die Rest-KP basierend auf dem nächsten Level $rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $nextLevel->qual_kp); $payline_points_qual_kp = $payline_points + $rest_kp; // Erstelle Level-Daten mit berechneten Werten $levelData = $nextLevel->toArray(); $levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $nextLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum; $levelData['_calculated_payline_points'] = $payline_points; $levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp; // Prüfe die KP-Qualifikation für den nächsten Level if ($this->b_user->sales_volume_points_KP_sum < $nextLevel->qual_kp) { // KP-Qualifikation nicht erfüllt - zeige als "next_can_user_level" $this->b_user->next_can_user_level = $levelData; $this->b_user->next_qual_user_level = null; \Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not meet KP requirement for next level {$nextLevel->name} ({$this->b_user->sales_volume_points_KP_sum} < {$nextLevel->qual_kp})"); return; } // Prüfe ob die PP-Qualifikation erfüllt ist if ($payline_points_qual_kp >= $nextLevel->qual_pp) { // Qualifiziert für den nächsten Level $this->b_user->next_qual_user_level = $levelData; $this->b_user->next_can_user_level = null; \Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} qualifies for next level {$nextLevel->name} (Payline Points: {$payline_points}, Rest KP: {$rest_kp}, Total: {$payline_points_qual_kp} >= {$nextLevel->qual_pp})"); } else { // PP-Qualifikation nicht erfüllt - zeige als "next_can_user_level" $this->b_user->next_can_user_level = $levelData; $this->b_user->next_qual_user_level = null; \Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not meet PP requirement for next level {$nextLevel->name} ({$payline_points_qual_kp} < {$nextLevel->qual_pp})"); } } private function setFirstQualLevel(): void { $qualUserLevelNext = UserLevel::where('pos', '=', 1) ->orderBy('qual_pp', 'asc') ->first(); if ($qualUserLevelNext) { $payline_points = $this->getPointsforPayline($qualUserLevelNext->paylines); // Berechne die Rest-KP basierend auf dem nächsten Level $rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevelNext->qual_kp); $payline_points_qual_kp = $payline_points + $rest_kp; $levelData = $qualUserLevelNext->toArray(); $levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $qualUserLevelNext->qual_kp : $this->b_user->sales_volume_points_KP_sum; $levelData['_calculated_payline_points'] = $payline_points; $levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp; $this->b_user->qual_user_level_next = $levelData; } } // Magic Methods für Property-Zugriff (Rückwärtskompatibilität) public function __get($name) { if (isset($this->b_user->$name)) { return $this->b_user->$name; } // Legacy-Properties $legacyMap = [ 'sales_volume_points_KP_sum' => 'sales_volume_points_KP_sum', 'sales_volume_points_TP_sum' => 'sales_volume_points_TP_sum', 'business_lines' => 'business_lines', 'user_id' => 'user_id', ]; if (isset($legacyMap[$name]) && isset($this->b_user->{$legacyMap[$name]})) { return $this->b_user->{$legacyMap[$name]}; } return null; } /** * Prüft und setzt Sponsor-Informationen (Original-Implementation) */ public function checkSponsor($user): void { try { // Check if already stored if ($this->isSave()) { return; } $sponsor = new stdClass; $sponsor->is_sponsor = false; $sponsor->user_id = false; $sponsor->first_name = ''; $sponsor->last_name = ''; $sponsor->email = ''; $sponsor->m_account = ''; $sponsor->full_name = 'Keinen Sponsor zugewiesen'; if ($user->m_sponsor) { if ($user->user_sponsor) { $sponsor->is_sponsor = true; $sponsor->user_id = $user->user_sponsor->id; if ($user->user_sponsor->account) { $sponsor->full_name = substr( 'Sponsor: '.$user->user_sponsor->account->first_name.' '. $user->user_sponsor->account->last_name.' | '. $user->user_sponsor->email.' | '. $user->user_sponsor->account->m_account, 0, 250 ); $sponsor->first_name = $user->user_sponsor->account->first_name; $sponsor->last_name = $user->user_sponsor->account->last_name; $sponsor->m_account = $user->user_sponsor->account->m_account; } else { $sponsor->full_name = 'Sponsor: '.$user->user_sponsor->email; } $sponsor->email = $user->user_sponsor->email; } else { $sponsor->full_name = 'Sponsor wurde gelöscht.'; } } $this->b_user->sponsor = $sponsor; } catch (\Exception $e) { Log::error("BusinessUserItem: Error checking sponsor for user {$user->id}: ".$e->getMessage()); } } /** * Lädt Parent Business Users rekursiv (Original-Implementation mit Optimierungen) * BUGFIX: Schutz vor unendlicher Rekursion durch zirkuläre Referenzen */ public function readParentsBusinessUsers($forceLiveCalculation = false, $depth = 0): void { // Schutz vor zu tiefer Rekursion (maximale Tiefe: 20 Levels) $maxDepth = 20; if ($depth > $maxDepth) { Log::warning("BusinessUserItem: Maximale Rekursionstiefe ({$maxDepth}) erreicht für User {$this->b_user->user_id}"); return; } try { // Optimiert: Lade mit Relations $users = User::with(['account']) ->select('users.*') ->where('users.deleted_at', '=', null) ->where('users.id', '!=', 1) ->where('users.admin', '<', 4) ->where('users.m_level', '!=', null) ->whereColumn('users.id', '!=', 'users.m_sponsor') ->where('users.m_sponsor', '=', $this->b_user->user_id) ->where('users.payment_account', '!=', null) ->where('users.active_date', '<=', $this->date->end_date) ->get(); if ($users->isNotEmpty()) { foreach ($users as $user) { // KRITISCHER BUGFIX: Prüfe ob User bereits verarbeitet wurde if ($this->treeCalcBot && $this->treeCalcBot->isUserProcessed($user->id)) { Log::debug("BusinessUserItem: Überspringe bereits verarbeiteten User {$user->id} (zirkuläre Referenz verhindert)"); continue; } $businessUserItem = new BusinessUserItemOptimized($this->date, $this->treeCalcBot); $businessUserItem->makeUserFromModel($user, $forceLiveCalculation); $businessUserItem->addUserID(); $this->businessUserItems[] = $businessUserItem; } } // Rekursiver Aufruf für alle Child-Items mit Tiefenprüfung foreach ($this->businessUserItems as $businessUserItem) { $businessUserItem->readParentsBusinessUsers($forceLiveCalculation, $depth + 1); } } catch (\Exception $e) { Log::error("BusinessUserItem: Error reading parent users for {$this->b_user->user_id} at depth {$depth}: ".$e->getMessage()); } } /** * Lädt Parent Business Users aus gespeicherter Struktur (Original-Implementation) * BUGFIX: Schutz vor unendlicher Rekursion durch zirkuläre Referenzen */ public function readStoredParentsBusinessUsers($structure, $depth = 0): void { // Schutz vor zu tiefer Rekursion (maximale Tiefe: 50 Levels) $maxDepth = 50; if ($depth > $maxDepth) { Log::warning("BusinessUserItem: Maximale Rekursionstiefe ({$maxDepth}) erreicht für gespeicherte User {$this->b_user->user_id}"); return; } try { $parents = $this->findParentsBusinessOnStored($this->b_user->user_id, $structure); if ($parents) { foreach ($parents as $obj) { // KRITISCHER BUGFIX: Prüfe ob User bereits verarbeitet wurde if ($this->treeCalcBot && $this->treeCalcBot->isUserProcessed($obj->user_id)) { Log::debug("BusinessUserItem: Überspringe bereits verarbeiteten gespeicherten User {$obj->user_id} (zirkuläre Referenz verhindert)"); continue; } $businessUserItem = new BusinessUserItemOptimized($this->date, $this->treeCalcBot); $businessUserItem->makeUser($obj->user_id); $businessUserItem->addUserID(); $this->businessUserItems[] = $businessUserItem; } foreach ($this->businessUserItems as $businessUserItem) { $businessUserItem->readStoredParentsBusinessUsers($parents, $depth + 1); } } } catch (\Exception $e) { Log::error("BusinessUserItem: Error reading stored parent users at depth {$depth}: ".$e->getMessage()); } } /** * Findet Parent Business Items in gespeicherter Struktur (Original-Implementation) */ private function findParentsBusinessOnStored($user_id, $structures) { if (! $structures) { return null; } foreach ($structures as $obj) { if ($user_id === $obj->user_id) { return $obj->parents ?? null; } if (! empty($obj->parents)) { $result = $this->findParentsBusinessOnStored($user_id, $obj->parents); if ($result) { return $result; } } } return null; } /** * Prüft ob User bereits gespeichert ist * Konsistent zur ursprünglichen BusinessUserItem Implementation */ public function isSave(): bool { return $this->b_user && $this->b_user->isSave(); } /** * Gibt die Anzahl der qualifizierten Paylines zurück */ public function getQualLevelPaylines() { if ($this->b_user && isset($this->b_user->qual_user_level) && $this->b_user->qual_user_level) { return $this->b_user->qual_user_level['paylines'] ?? 0; } return 0; } /** * Prüft ob eine Line für Growth-Bonus qualifiziert ist */ public function isQualLevelGrowth($line) { if ($this->b_user && isset($this->b_user->business_lines[$line])) { $object = $this->b_user->business_lines[$line]; if (isset($object->growth_bonus)) { return $object->growth_bonus > 0; } } return false; } /** * Intelligentes Laden des UserAccount für einen User * Prüft zuerst geladene Relations, lädt nach wenn nötig */ private function getAccountForUser(User $user): ?UserAccount { try { // Prüfe ob Account-Relation bereits geladen ist if ($user->relationLoaded('account')) { $account = $user->account; if ($account instanceof UserAccount) { \Log::debug("BusinessUserItem: Using pre-loaded account for user {$user->id}"); return $account; } } // Wenn User keine account_id hat, gibt es definitiv kein Account if (! $user->account_id) { \Log::info("BusinessUserItem: User {$user->id} has no account_id - no account available"); return null; } // Account nachladen falls nötig \Log::info("BusinessUserItem: Loading account for user {$user->id} (account_id: {$user->account_id})"); $account = UserAccount::find($user->account_id); if (! $account) { \Log::warning("BusinessUserItem: Account {$user->account_id} not found for user {$user->id}"); return null; } \Log::debug("BusinessUserItem: Successfully loaded account {$account->id} for user {$user->id}"); return $account; } catch (\Exception $e) { \Log::error("BusinessUserItem: Error loading account for user {$user->id}: ".$e->getMessage()); return null; } } }