sendUpdateMail; } /** * Setter für sendUpdateMail (für Tests) */ public function setSendUpdateMail(bool $sendUpdateMail): void { $this->sendUpdateMail = $sendUpdateMail; } /** * Prüft ob der Command heute ausgeführt werden soll * * WICHTIG: Diese Methode verhindert, dass der Command täglich läuft! * Der Command sollte nur am konfigurierten Tag des Monats laufen. * * @return bool True wenn Command ausgeführt werden soll, False sonst */ private function shouldExecuteToday(): bool { // Hole konfigurierten Ausführungstag (Standard: 1 = Monatserster) $executeDay = (int) Setting::getContentBySlug('day-exectute-business-structur'); // Fallback: Wenn Setting leer oder 0, verwende Tag 1 if ($executeDay === 0) { $executeDay = 1; $this->warn('Setting "day-exectute-business-structur" ist leer oder 0. Verwende Standard: Tag 1'); \Log::channel('cron')->warning('BusinessStoreOptimized: Setting day-exectute-business-structur is empty, using default: 1'); } $presentDay = (int) date('d'); // Logging für Debugging $this->info("BusinessStoreOptimized: Configured Day: {$executeDay}, Present Day: {$presentDay}"); \Log::channel('cron')->info("BusinessStoreOptimized: Configured Day: {$executeDay}, Present Day: {$presentDay}"); // Prüfe ob heute der konfigurierte Tag ist if ($executeDay !== $presentDay) { // Erlaubnis zum Überschreiben für Entwicklung/Testing // ENV-Variable BUSINESS_FORCE_EXECUTE=true überschreibt den Check if (env('BUSINESS_FORCE_EXECUTE', false) === true) { $this->warn('⚠️ BUSINESS_FORCE_EXECUTE ist aktiv - Command wird trotz falschem Tag ausgeführt!'); $this->warn('⚠️ Dies sollte NUR auf Test-Servern verwendet werden!'); \Log::channel('cron')->warning('BusinessStoreOptimized: FORCED execution via BUSINESS_FORCE_EXECUTE'); return true; } // Command sollte heute NICHT laufen $this->info("❌ Command wird NICHT ausgeführt - falscher Tag (erwartet: {$executeDay}, heute: {$presentDay})"); \Log::channel('cron')->info("BusinessStoreOptimized: NOT EXECUTED - wrong day (expected: {$executeDay}, today: {$presentDay})"); return false; } // Command wird ausgeführt $this->info("✅ Command wird ausgeführt - korrekter Tag ({$presentDay})"); \Log::channel('cron')->info("BusinessStoreOptimized: EXECUTING - correct day ({$presentDay})"); return true; } /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return int */ public function handle() { try { // Prüfe ob Command am richtigen Tag ausgeführt werden soll if (! $this->shouldExecuteToday()) { return 0; } $this->logMemoryUsage('Command Start'); $this->timeStart = microtime(true); // Argumente mit Standardwerten für den Vormonat $this->month = $this->argument('month') ?: (int) date('m', strtotime('-1 month')); $this->year = $this->argument('year') ?: (int) date('Y', strtotime('-1 month')); $this->info('RUN Command BusinessStoreOptimized on month: '.$this->month.' | year: '.$this->year); $this->logMemoryUsage('Parameters initialized'); // Prüfe --clear Option und lösche gespeicherte Daten falls gewünscht if ($this->option('clear')) { $this->executeWithErrorHandling('Clear Stored Data', function () { $this->clearStoredData(); }); } // Prozesse ausführen mit optimierter Fehlerbehandlung $this->executeWithErrorHandling('Business Structure Storage', function () { \Log::channel('cron')->info('RUN Command BusinessStoreOptimized Business Structure Storage'); $this->storeBusinessStructureUsersDetailMonth(); }); $this->executeWithErrorHandling('Commission Calculation', function () { \Log::channel('cron')->info('RUN Command BusinessStoreOptimized Commission Calculation'); $this->userBusinessCommissionsToCredit(); }); $this->executeWithErrorHandling('User Level Update', function () { \Log::channel('cron')->info('RUN Command BusinessStoreOptimized User Level Update'); $this->userLevelUpdate(); }); $this->executeWithErrorHandling('Monthly Qual-KP Bonus Points', function () { \Log::channel('cron')->info('RUN Command BusinessStoreOptimized Monthly Qual-KP Bonus Points'); $this->assignMonthlyQualKpBonusPoints(); }); // Auskommentierte Prozesse bleiben inaktiv // $this->userCreatePaymentCreditsPDF(); // $this->storeBusinessStructureUsersDetailPeriod(1, 6); $this->logExecutionTime('COMMAND COMPLETED SUCCESSFULLY'); $this->logMemoryUsage('Command End'); \Log::channel('cron')->info('COMMAND COMPLETED SUCCESSFULLY'); return 0; } catch (\Exception $e) { $this->error('Command failed with error: '.$e->getMessage()); $this->error('Stack trace: '.$e->getTraceAsString()); $this->logExecutionTime('COMMAND FAILED'); \Log::channel('cron')->info('COMMAND FAILED'); return 1; } } private function storeBusinessStructureUsersDetailMonth() { $this->info('storeBusinessStructureUsersDetailMonth month: '.$this->month.' year:'.$this->year); try { $businessUsersStore = new BusinessUsersStoreOptimized($this->month, $this->year); $businessUsersStore->storeUserBusinessStructure(); $businessUsersStore->storeBusinessUsersDetail(); $bool = $businessUsersStore->storeBusinessCompleted(); $this->logExecutionTime('END Command storeBusinessStructureUsersDetailMonth: '.$bool); } catch (\Exception $e) { $this->error('Error in storeBusinessStructureUsersDetailMonth: '.$e->getMessage()); throw $e; } } private function userBusinessCommissionsToCredit() { $this->info('userBusinessCommissionsToCredit month: '.$this->month.' year:'.$this->year); try { $userPaymentCredits = new UserPaymentCredits($this->month, $this->year); $userBusinesses = $userPaymentCredits->getUserBusinessByMonthYear(); $processedCount = 0; foreach ($userBusinesses as $userBusiness) { $ret = $userPaymentCredits->addUserCreditItem($userBusiness); $this->info('userBusinessCredit: '.$ret->user_id.' : Team: '.$ret->commission_pp_total.' | Shop: '.$ret->commission_shop_sales); $processedCount++; // Memory-Check alle 100 User if ($processedCount % 100 === 0) { $this->logMemoryUsage("After processing {$processedCount} users"); } } $this->info("Processed {$processedCount} user businesses total"); $this->logExecutionTime('END Command userBusinessCommissionsToCredit:'); } catch (\Exception $e) { $this->error('Error in userBusinessCommissionsToCredit: '.$e->getMessage()); throw $e; } } private function userCreatePaymentCreditsPDF() { $this->info('userCreatePaymentCreditsPDF month: '.$this->month.' year:'.$this->year); try { $userPaymentCredits = new UserPaymentCredits($this->month, $this->year); $creditItemUsers = $userPaymentCredits->getUserCreditItemUsersByMonthYear(); $processedCount = 0; foreach ($creditItemUsers as $creditItemUser) { $bool = $userPaymentCredits->makeCreditPaymentPDF($creditItemUser->user_id, $this->sendCreditMail); $this->info('creditsPDF: '.$bool.' user_id: '.$creditItemUser->user_id); $processedCount++; // Memory-Check alle 50 PDFs if ($processedCount % 50 === 0) { $this->logMemoryUsage("After processing {$processedCount} PDFs"); } } $this->info("Created {$processedCount} PDF files total"); $this->logExecutionTime('END Command userCreatePaymentCreditsPDF:'); } catch (\Exception $e) { $this->error('Error in userCreatePaymentCreditsPDF: '.$e->getMessage()); throw $e; } } /** * Aktualisiert User-Level basierend auf next_qual_user_level * Kann auch von Tests aufgerufen werden */ public function userLevelUpdate() { $this->info('userLevelUpdate month: '.$this->month.' year:'.$this->year); try { $userLevelUpdate = new UserLevelUpdate($this->month, $this->year); $levelUpdateUsers = $userLevelUpdate->getUserBusinessByMonthYear(); $this->info('Found '.$levelUpdateUsers->count().' user businesses with level promotions to process'); $updatedCount = 0; $skippedCount = 0; $errorCount = 0; foreach ($levelUpdateUsers as $userBusiness) { try { $ret = $userLevelUpdate->makeUserLevelUpdate($userBusiness, $this->sendUpdateMail); if ($ret) { $oldLevel = $userBusiness->m_level_id.' '.($userBusiness->user_level_name ?? 'N/A'); $this->info('updateLevel: User '.$userBusiness->user->id. ' | '.$userBusiness->user->email. ' | from: '.$oldLevel. ' | to: '.$ret); $updatedCount++; } else { $skippedCount++; } // Memory-Check alle 50 User if (($updatedCount + $skippedCount) % 50 === 0) { $this->logMemoryUsage('After processing '.($updatedCount + $skippedCount).' users'); } } catch (\Exception $e) { $errorCount++; $this->warn('Error updating level for UserBusiness '.$userBusiness->id.': '.$e->getMessage()); \Log::channel('cron')->warning('UserLevelUpdate error for UserBusiness '.$userBusiness->id.': '.$e->getMessage()); // Weiter mit nächstem User statt abzubrechen continue; } } $this->info("Level update completed: {$updatedCount} updated, {$skippedCount} skipped, {$errorCount} errors"); $this->logExecutionTime('END Command userLevelUpdate:'); $this->logMemoryUsage('After userLevelUpdate'); } catch (\Exception $e) { $this->error('Error in userLevelUpdate: '.$e->getMessage()); $this->error('Stack trace: '.$e->getTraceAsString()); \Log::channel('cron')->error('UserLevelUpdate command failed: '.$e->getMessage()); throw $e; } } private function storeBusinessStructureUsersDetailPeriod($from, $to) { try { for ($i = $from; $i <= $to; $i++) { $this->info('Store Business Structure Users Detail month: '.$i.' year:'.$this->year); $this->logMemoryUsage("Before month {$i}"); $businessUsersStore = new BusinessUsersStoreOptimized($i, $this->year); $businessUsersStore->storeUserBusinessStructure(); $businessUsersStore->storeBusinessUsersDetail(); $bool = $businessUsersStore->storeBusinessCompleted(); $this->logExecutionTime('Period BusinessStore: '.$bool); $this->logMemoryUsage("After month {$i}"); } } catch (\Exception $e) { $this->error('Error in storeBusinessStructureUsersDetailPeriod: '.$e->getMessage()); throw $e; } } /** * Löscht gespeicherte Business Structure Daten für den angegebenen Monat/Jahr */ private function clearStoredData() { try { $this->info("Clearing stored business data for month: {$this->month} | year: {$this->year}"); // Finde bestehende UserBusinessStructure $existingStructure = UserBusinessStructure::where('year', $this->year) ->where('month', $this->month) ->first(); if (! $existingStructure) { $this->info('No stored business structure found to clear'); return; } $structureId = $existingStructure->id; $this->info("Found existing structure with ID: {$structureId}"); // Lösche zugehörige UserBusiness Einträge $deletedUserBusinesses = UserBusiness::where('b_structure_id', $structureId)->count(); if ($deletedUserBusinesses > 0) { UserBusiness::where('b_structure_id', $structureId)->delete(); $this->info("Deleted {$deletedUserBusinesses} UserBusiness records"); } // Lösche die UserBusinessStructure $existingStructure->delete(); $this->info("Deleted UserBusinessStructure with ID: {$structureId}"); // Garbage Collection nach dem Löschen gc_collect_cycles(); $this->info('Successfully cleared all stored business data'); $this->logMemoryUsage('After clearing data'); } catch (\Exception $e) { $this->error('Error clearing stored data: '.$e->getMessage()); throw $e; } } /** * Schreibt ausgewählten Usern einmal pro Monat ihre Level-qual_kp als KP-Bonus gut. * Idempotent: Ein bereits vorhandener Eintrag für diesen Monat/Jahr wird übersprungen. * * @var array User-IDs die den monatlichen Qual-KP-Bonus erhalten sollen */ private function assignMonthlyQualKpBonusPoints(): void { $bonusUserIds = [486]; $month = date('m'); $year = date('Y'); $users = User::query() ->whereIn('id', $bonusUserIds) ->whereNotNull('m_level') ->with('user_level') ->get() ->filter(fn (User $user) => $user->user_level && $user->user_level->qual_kp > 0); $assigned = 0; $skipped = 0; foreach ($users as $user) { $alreadyExists = UserSalesVolume::where('user_id', $user->id) ->where('month', $month) ->where('year', $year) ->where('info', 'qual_kp_bonus') ->exists(); if ($alreadyExists) { $skipped++; continue; } SalesPointsVolume::addSalesPointsVolume([ 'user_id' => $user->id, 'points' => $user->user_level->qual_kp, 'status_points' => 2, 'total_net' => 0, 'status_turnover' => 1, 'info' => 'qual_kp_bonus', ]); $assigned++; } $this->info("Qual-KP Bonus: {$assigned} zugewiesen, {$skipped} übersprungen (bereits vorhanden)"); \Log::channel('cron')->info("Qual-KP Bonus Points: assigned={$assigned}, skipped={$skipped}"); } private function logExecutionTime($message) { $diff = microtime(true) - $this->timeStart; $sec = intval($diff); $micro = $diff - $sec; $this->info($message.' | Time: '.$sec.'sec :'.round($micro * 1000, 4).' ms'); } /** * Führt eine Funktion mit Fehlerbehandlung aus */ private function executeWithErrorHandling(string $processName, callable $callback): void { try { $startTime = microtime(true); $this->info("Starting: {$processName}"); $this->logMemoryUsage("Before {$processName}"); $callback(); $endTime = microtime(true); $duration = round(($endTime - $startTime) * 1000, 2); $this->info("Completed: {$processName} in {$duration}ms"); $this->logMemoryUsage("After {$processName}"); } catch (\Exception $e) { $this->error("Error in {$processName}: ".$e->getMessage()); $this->error('Stack trace: '.$e->getTraceAsString()); throw $e; } } /** * Loggt aktuelle Memory-Nutzung */ private function logMemoryUsage(string $checkpoint): void { $currentMemory = memory_get_usage(); $peakMemory = memory_get_peak_usage(); $memoryLimit = $this->parseMemoryLimit(ini_get('memory_limit')); $currentFormatted = $this->formatBytes($currentMemory); $peakFormatted = $this->formatBytes($peakMemory); $limitFormatted = $this->formatBytes($memoryLimit); $usagePercent = round(($currentMemory / $memoryLimit) * 100, 2); $this->info("[{$checkpoint}] Memory: {$currentFormatted} / {$limitFormatted} ({$usagePercent}%) | Peak: {$peakFormatted}"); if ($usagePercent > 80) { $this->warn("High memory usage detected at {$checkpoint}: {$usagePercent}%"); } } /** * Konvertiert Memory-Limit String zu Bytes */ 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; } /** * Formatiert Bytes in lesbare Einheiten */ private function formatBytes(int $bytes, int $precision = 2): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { $bytes /= 1024; } return round($bytes, $precision).' '.$units[$i]; } }