mivita/app/Console/Commands/BusinessStoreOptimized.php
2026-04-10 17:15:27 +02:00

526 lines
19 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Cron\BusinessUsersStoreOptimized;
use App\Cron\UserLevelUpdate;
use App\Cron\UserPaymentCredits;
use App\Models\Setting;
use App\Models\UserBusiness;
use App\Models\UserBusinessStructure;
use App\Models\UserSalesVolume;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\User;
use Illuminate\Console\Command;
class BusinessStoreOptimized extends Command
{
/**
* ln -sfv /usr/bin/php73 /usr/bin/php
* php artisan business:store-optimized month year
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'business:store-optimized {month} {year} {--clear : Clear stored data before processing}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create Business Structure and UserDetails with optimized performance and monitoring';
private $timeStart;
private $month;
private $year;
private $sendCreditMail = false;
private $sendUpdateMail = false;
/**
* Getter für sendUpdateMail (für Tests)
*/
public function getSendUpdateMail(): bool
{
return $this->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<int> 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];
}
}