23-01-2026
This commit is contained in:
parent
a939cd51ef
commit
a8b395e20d
248 changed files with 29342 additions and 4805 deletions
|
|
@ -35,6 +35,22 @@ class BusinessStoreOptimized extends Command
|
|||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
|
|
@ -65,7 +81,7 @@ class BusinessStoreOptimized extends Command
|
|||
if ($executeDay !== $presentDay) {
|
||||
$this->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
|
||||
\Log::channel('cron')->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
|
||||
return 0;
|
||||
// return 0;
|
||||
}
|
||||
|
||||
$this->timeStart = microtime(true);
|
||||
|
|
@ -95,9 +111,13 @@ class BusinessStoreOptimized extends Command
|
|||
$this->userBusinessCommissionsToCredit();
|
||||
});
|
||||
|
||||
$this->executeWithErrorHandling('User Level Update', function () {
|
||||
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized User Level Update');
|
||||
$this->userLevelUpdate();
|
||||
});
|
||||
|
||||
// Auskommentierte Prozesse bleiben inaktiv
|
||||
// $this->userCreatePaymentCreditsPDF();
|
||||
// $this->userLevelUpdate();
|
||||
// $this->storeBusinessStructureUsersDetailPeriod(1, 6);
|
||||
|
||||
$this->logExecutionTime('COMMAND COMPLETED SUCCESSFULLY');
|
||||
|
|
@ -187,7 +207,11 @@ class BusinessStoreOptimized extends Command
|
|||
}
|
||||
}
|
||||
|
||||
private function userLevelUpdate()
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
|
@ -195,21 +219,46 @@ class BusinessStoreOptimized extends Command
|
|||
$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) {
|
||||
$ret = $userLevelUpdate->makeUserLevelUpdate($userBusiness, $this->sendUpdateMail);
|
||||
if ($ret) {
|
||||
$this->info('updateLevel: ' . $userBusiness->user->id . ' | ' . $userBusiness->user->email . ' | ' .
|
||||
'from: ' . $userBusiness->m_level_id . ' ' . $userBusiness->user_level_name . ' | ' .
|
||||
'to: ' . $ret);
|
||||
$updatedCount++;
|
||||
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("Updated {$updatedCount} user levels total");
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\User;
|
||||
use App\Cron\UserPaymentCredits;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Services\BusinessPlan\BusinessUserItemOptimized;
|
||||
use App\Cron\UserPaymentCredits;
|
||||
use App\Services\BusinessPlan\TreeCalcBotOptimized;
|
||||
use App\User;
|
||||
use Illuminate\Console\Command;
|
||||
use stdClass;
|
||||
|
||||
|
|
@ -74,8 +75,10 @@ class BusinessTestAccount extends Command
|
|||
$date->end_date = date('Y-m-t 23:59:59', strtotime("{$year}-{$month}-01"));
|
||||
|
||||
// Teste BusinessUserItemOptimized
|
||||
$businessUserItem = new BusinessUserItemOptimized($date);
|
||||
$businessUserItem->makeUserFromModel($user, true);
|
||||
$TreeCalcBot = new TreeCalcBotOptimized($date->month, $date->year, 'admin');
|
||||
$TreeCalcBot->initBusinesslUserDetail($user, true);
|
||||
|
||||
$businessUserItem = $TreeCalcBot->getItem();
|
||||
|
||||
$bUser = $businessUserItem->getBUser();
|
||||
|
||||
|
|
|
|||
430
app/Console/Commands/BusinessUpdateCalculatedFields.php
Normal file
430
app/Console/Commands/BusinessUpdateCalculatedFields.php
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Models\UserLevel;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BusinessUpdateCalculatedFields extends Command
|
||||
{
|
||||
/**
|
||||
* php artisan business:update-calculated-fields {year} {month?}
|
||||
*
|
||||
* Aktualisiert bereits gespeicherte UserBusiness-Einträge mit den neuen berechneten Feldern:
|
||||
* - calc_qual_kp
|
||||
* - _calculated_qual_kp in Level-Arrays
|
||||
* - _calculated_payline_points_qual_kp in Level-Arrays
|
||||
*
|
||||
* Wenn kein Monat angegeben wird, werden alle 12 Monate des Jahres aktualisiert.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'business:update-calculated-fields {year} {month? : Optional - Wenn nicht angegeben, werden alle Monate des Jahres aktualisiert} {--dry-run : Zeige nur was gemacht werden würde, ohne zu speichern}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Aktualisiert gespeicherte UserBusiness-Einträge mit neuen berechneten Feldern für Level-Qualifikationen';
|
||||
|
||||
private $timeStart;
|
||||
private $month;
|
||||
private $year;
|
||||
private $isDryRun = false;
|
||||
private $processAllMonths = false;
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$this->timeStart = microtime(true);
|
||||
$this->year = (int) $this->argument('year');
|
||||
$monthArg = $this->argument('month');
|
||||
$this->isDryRun = $this->option('dry-run');
|
||||
|
||||
if ($this->isDryRun) {
|
||||
$this->warn('DRY RUN MODE - Keine Änderungen werden gespeichert');
|
||||
}
|
||||
|
||||
// Prüfe ob ein Monat angegeben wurde
|
||||
if ($monthArg === null) {
|
||||
$this->processAllMonths = true;
|
||||
$this->info("Starte Update für ALLE MONATE des Jahres: {$this->year}");
|
||||
$this->info(str_repeat('=', 70));
|
||||
|
||||
return $this->processFullYear();
|
||||
} else {
|
||||
$this->month = (int) $monthArg;
|
||||
$this->info("Starte Update für Monat: {$this->month} | Jahr: {$this->year}");
|
||||
$this->logMemoryUsage('Command Start');
|
||||
|
||||
return $this->processSingleMonth();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Command failed with error: ' . $e->getMessage());
|
||||
$this->error('Stack trace: ' . $e->getTraceAsString());
|
||||
$this->logExecutionTime('COMMAND FAILED');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeite alle 12 Monate eines Jahres
|
||||
*/
|
||||
private function processFullYear(): int
|
||||
{
|
||||
$totalUpdated = 0;
|
||||
$totalSkipped = 0;
|
||||
$totalErrors = 0;
|
||||
$monthsProcessed = 0;
|
||||
$monthsFailed = 0;
|
||||
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$this->month = $month;
|
||||
|
||||
$this->newLine();
|
||||
$this->info("┌─────────────────────────────────────────────────────────────────────┐");
|
||||
$this->info("│ Verarbeite Monat: " . str_pad($month, 2, '0', STR_PAD_LEFT) . "/" . $this->year . str_repeat(' ', 51) . "│");
|
||||
$this->info("└─────────────────────────────────────────────────────────────────────┘");
|
||||
|
||||
try {
|
||||
$userBusinesses = $this->getUserBusinesses();
|
||||
|
||||
if ($userBusinesses->isEmpty()) {
|
||||
$this->warn(" Keine UserBusiness-Einträge für Monat {$month}/{$this->year} gefunden - überspringe");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->info(" Gefunden: {$userBusinesses->count()} UserBusiness-Einträge");
|
||||
|
||||
// Aktualisiere die Einträge
|
||||
$stats = $this->updateCalculatedFieldsWithStats($userBusinesses);
|
||||
|
||||
$totalUpdated += $stats['updated'];
|
||||
$totalSkipped += $stats['skipped'];
|
||||
$totalErrors += $stats['errors'];
|
||||
$monthsProcessed++;
|
||||
|
||||
$this->info(" ✓ Monat {$month} abgeschlossen: {$stats['updated']} aktualisiert, {$stats['skipped']} übersprungen, {$stats['errors']} Fehler");
|
||||
$this->logMemoryUsage("Nach Monat {$month}");
|
||||
} catch (\Exception $e) {
|
||||
$monthsFailed++;
|
||||
$this->error(" ✗ Fehler in Monat {$month}: " . $e->getMessage());
|
||||
\Log::error("BusinessUpdateCalculatedFields: Error in month {$month}/{$this->year}: " . $e->getMessage());
|
||||
// Weiter mit nächstem Monat
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Zusammenfassung für das ganze Jahr
|
||||
$this->newLine(2);
|
||||
$this->info(str_repeat('=', 70));
|
||||
$this->info("ZUSAMMENFASSUNG FÜR DAS JAHR {$this->year}:");
|
||||
$this->info(str_repeat('=', 70));
|
||||
$this->info("Verarbeitete Monate: {$monthsProcessed}/12");
|
||||
$this->info("Fehlgeschlagene Monate: {$monthsFailed}");
|
||||
$this->info("Gesamt aktualisiert: {$totalUpdated}");
|
||||
$this->info("Gesamt übersprungen: {$totalSkipped}");
|
||||
$this->info("Gesamt Fehler: {$totalErrors}");
|
||||
$this->info(str_repeat('=', 70));
|
||||
|
||||
$this->logExecutionTime('JAHRES-UPDATE ABGESCHLOSSEN');
|
||||
$this->logMemoryUsage('Command End');
|
||||
|
||||
return $monthsFailed > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeite einen einzelnen Monat
|
||||
*/
|
||||
private function processSingleMonth(): int
|
||||
{
|
||||
// Hole alle UserBusiness-Einträge für den Monat
|
||||
$userBusinesses = $this->getUserBusinesses();
|
||||
|
||||
if ($userBusinesses->isEmpty()) {
|
||||
$this->error("Keine UserBusiness-Einträge für Monat {$this->month}/{$this->year} gefunden");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Gefunden: {$userBusinesses->count()} UserBusiness-Einträge");
|
||||
|
||||
// Aktualisiere die Einträge
|
||||
$this->updateCalculatedFields($userBusinesses);
|
||||
|
||||
$this->logExecutionTime('UPDATE COMPLETED SUCCESSFULLY');
|
||||
$this->logMemoryUsage('Command End');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole alle UserBusiness-Einträge für den Monat
|
||||
*/
|
||||
private function getUserBusinesses()
|
||||
{
|
||||
return UserBusiness::where('month', $this->month)
|
||||
->where('year', $this->year)
|
||||
->orderBy('id', 'asc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiere die berechneten Felder für alle UserBusiness-Einträge
|
||||
*/
|
||||
private function updateCalculatedFields($userBusinesses)
|
||||
{
|
||||
$stats = $this->updateCalculatedFieldsWithStats($userBusinesses);
|
||||
|
||||
$this->info("Update abgeschlossen:");
|
||||
$this->info(" - Aktualisiert: {$stats['updated']}");
|
||||
$this->info(" - Übersprungen: {$stats['skipped']}");
|
||||
$this->info(" - Fehler: {$stats['errors']}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiere die berechneten Felder und gebe Statistiken zurück
|
||||
*/
|
||||
private function updateCalculatedFieldsWithStats($userBusinesses): array
|
||||
{
|
||||
$bar = $this->output->createProgressBar($userBusinesses->count());
|
||||
$bar->start();
|
||||
|
||||
$updatedCount = 0;
|
||||
$skippedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
foreach ($userBusinesses as $userBusiness) {
|
||||
try {
|
||||
$updated = $this->updateSingleUserBusiness($userBusiness);
|
||||
|
||||
if ($updated) {
|
||||
$updatedCount++;
|
||||
} else {
|
||||
$skippedCount++;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
|
||||
// Memory-Check alle 100 Einträge (nur wenn nicht ganzes Jahr)
|
||||
if (!$this->processAllMonths && ($updatedCount + $skippedCount) % 100 === 0) {
|
||||
$this->logMemoryUsage("Nach " . ($updatedCount + $skippedCount) . " Einträgen");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$this->newLine();
|
||||
$this->warn("Fehler bei UserBusiness ID {$userBusiness->id}: " . $e->getMessage());
|
||||
\Log::error("BusinessUpdateCalculatedFields: Error for UserBusiness {$userBusiness->id}: " . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
|
||||
return [
|
||||
'updated' => $updatedCount,
|
||||
'skipped' => $skippedCount,
|
||||
'errors' => $errorCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiere einen einzelnen UserBusiness-Eintrag
|
||||
*/
|
||||
private function updateSingleUserBusiness(UserBusiness $userBusiness): bool
|
||||
{
|
||||
$hasChanges = false;
|
||||
|
||||
// 1. Aktualisiere calc_qual_kp für qual_user_level
|
||||
if (!$userBusiness->calc_qual_kp) {
|
||||
$qualKp = $userBusiness->qual_user_level['qual_kp'] ?? null;
|
||||
|
||||
if ($qualKp !== null) {
|
||||
$rest_kp = max(0, $userBusiness->sales_volume_points_KP_sum - $qualKp);
|
||||
$calc_qual_kp = $rest_kp > 0 ? $qualKp : $userBusiness->sales_volume_points_KP_sum;
|
||||
|
||||
if ($userBusiness->calc_qual_kp !== $calc_qual_kp) {
|
||||
$userBusiness->calc_qual_kp = $calc_qual_kp;
|
||||
$hasChanges = true;
|
||||
}
|
||||
} else {
|
||||
$userBusiness->calc_qual_kp = $userBusiness->sales_volume_points_KP_sum;
|
||||
$hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Aktualisiere qual_user_level_next
|
||||
if (!empty($userBusiness->qual_user_level_next) && is_array($userBusiness->qual_user_level_next)) {
|
||||
$levelData = $userBusiness->qual_user_level_next;
|
||||
$updated = $this->addCalculatedFieldsToLevel($levelData, $userBusiness);
|
||||
|
||||
if ($updated !== false) {
|
||||
$userBusiness->qual_user_level_next = $updated;
|
||||
$hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Aktualisiere next_qual_user_level
|
||||
if (!empty($userBusiness->next_qual_user_level) && is_array($userBusiness->next_qual_user_level)) {
|
||||
$levelData = $userBusiness->next_qual_user_level;
|
||||
$updated = $this->addCalculatedFieldsToLevel($levelData, $userBusiness);
|
||||
|
||||
if ($updated !== false) {
|
||||
$userBusiness->next_qual_user_level = $updated;
|
||||
$hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Aktualisiere next_can_user_level
|
||||
if (!empty($userBusiness->next_can_user_level) && is_array($userBusiness->next_can_user_level)) {
|
||||
$levelData = $userBusiness->next_can_user_level;
|
||||
$updated = $this->addCalculatedFieldsToLevel($levelData, $userBusiness);
|
||||
|
||||
if ($updated !== false) {
|
||||
$userBusiness->next_can_user_level = $updated;
|
||||
$hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Speichere nur wenn Änderungen vorhanden sind und nicht im Dry-Run Mode
|
||||
if ($hasChanges && !$this->isDryRun) {
|
||||
$userBusiness->save();
|
||||
}
|
||||
|
||||
return $hasChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Füge berechnete Felder zu einem Level-Array hinzu
|
||||
*
|
||||
* @return array|false Array mit neuen Feldern oder false wenn keine Änderungen
|
||||
*/
|
||||
private function addCalculatedFieldsToLevel(array $levelData, UserBusiness $userBusiness)
|
||||
{
|
||||
// Prüfe ob Felder bereits existieren
|
||||
if (isset($levelData['_calculated_qual_kp']) && isset($levelData['_calculated_payline_points_qual_kp'])) {
|
||||
return false; // Keine Änderungen nötig
|
||||
}
|
||||
|
||||
$qualKp = $levelData['qual_kp'] ?? null;
|
||||
$paylines = $levelData['paylines'] ?? 0;
|
||||
|
||||
if ($qualKp === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Berechne die Werte
|
||||
$payline_points = $this->getPointsForPayline($userBusiness, $paylines);
|
||||
$rest_kp = max(0, $userBusiness->sales_volume_points_KP_sum - $qualKp);
|
||||
$payline_points_qual_kp = $payline_points + $rest_kp;
|
||||
$calc_qual_kp = $rest_kp > 0 ? $qualKp : $userBusiness->sales_volume_points_KP_sum;
|
||||
|
||||
// Füge die berechneten Felder hinzu
|
||||
$levelData['_calculated_qual_kp'] = $calc_qual_kp;
|
||||
$levelData['_calculated_payline_points'] = $payline_points;
|
||||
$levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp;
|
||||
|
||||
return $levelData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechne Payline-Punkte für eine bestimmte Anzahl von Paylines
|
||||
*/
|
||||
private function getPointsForPayline(UserBusiness $userBusiness, int $paylines): float
|
||||
{
|
||||
$payline_points = 0;
|
||||
$businessLines = $userBusiness->business_lines ?? [];
|
||||
|
||||
for ($i = 1; $i <= $paylines; $i++) {
|
||||
if (isset($businessLines[$i])) {
|
||||
$line = $businessLines[$i];
|
||||
|
||||
// Handle both array and object types
|
||||
if (is_array($line)) {
|
||||
$payline_points += (float) ($line['points'] ?? 0);
|
||||
} elseif (is_object($line)) {
|
||||
$payline_points += (float) ($line->points ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $payline_points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logge Ausführungszeit
|
||||
*/
|
||||
private function logExecutionTime($message)
|
||||
{
|
||||
$diff = microtime(true) - $this->timeStart;
|
||||
$sec = intval($diff);
|
||||
$micro = $diff - $sec;
|
||||
|
||||
$this->info($message . ' | Zeit: ' . $sec . 'sec :' . round($micro * 1000, 4) . " ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* Logge 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("Hohe Memory-Nutzung bei {$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 = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
}
|
||||
}
|
||||
97
app/Console/Commands/DhlBackfillEmails.php
Normal file
97
app/Console/Commands/DhlBackfillEmails.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class DhlBackfillEmails extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'dhl:backfill-emails
|
||||
{--dry-run : Nur simulieren, keine Änderungen}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Füllt das E-Mail-Feld für bestehende DHL-Sendungen nach';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info('DHL E-Mail Backfill gestartet');
|
||||
$this->info('Modus: ' . ($dryRun ? 'DRY-RUN (keine Änderungen)' : 'LIVE'));
|
||||
$this->newLine();
|
||||
|
||||
// Hole alle Sendungen ohne E-Mail
|
||||
$shipments = DhlShipment::with('shoppingOrder.shopping_user')
|
||||
->whereNull('email')
|
||||
->orWhere('email', '')
|
||||
->get();
|
||||
|
||||
$total = $shipments->count();
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$this->info("Gefundene Sendungen ohne E-Mail: {$total}");
|
||||
$this->newLine();
|
||||
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
|
||||
foreach ($shipments as $shipment) {
|
||||
$bar->advance();
|
||||
|
||||
// Hole E-Mail aus Shopping User
|
||||
$email = null;
|
||||
if ($shipment->shoppingOrder && $shipment->shoppingOrder->shopping_user) {
|
||||
$email = $shipment->shoppingOrder->shopping_user->email;
|
||||
}
|
||||
|
||||
if (empty($email)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $dryRun) {
|
||||
$shipment->email = $email;
|
||||
$shipment->save();
|
||||
}
|
||||
|
||||
$updated++;
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// Statistik
|
||||
$this->info('Backfill abgeschlossen!');
|
||||
$this->newLine();
|
||||
$this->table(
|
||||
['Metrik', 'Anzahl'],
|
||||
[
|
||||
['Gesamt geprüft', $total],
|
||||
['E-Mail gesetzt', $updated],
|
||||
['Übersprungen (keine E-Mail)', $skipped],
|
||||
]
|
||||
);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('DRY-RUN: Keine Änderungen wurden vorgenommen.');
|
||||
$this->info('Führen Sie den Befehl ohne --dry-run aus, um die Änderungen zu speichern.');
|
||||
} else {
|
||||
$this->info("{$updated} Sendungen wurden aktualisiert.");
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
226
app/Console/Commands/DhlUpdateTracking.php
Normal file
226
app/Console/Commands/DhlUpdateTracking.php
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use App\Mail\MailDhlTracking;
|
||||
use App\Services\DhlTrackingService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class DhlUpdateTracking extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'dhl:update-tracking
|
||||
{--days=14 : Sendungen der letzten X Tage aktualisieren}
|
||||
{--send-emails : Automatisch E-Mails bei Transit-Status senden}
|
||||
{--dry-run : Nur simulieren, keine Änderungen}
|
||||
{--test-email= : Test-E-Mail an angegebene Adresse senden}
|
||||
{--order= : Nur für bestimmte Bestellung (Order-ID)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Aktualisiert Tracking-Status für alle aktiven DHL Sendungen und sendet automatisch E-Mails bei Transit-Status';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$days = (int) $this->option('days');
|
||||
$sendEmails = $this->option('send-emails');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$testEmail = $this->option('test-email');
|
||||
$orderId = $this->option('order');
|
||||
|
||||
$this->info('DHL Tracking Update gestartet');
|
||||
$this->info("Optionen: --days={$days}, --send-emails=" . ($sendEmails ? 'ja' : 'nein') . ', --dry-run=' . ($dryRun ? 'ja' : 'nein'));
|
||||
if ($testEmail) {
|
||||
$this->info("Test-Modus: E-Mails werden an {$testEmail} gesendet");
|
||||
}
|
||||
if ($orderId) {
|
||||
$this->info("Filter: Nur Order-ID {$orderId}");
|
||||
}
|
||||
$this->newLine();
|
||||
|
||||
// Hole alle aktiven Sendungen der letzten X Tage
|
||||
$query = DhlShipment::active()
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->whereNotNull('dhl_shipment_no');
|
||||
|
||||
// Filter nach Order-ID wenn angegeben
|
||||
if ($orderId) {
|
||||
$query->where('order_id', $orderId);
|
||||
}
|
||||
|
||||
$shipments = $query->orderBy('created_at', 'desc')->get();
|
||||
|
||||
$total = $shipments->count();
|
||||
$this->info("Gefundene aktive Sendungen: {$total}");
|
||||
|
||||
if ($total === 0) {
|
||||
$this->info('Keine Sendungen zum Aktualisieren gefunden.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$bar->start();
|
||||
|
||||
$trackingService = new DhlTrackingService;
|
||||
$stats = [
|
||||
'updated' => 0,
|
||||
'failed' => 0,
|
||||
'emails_sent' => 0,
|
||||
'skipped' => 0,
|
||||
];
|
||||
|
||||
foreach ($shipments as $shipment) {
|
||||
try {
|
||||
$oldStatus = $shipment->status;
|
||||
|
||||
if (! $dryRun) {
|
||||
// Tracking aktualisieren
|
||||
$result = $trackingService->updateTracking($shipment, ['auto_retrack' => false]);
|
||||
|
||||
if ($result['success']) {
|
||||
$shipment->refresh();
|
||||
$stats['updated']++;
|
||||
|
||||
// Prüfen ob E-Mail gesendet werden soll
|
||||
if ($sendEmails && $this->shouldSendEmail($shipment, $oldStatus)) {
|
||||
$this->sendTrackingEmail($shipment, $testEmail);
|
||||
$stats['emails_sent']++;
|
||||
}
|
||||
} else {
|
||||
$stats['failed']++;
|
||||
Log::warning('[DHL Cron] Tracking update failed', [
|
||||
'shipment_id' => $shipment->id,
|
||||
'message' => $result['message'] ?? 'Unknown error',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$stats['skipped']++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$stats['failed']++;
|
||||
Log::error('[DHL Cron] Exception during tracking update', [
|
||||
'shipment_id' => $shipment->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// Zusammenfassung
|
||||
$this->info('Zusammenfassung:');
|
||||
$this->table(
|
||||
['Metrik', 'Anzahl'],
|
||||
[
|
||||
['Gesamt', $total],
|
||||
['Aktualisiert', $stats['updated']],
|
||||
['Fehlgeschlagen', $stats['failed']],
|
||||
['E-Mails gesendet', $stats['emails_sent']],
|
||||
['Übersprungen (Dry-Run)', $stats['skipped']],
|
||||
]
|
||||
);
|
||||
|
||||
Log::info('[DHL Cron] Tracking update completed', $stats);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine E-Mail gesendet werden soll
|
||||
*/
|
||||
private function shouldSendEmail(DhlShipment $shipment, string $oldStatus): bool
|
||||
{
|
||||
// E-Mail nur senden wenn:
|
||||
// 1. Status ist jetzt "in_transit"
|
||||
// 2. Vorheriger Status war NICHT "in_transit" (also Status hat sich geändert)
|
||||
// 3. Noch keine E-Mail gesendet wurde
|
||||
return $shipment->status === 'in_transit'
|
||||
&& $oldStatus !== 'in_transit'
|
||||
&& ! $shipment->wasTrackingEmailSent()
|
||||
&& $shipment->canSendTrackingEmail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet die Tracking-E-Mail (mit Unterstützung für mehrere Sendungen pro Bestellung)
|
||||
*/
|
||||
private function sendTrackingEmail(DhlShipment $shipment, ?string $testEmail = null): void
|
||||
{
|
||||
try {
|
||||
$order = $shipment->shoppingOrder;
|
||||
|
||||
// Determine recipient email: test email > shipment email > shopping user email
|
||||
$recipientEmail = null;
|
||||
if ($testEmail) {
|
||||
$recipientEmail = $testEmail;
|
||||
} elseif (! empty($shipment->email)) {
|
||||
$recipientEmail = $shipment->email;
|
||||
} elseif ($order->shopping_user && ! empty($order->shopping_user->email)) {
|
||||
$recipientEmail = $order->shopping_user->email;
|
||||
}
|
||||
|
||||
if (! $recipientEmail) {
|
||||
Log::warning('[DHL Cron] Cannot send email - no recipient', [
|
||||
'shipment_id' => $shipment->id,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Sammle alle Sendungen für diese Bestellung, die noch keine E-Mail erhalten haben
|
||||
$allShipments = DhlShipment::where('order_id', $order->id)
|
||||
->where('status', 'in_transit')
|
||||
->whereNotNull('dhl_shipment_no')
|
||||
->whereNull('tracking_email_sent_at')
|
||||
->get();
|
||||
|
||||
// Wenn keine Sendungen gefunden, nutze nur die aktuelle
|
||||
if ($allShipments->isEmpty()) {
|
||||
$allShipments = collect([$shipment]);
|
||||
}
|
||||
|
||||
// Sende E-Mail mit allen Sendungen
|
||||
Mail::to($recipientEmail)->send(new MailDhlTracking($allShipments, $order));
|
||||
|
||||
// Markiere alle Sendungen als versendet
|
||||
foreach ($allShipments as $s) {
|
||||
$s->markTrackingEmailSent('auto');
|
||||
}
|
||||
|
||||
Log::info('[DHL Cron] Tracking email sent automatically', [
|
||||
'shipment_ids' => $allShipments->pluck('id')->toArray(),
|
||||
'shipments_count' => $allShipments->count(),
|
||||
'dhl_shipment_nos' => $allShipments->pluck('dhl_shipment_no')->toArray(),
|
||||
'email' => $recipientEmail,
|
||||
'is_test' => ! is_null($testEmail),
|
||||
]);
|
||||
|
||||
if ($allShipments->count() > 1) {
|
||||
$this->line(" -> E-Mail mit {$allShipments->count()} Sendungen gesendet an: {$recipientEmail}");
|
||||
} else {
|
||||
$this->line(" -> E-Mail gesendet an: {$recipientEmail}");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[DHL Cron] Failed to send tracking email', [
|
||||
'shipment_id' => $shipment->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
app/Console/Commands/LogCleanup.php
Normal file
106
app/Console/Commands/LogCleanup.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class LogCleanup extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'logs:cleanup {--days=30 : Number of days to keep logs}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clean up old log files older than specified days';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$days = $this->option('days');
|
||||
$logPath = storage_path('logs');
|
||||
|
||||
if (!File::isDirectory($logPath)) {
|
||||
$this->error("Log directory not found: {$logPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$deletedFiles = 0;
|
||||
$deletedSize = 0;
|
||||
$cutoffDate = Carbon::now()->subDays($days);
|
||||
|
||||
$this->info("Cleaning up log files older than {$days} days (before {$cutoffDate->toDateString()})...");
|
||||
|
||||
$files = File::files($logPath);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filename = $file->getFilename();
|
||||
|
||||
// Skip the current laravel.log file
|
||||
if ($filename === 'laravel.log') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lastModified = Carbon::createFromTimestamp($file->getMTime());
|
||||
|
||||
if ($lastModified->lt($cutoffDate)) {
|
||||
$fileSize = $file->getSize();
|
||||
|
||||
try {
|
||||
File::delete($file->getPathname());
|
||||
$deletedFiles++;
|
||||
$deletedSize += $fileSize;
|
||||
|
||||
$this->line("Deleted: {$filename} (" . $this->formatBytes($fileSize) . ", modified: {$lastModified->toDateString()})");
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Failed to delete {$filename}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($deletedFiles > 0) {
|
||||
$this->info("\nCleanup complete!");
|
||||
$this->info("Deleted {$deletedFiles} file(s), freed " . $this->formatBytes($deletedSize));
|
||||
|
||||
Log::channel('cleanup')->info("Log cleanup completed", [
|
||||
'deleted_files' => $deletedFiles,
|
||||
'freed_space' => $this->formatBytes($deletedSize),
|
||||
'days' => $days
|
||||
]);
|
||||
} else {
|
||||
$this->info("No old log files found to delete.");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human readable format
|
||||
*
|
||||
* @param int $bytes
|
||||
* @return string
|
||||
*/
|
||||
private function formatBytes($bytes)
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
319
app/Console/Commands/TestGrowthBonusCalculation.php
Normal file
319
app/Console/Commands/TestGrowthBonusCalculation.php
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\BusinessPlan\TreeCalcBotOptimized;
|
||||
use App\Services\BusinessPlan\BusinessUserItemOptimized;
|
||||
use App\Services\BusinessPlan\GrowthBonusCalculator;
|
||||
use App\User;
|
||||
use App\Models\UserBusiness;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class TestGrowthBonusCalculation extends Command
|
||||
{
|
||||
protected $signature = 'test:growth-bonus
|
||||
{user_id : Die User-ID für den Test}
|
||||
{--month=12 : Der Monat}
|
||||
{--year=2025 : Das Jahr}
|
||||
{--debug : Zeigt detaillierte Debug-Informationen}
|
||||
{--from-db : Verwendet gespeicherte Daten und simuliert Berechnung}';
|
||||
|
||||
protected $description = 'Testet die Growth Bonus Berechnung für einen User';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$userId = $this->argument('user_id');
|
||||
$month = (int) $this->option('month');
|
||||
$year = (int) $this->option('year');
|
||||
$debug = $this->option('debug');
|
||||
$fromDb = $this->option('from-db');
|
||||
|
||||
$this->info("=== Growth Bonus Test ===");
|
||||
$this->info("User: {$userId}, Monat: {$month}/{$year}");
|
||||
$this->newLine();
|
||||
|
||||
// User laden
|
||||
$user = User::find($userId);
|
||||
if (!$user) {
|
||||
$this->error("User {$userId} nicht gefunden!");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$date = Carbon::createFromDate($year, $month, 1);
|
||||
|
||||
// Gespeicherte UserBusiness-Daten laden
|
||||
$userBusiness = UserBusiness::where('user_id', $userId)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
|
||||
if ($userBusiness) {
|
||||
$this->info("=== GESPEICHERTE DATEN (UserBusiness) ===");
|
||||
$qualLevel = $userBusiness->qual_user_level;
|
||||
$this->table(
|
||||
['Feld', 'Wert'],
|
||||
[
|
||||
['user_id', $userBusiness->user_id],
|
||||
['qual_user_level (Name)', $qualLevel['name'] ?? 'NULL'],
|
||||
['Growth Bonus (aus qual_user_level)', $qualLevel['growth_bonus'] ?? 'NULL'],
|
||||
['active_growth_bonus (gespeichert)', $userBusiness->active_growth_bonus ?? 'NULL'],
|
||||
['commission_growth_total', $userBusiness->commission_growth_total ?? 0],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$this->warn("Keine gespeicherten UserBusiness-Daten für {$month}/{$year}");
|
||||
if (!$fromDb) {
|
||||
$this->info("Versuche Live-Berechnung...");
|
||||
} else {
|
||||
$this->error("--from-db erfordert gespeicherte Daten!");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($fromDb && $userBusiness) {
|
||||
$this->testFromDatabase($userId, $month, $year, $debug);
|
||||
} else {
|
||||
$this->testLiveCalculation($user, $date, $month, $year, $debug);
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("=== TEST ABGESCHLOSSEN ===");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function testFromDatabase(int $userId, int $month, int $year, bool $debug): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info("=== ANALYSE AUS DATENBANK ===");
|
||||
|
||||
// Lade User und seine Firstlines
|
||||
$userBusiness = UserBusiness::where('user_id', $userId)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
|
||||
$qualLevel = $userBusiness->qual_user_level;
|
||||
$myGrowthBonus = (float) ($qualLevel['growth_bonus'] ?? 0);
|
||||
|
||||
$this->info("Mein Growth Bonus Anspruch: {$myGrowthBonus}%");
|
||||
|
||||
if ($myGrowthBonus <= 0) {
|
||||
$this->warn("Kein Growth Bonus Anspruch!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lade Firstlines
|
||||
$firstlineIds = User::where('m_sponsor', $userId)->pluck('id')->toArray();
|
||||
$this->info("Anzahl Firstlines: " . count($firstlineIds));
|
||||
|
||||
$this->newLine();
|
||||
$this->info("=== FIRSTLINE ANALYSE ===");
|
||||
|
||||
$totalExpectedGrowth = 0;
|
||||
$problemsFound = false;
|
||||
|
||||
foreach ($firstlineIds as $flId) {
|
||||
$flBusiness = UserBusiness::where('user_id', $flId)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
|
||||
if (!$flBusiness) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$flQualLevel = $flBusiness->qual_user_level;
|
||||
$flGrowthBonus = (float) ($flQualLevel['growth_bonus'] ?? 0);
|
||||
$flLevelName = $flQualLevel['name'] ?? 'Kein Level';
|
||||
$flTPSum = (float) ($flBusiness->sales_volume_points_TP_sum ?? 0);
|
||||
|
||||
if ($flTPSum <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Berechne erwartete Differenz
|
||||
$expectedDiff = max(0, $myGrowthBonus - $flGrowthBonus);
|
||||
$expectedCommission = round($flTPSum / 100 * $expectedDiff, 2);
|
||||
|
||||
// Problem-Erkennung: Wenn FL einen Growth Bonus hat aber wir
|
||||
// trotzdem den vollen Betrag bekommen
|
||||
$isPotentialProblem = $flGrowthBonus > 0 && $expectedDiff < $myGrowthBonus;
|
||||
|
||||
$this->info("--- Firstline User {$flId} ---");
|
||||
$this->table(
|
||||
['Feld', 'Wert'],
|
||||
[
|
||||
['Level erreicht', $flLevelName],
|
||||
['Growth Bonus (FL)', $flGrowthBonus . '%'],
|
||||
['Team-Punkte (TP_sum)', number_format($flTPSum, 0, ',', '.')],
|
||||
['Mein Anspruch', $myGrowthBonus . '%'],
|
||||
['Differenz (erwartet)', $expectedDiff . '%'],
|
||||
['Erwartete Provision', number_format($expectedCommission, 2, ',', '.') . ' €'],
|
||||
['ACHTUNG: Blockade?', $isPotentialProblem ? 'JA - FL sollte blockieren!' : 'Nein'],
|
||||
]
|
||||
);
|
||||
|
||||
if ($isPotentialProblem) {
|
||||
$problemsFound = true;
|
||||
$this->error(" ⚠️ User {$flId} hat {$flLevelName} ({$flGrowthBonus}%) erreicht!");
|
||||
$this->error(" Differenz sollte nur {$expectedDiff}% sein, nicht {$myGrowthBonus}%!");
|
||||
}
|
||||
|
||||
$totalExpectedGrowth += $expectedCommission;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("=== ZUSAMMENFASSUNG ===");
|
||||
$this->info("Erwartete Growth Bonus Summe (mit korrekter Differenz): " . number_format($totalExpectedGrowth, 2, ',', '.') . ' €');
|
||||
$this->info("Gespeicherte Growth Bonus Summe: " . number_format($userBusiness->commission_growth_total ?? 0, 2, ',', '.') . ' €');
|
||||
|
||||
if ($problemsFound) {
|
||||
$this->newLine();
|
||||
$this->error("⚠️ PROBLEME GEFUNDEN! Die Blockade durch qualifizierte Firstlines funktioniert möglicherweise nicht korrekt.");
|
||||
}
|
||||
|
||||
// Detailanalyse: Wie wurde die Berechnung durchgeführt?
|
||||
if ($debug) {
|
||||
$this->newLine();
|
||||
$this->info("=== DEBUG: Rekursive Volumen-Analyse ===");
|
||||
$this->analyzeVolumeDistribution($userId, $month, $year, $myGrowthBonus, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private function analyzeVolumeDistribution(int $userId, int $month, int $year, float $myPercent, int $depth = 1): array
|
||||
{
|
||||
if ($depth > 10) {
|
||||
return ['0.0' => 0];
|
||||
}
|
||||
|
||||
$userBusiness = UserBusiness::where('user_id', $userId)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
|
||||
if (!$userBusiness) {
|
||||
return ['0.0' => 0];
|
||||
}
|
||||
|
||||
$qualLevel = $userBusiness->qual_user_level;
|
||||
$myProtection = (float) ($qualLevel['growth_bonus'] ?? 0);
|
||||
|
||||
$indent = str_repeat(" ", $depth);
|
||||
|
||||
// Eigenes Volumen (TP_sum - aber nur direkte Punkte, nicht Team)
|
||||
// Bei gespeicherten Daten ist das schwer zu unterscheiden
|
||||
// Vereinfachung: Für den Test nehmen wir TP_sum als Gesamt-Volumen
|
||||
|
||||
$this->line("{$indent}User {$userId}: Protection={$myProtection}%");
|
||||
|
||||
// Lade Kinder
|
||||
$childIds = User::where('m_sponsor', $userId)->pluck('id')->toArray();
|
||||
|
||||
$volumes = [];
|
||||
|
||||
foreach ($childIds as $childId) {
|
||||
$childBusiness = UserBusiness::where('user_id', $childId)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
|
||||
if (!$childBusiness) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$childTP = (float) ($childBusiness->sales_volume_points_TP_sum ?? 0);
|
||||
if ($childTP <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$childQual = $childBusiness->qual_user_level;
|
||||
$childProtection = (float) ($childQual['growth_bonus'] ?? 0);
|
||||
$childLevelName = $childQual['name'] ?? 'Kein Level';
|
||||
|
||||
// Berechne Differenz
|
||||
$diff = max(0, $myPercent - $childProtection);
|
||||
$commission = round($childTP / 100 * $diff, 2);
|
||||
|
||||
$this->line("{$indent} └─ Child {$childId}: {$childLevelName}, Protection={$childProtection}%, TP={$childTP}");
|
||||
$this->line("{$indent} Differenz: {$myPercent}% - {$childProtection}% = {$diff}%");
|
||||
$this->line("{$indent} Provision: {$childTP} * {$diff}% = {$commission}€");
|
||||
|
||||
if ($childProtection > 0) {
|
||||
$this->warn("{$indent} ⚠️ BLOCKADE durch {$childLevelName}!");
|
||||
}
|
||||
}
|
||||
|
||||
return $volumes;
|
||||
}
|
||||
|
||||
private function testLiveCalculation(User $user, Carbon $date, int $month, int $year, bool $debug): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info("=== LIVE-BERECHNUNG ===");
|
||||
|
||||
// TreeCalcBot erstellen
|
||||
$treeCalcBot = new TreeCalcBotOptimized($month, $year, 'member', true);
|
||||
|
||||
// BusinessUserItem erstellen
|
||||
$businessUserItem = new BusinessUserItemOptimized($date, $treeCalcBot);
|
||||
$businessUserItem->makeUserFromModel($user, true);
|
||||
$businessUserItem->addUserID();
|
||||
|
||||
// Kinder laden
|
||||
$businessUserItem->readParentsBusinessUsers(true, 0);
|
||||
|
||||
// Qualifikation berechnen
|
||||
$businessUserItem->calcQualPP(true);
|
||||
|
||||
$qualUserLevel = $businessUserItem->getQualUserLevel();
|
||||
|
||||
$this->table(
|
||||
['Feld', 'Wert'],
|
||||
[
|
||||
['user_id', $businessUserItem->user_id],
|
||||
['isQualLevel()', $businessUserItem->isQualLevel() ? 'JA' : 'NEIN'],
|
||||
['isQualificationCalculated()', $businessUserItem->isQualificationCalculated() ? 'JA' : 'NEIN'],
|
||||
['getQualUserLevel() (Name)', $qualUserLevel['name'] ?? 'NULL'],
|
||||
['Growth Bonus (qual_user_level)', $qualUserLevel['growth_bonus'] ?? 'NULL'],
|
||||
['getActiveGrowthBonus()', $businessUserItem->getActiveGrowthBonus()],
|
||||
['getQualifiedGrowthBonus()', $businessUserItem->getQualifiedGrowthBonus()],
|
||||
]
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->info("=== FIRSTLINES (Kinder) ===");
|
||||
|
||||
if (empty($businessUserItem->businessUserItems)) {
|
||||
$this->warn("Keine Firstlines geladen!");
|
||||
} else {
|
||||
foreach ($businessUserItem->businessUserItems as $index => $childItem) {
|
||||
$childQual = $childItem->getQualUserLevel();
|
||||
|
||||
$this->info("--- Firstline " . ($index + 1) . " ---");
|
||||
$this->table(
|
||||
['Feld', 'Wert'],
|
||||
[
|
||||
['user_id', $childItem->user_id],
|
||||
['isQualLevel()', $childItem->isQualLevel() ? 'JA' : 'NEIN'],
|
||||
['getQualUserLevel() (Name)', $childQual['name'] ?? 'NULL'],
|
||||
['Growth Bonus (qual_user_level)', $childQual['growth_bonus'] ?? 'NULL'],
|
||||
['getActiveGrowthBonus()', $childItem->getActiveGrowthBonus()],
|
||||
['getQualifiedGrowthBonus()', $childItem->getQualifiedGrowthBonus()],
|
||||
['sales_volume_points_TP_sum', $childItem->sales_volume_points_TP_sum ?? 0],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($qualUserLevel && ($qualUserLevel['growth_bonus'] ?? 0) > 0) {
|
||||
$calculator = new GrowthBonusCalculator();
|
||||
$qualData = (object) $qualUserLevel;
|
||||
$totalGrowthBonus = $calculator->calculate($businessUserItem, $qualData);
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Berechneter Growth Bonus: {$totalGrowthBonus}");
|
||||
}
|
||||
}
|
||||
}
|
||||
321
app/Console/Commands/TestUserLevelUpdate.php
Normal file
321
app/Console/Commands/TestUserLevelUpdate.php
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\User;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Models\UserLevel;
|
||||
use App\Cron\UserLevelUpdate;
|
||||
use App\Console\Commands\BusinessStoreOptimized;
|
||||
use Illuminate\Console\Command;
|
||||
use ReflectionClass;
|
||||
|
||||
class TestUserLevelUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* php artisan test:user-level-update {month} {year} {--user_id=} {--send-mail} {--dry-run}
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'test:user-level-update {month} {year} {--user_id= : Test für spezifischen User} {--send-mail : E-Mail senden} {--dry-run : Nur anzeigen, nicht speichern}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Testet die UserLevelUpdate-Funktion aus BusinessStoreOptimized für einen Monat/Jahr oder spezifischen User';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$month = (int) $this->argument('month');
|
||||
$year = (int) $this->argument('year');
|
||||
$userId = $this->option('user_id') ? (int) $this->option('user_id') : null;
|
||||
$sendMail = $this->option('send-mail');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info("===========================================");
|
||||
$this->info("UserLevelUpdate Test");
|
||||
$this->info("===========================================");
|
||||
$this->line("Monat: {$month}");
|
||||
$this->line("Jahr: {$year}");
|
||||
if ($userId) {
|
||||
$this->line("User ID: {$userId}");
|
||||
}
|
||||
$this->line("E-Mail senden: " . ($sendMail ? 'Ja' : 'Nein'));
|
||||
$this->line("Dry-Run (nur zeigen): " . ($dryRun ? 'Ja' : 'Nein'));
|
||||
$this->line("");
|
||||
|
||||
if ($dryRun) {
|
||||
// Im Dry-Run Modus zeigen wir nur die Analyse
|
||||
$this->performDryRunAnalysis($month, $year, $userId);
|
||||
} else {
|
||||
// Nutze die originale Funktion aus BusinessStoreOptimized
|
||||
$this->runOriginalFunction($month, $year, $sendMail);
|
||||
}
|
||||
|
||||
$this->info("");
|
||||
$this->info("===========================================");
|
||||
$this->info("Test abgeschlossen!");
|
||||
$this->info("===========================================");
|
||||
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Test fehlgeschlagen: ' . $e->getMessage());
|
||||
$this->error('Stack trace: ' . $e->getTraceAsString());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die originale userLevelUpdate Funktion aus BusinessStoreOptimized aus
|
||||
*/
|
||||
private function runOriginalFunction(int $month, int $year, bool $sendMail)
|
||||
{
|
||||
$this->info("Erstelle BusinessStoreOptimized Instanz...");
|
||||
|
||||
// Erstelle BusinessStoreOptimized Command-Instanz
|
||||
$businessStoreCommand = new BusinessStoreOptimized();
|
||||
|
||||
// Setze Output auf aktuellen Command (damit Ausgaben weitergeleitet werden)
|
||||
$businessStoreCommand->setOutput($this->output);
|
||||
|
||||
// Setze Monat und Jahr über Reflection (da private)
|
||||
$reflection = new ReflectionClass($businessStoreCommand);
|
||||
|
||||
$monthProperty = $reflection->getProperty('month');
|
||||
$monthProperty->setAccessible(true);
|
||||
$monthProperty->setValue($businessStoreCommand, $month);
|
||||
|
||||
$yearProperty = $reflection->getProperty('year');
|
||||
$yearProperty->setAccessible(true);
|
||||
$yearProperty->setValue($businessStoreCommand, $year);
|
||||
|
||||
$timeStartProperty = $reflection->getProperty('timeStart');
|
||||
$timeStartProperty->setAccessible(true);
|
||||
$timeStartProperty->setValue($businessStoreCommand, microtime(true));
|
||||
|
||||
// Setze sendUpdateMail
|
||||
$businessStoreCommand->setSendUpdateMail($sendMail);
|
||||
|
||||
$this->info("Führe originale userLevelUpdate() Funktion aus...");
|
||||
$this->line("");
|
||||
|
||||
// Rufe die originale Funktion auf
|
||||
$businessStoreCommand->userLevelUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt Dry-Run Analyse durch
|
||||
*/
|
||||
private function performDryRunAnalysis(int $month, int $year, ?int $userId)
|
||||
{
|
||||
$userLevelUpdate = new UserLevelUpdate($month, $year);
|
||||
|
||||
if ($userId) {
|
||||
// Test für spezifischen User
|
||||
$this->testSingleUserDryRun($userLevelUpdate, $userId);
|
||||
} else {
|
||||
// Test für alle User
|
||||
$this->testAllUsersDryRun($userLevelUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dry-Run Analyse für einen spezifischen User
|
||||
*/
|
||||
private function testSingleUserDryRun(UserLevelUpdate $userLevelUpdate, int $userId)
|
||||
{
|
||||
$userBusiness = UserBusiness::with('user')
|
||||
->where('month', $this->argument('month'))
|
||||
->where('year', $this->argument('year'))
|
||||
->where('user_id', $userId)
|
||||
->whereNotNull('next_qual_user_level')
|
||||
->whereRaw("JSON_LENGTH(next_qual_user_level) > 0")
|
||||
->first();
|
||||
|
||||
if (!$userBusiness) {
|
||||
$this->warn("Keine UserBusiness mit next_qual_user_level gefunden für User ID: {$userId}");
|
||||
|
||||
// Zeige vorhandene UserBusiness-Daten
|
||||
$anyUserBusiness = UserBusiness::where('user_id', $userId)
|
||||
->where('month', $this->argument('month'))
|
||||
->where('year', $this->argument('year'))
|
||||
->first();
|
||||
|
||||
if ($anyUserBusiness) {
|
||||
$this->info("UserBusiness existiert, aber hat kein next_qual_user_level");
|
||||
$this->line("Current Level ID: " . ($anyUserBusiness->m_level_id ?? 'NULL'));
|
||||
$this->line("next_qual_user_level: " . (is_null($anyUserBusiness->next_qual_user_level) ? 'NULL' : json_encode($anyUserBusiness->next_qual_user_level)));
|
||||
} else {
|
||||
$this->warn("Keine UserBusiness gefunden für diesen Monat/Jahr");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$this->displayUserBusinessInfo($userBusiness);
|
||||
$this->info("");
|
||||
$this->info("DRY-RUN MODUS: Änderungen werden nicht gespeichert");
|
||||
$this->analyzeLevelUpdate($userBusiness);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dry-Run Analyse für alle User
|
||||
*/
|
||||
private function testAllUsersDryRun(UserLevelUpdate $userLevelUpdate)
|
||||
{
|
||||
$levelUpdateUsers = $userLevelUpdate->getUserBusinessByMonthYear();
|
||||
|
||||
$this->info("Gefunden: " . $levelUpdateUsers->count() . " UserBusiness-Einträge mit next_qual_user_level");
|
||||
$this->line("");
|
||||
|
||||
if ($levelUpdateUsers->count() === 0) {
|
||||
$this->warn("Keine UserBusiness-Einträge mit Level-Updates gefunden.");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info("DRY-RUN MODUS: Änderungen werden nicht gespeichert");
|
||||
$this->line("");
|
||||
|
||||
foreach ($levelUpdateUsers as $userBusiness) {
|
||||
$this->line("---------------------------------------------------");
|
||||
$this->displayUserBusinessInfo($userBusiness);
|
||||
$this->analyzeLevelUpdate($userBusiness);
|
||||
$this->line("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Informationen über UserBusiness
|
||||
*/
|
||||
private function displayUserBusinessInfo(UserBusiness $userBusiness)
|
||||
{
|
||||
$user = $userBusiness->user;
|
||||
|
||||
$this->line("User ID: " . ($user ? $user->id : 'NULL'));
|
||||
$this->line("E-Mail: " . ($user ? $user->email : 'N/A'));
|
||||
$this->line("Aktuelles Level: " . ($user && $user->m_level ? $user->m_level . ' (' . $this->getLevelName($user->m_level) . ')' : 'Kein Level'));
|
||||
$this->line("UserBusiness Level ID: " . ($userBusiness->m_level_id ?? 'NULL'));
|
||||
$this->line("UserBusiness Level Name: " . ($userBusiness->user_level_name ?? 'NULL'));
|
||||
|
||||
$nextQual = $userBusiness->next_qual_user_level;
|
||||
if (is_array($nextQual)) {
|
||||
if (isset($nextQual['id'])) {
|
||||
// Einzelnes Level
|
||||
$this->line("Nächstes qualifiziertes Level: ID " . $nextQual['id'] . ' - ' . ($nextQual['name'] ?? 'N/A') . ' (POS: ' . ($nextQual['pos'] ?? 'N/A') . ')');
|
||||
$this->line(" Bereits aktualisiert: " . (isset($nextQual['hasUpdated']) && $nextQual['hasUpdated'] == 1 ? 'Ja' : 'Nein'));
|
||||
} else {
|
||||
// Array von Leveln
|
||||
$this->line("Nächste qualifizierte Level: " . count($nextQual) . " Level gefunden");
|
||||
foreach ($nextQual as $idx => $level) {
|
||||
if (is_array($level) && isset($level['id'])) {
|
||||
$updated = isset($level['hasUpdated']) && $level['hasUpdated'] == 1 ? ' (bereits aktualisiert)' : '';
|
||||
$this->line(" [{$idx}] ID " . $level['id'] . ' - ' . ($level['name'] ?? 'N/A') . ' (POS: ' . ($level['pos'] ?? 'N/A') . ')' . $updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->line("next_qual_user_level: " . (is_null($nextQual) ? 'NULL' : gettype($nextQual)));
|
||||
}
|
||||
|
||||
$this->line("Total Qual PP: " . ($userBusiness->total_qual_pp ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Analysiert ob und wie ein Level-Update durchgeführt würde (Dry-Run)
|
||||
*/
|
||||
private function analyzeLevelUpdate(UserBusiness $userBusiness)
|
||||
{
|
||||
$user = $userBusiness->user;
|
||||
if (!$user) {
|
||||
$this->warn("⚠ Kein User-Objekt vorhanden");
|
||||
return;
|
||||
}
|
||||
|
||||
$nextQual = $userBusiness->next_qual_user_level;
|
||||
if (!is_array($nextQual) || empty($nextQual)) {
|
||||
$this->warn("⚠ next_qual_user_level ist kein gültiges Array");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lade UserLevels für Vergleich
|
||||
$userLevels = UserLevel::where('active', 1)->orderBy('pos')->get()->keyBy('id');
|
||||
|
||||
// Prüfe ob einzelnes Level oder Array
|
||||
$levelArray = isset($nextQual['id']) ? [$nextQual] : $nextQual;
|
||||
|
||||
$currentUserLevel = null;
|
||||
if ($user->m_level) {
|
||||
$currentUserLevel = $userLevels->get($user->m_level);
|
||||
}
|
||||
|
||||
$this->info("");
|
||||
$this->info("📊 Analyse:");
|
||||
|
||||
if ($currentUserLevel) {
|
||||
$this->line(" Aktuelles Level POS: {$currentUserLevel->pos}");
|
||||
} else {
|
||||
$this->line(" Aktuelles Level: Kein Level gesetzt");
|
||||
}
|
||||
|
||||
$wouldUpdate = false;
|
||||
$highestLevel = null;
|
||||
$highestPos = 0;
|
||||
|
||||
foreach ($levelArray as $levelData) {
|
||||
if (!is_array($levelData) || !isset($levelData['id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($levelData['hasUpdated']) && $levelData['hasUpdated'] == 1) {
|
||||
$this->line(" ⏭ Level ID {$levelData['id']} wurde bereits aktualisiert");
|
||||
continue;
|
||||
}
|
||||
|
||||
$newLevel = $userLevels->get($levelData['id']);
|
||||
$newLevelPos = $newLevel ? $newLevel->pos : ($levelData['pos'] ?? 0);
|
||||
|
||||
$levelName = $levelData['name'] ?? 'N/A';
|
||||
$this->line(" 📈 Level ID {$levelData['id']} ({$levelName}): POS {$newLevelPos}");
|
||||
|
||||
if (!$currentUserLevel || $newLevelPos > $currentUserLevel->pos) {
|
||||
if ($newLevelPos > $highestPos) {
|
||||
$highestPos = $newLevelPos;
|
||||
$highestLevel = $levelData;
|
||||
$wouldUpdate = true;
|
||||
}
|
||||
} else {
|
||||
$this->line(" ⚠ Level ist nicht höher als aktuelles Level (POS {$currentUserLevel->pos})");
|
||||
}
|
||||
}
|
||||
|
||||
if ($wouldUpdate && $highestLevel) {
|
||||
$this->info("");
|
||||
$highestLevelName = $highestLevel['name'] ?? 'N/A';
|
||||
$this->info("✅ Würde Level aktualisieren zu: {$highestLevel['id']} ({$highestLevelName})");
|
||||
$this->line(" Von: " . ($currentUserLevel ? "POS {$currentUserLevel->pos}" : "Kein Level") . " → Zu: POS {$highestPos}");
|
||||
} else {
|
||||
$this->info("");
|
||||
$this->warn("⚠ Kein Level-Update würde durchgeführt:");
|
||||
if (!$wouldUpdate) {
|
||||
$this->line(" Kein höheres Level gefunden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt Level-Name nach ID
|
||||
*/
|
||||
private function getLevelName($levelId)
|
||||
{
|
||||
$level = UserLevel::find($levelId);
|
||||
return $level ? $level->name : 'Unbekannt';
|
||||
}
|
||||
}
|
||||
380
app/Console/Commands/TestUserMakeAboOrder.php
Normal file
380
app/Console/Commands/TestUserMakeAboOrder.php
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\UserAbo;
|
||||
use App\Cron\UserMakeOrder;
|
||||
use App\Services\AboHelper;
|
||||
use App\Models\UserAboOrder;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TestUserMakeAboOrder extends Command
|
||||
{
|
||||
/**
|
||||
* php artisan test:user-make-abo-order {--abo_id=} {--date=} {--dry-run} {--force}
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'test:user-make-abo-order
|
||||
{--abo_id= : Test für spezifisches Abo (ID)}
|
||||
{--date= : Test-Datum im Format Y-m-d (Standard: heute)}
|
||||
{--dry-run : Nur anzeigen, keine Bestellung erstellen}
|
||||
{--force : Überschreibt Duplikatsprüfung}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Testet die Abo-Bestellungserstellung für ein oder mehrere Abos';
|
||||
|
||||
private $timeStart;
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->timeStart = microtime(true);
|
||||
$this->info('=== Test: UserMakeAboOrder ===');
|
||||
$this->newLine();
|
||||
|
||||
try {
|
||||
$aboId = $this->option('abo_id');
|
||||
$testDate = $this->option('date') ? Carbon::parse($this->option('date'))->format('Y-m-d') : Carbon::now()->format('Y-m-d');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$force = $this->option('force');
|
||||
|
||||
$this->info("Test-Datum: {$testDate}");
|
||||
if ($dryRun) {
|
||||
$this->warn('DRY-RUN Modus: Es werden keine Bestellungen erstellt!');
|
||||
}
|
||||
if ($force) {
|
||||
$this->warn('FORCE Modus: Duplikatsprüfung wird überschrieben!');
|
||||
}
|
||||
$this->newLine();
|
||||
|
||||
if ($aboId) {
|
||||
// Test für spezifisches Abo
|
||||
$userAbo = UserAbo::find($aboId);
|
||||
if (!$userAbo) {
|
||||
$this->error("Abo mit ID {$aboId} nicht gefunden!");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->testSingleAbo($userAbo, $testDate, $dryRun, $force);
|
||||
} else {
|
||||
// Test für alle fälligen Abos
|
||||
$this->testAllAbos($testDate, $dryRun, $force);
|
||||
}
|
||||
|
||||
$executionTime = $this->getExecutionTime();
|
||||
$this->newLine();
|
||||
$this->info("Test erfolgreich abgeschlossen in {$executionTime}");
|
||||
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Fehler beim Testen: ' . $e->getMessage());
|
||||
$this->error($e->getTraceAsString());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet ein einzelnes Abo
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param string $testDate
|
||||
* @param bool $dryRun
|
||||
* @param bool $force
|
||||
* @return void
|
||||
*/
|
||||
private function testSingleAbo($userAbo, $testDate, $dryRun, $force)
|
||||
{
|
||||
$this->info("Teste Abo ID: {$userAbo->id}");
|
||||
$this->displayAboInfo($userAbo);
|
||||
|
||||
// Prüfe ob Abo für Test-Datum fällig ist
|
||||
if ($userAbo->next_date != $testDate && !$force) {
|
||||
$this->warn("Abo ist nicht für {$testDate} fällig (next_date: {$userAbo->next_date})");
|
||||
if (!$this->confirm('Trotzdem fortfahren?', false)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe auf Duplikate
|
||||
if (!$force) {
|
||||
$existingOrder = UserAboOrder::where('user_abo_id', $userAbo->id)
|
||||
->whereDate('created_at', $testDate)
|
||||
->first();
|
||||
|
||||
if ($existingOrder) {
|
||||
$this->warn("Es existiert bereits eine Bestellung für dieses Abo am {$testDate}");
|
||||
$this->info("Bestell-ID: {$existingOrder->shopping_order_id}");
|
||||
if (!$this->confirm('Trotzdem fortfahren?', false)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('Starte Bestellungserstellung...');
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info('[DRY-RUN] Bestellung würde erstellt werden');
|
||||
$this->displayOrderPreview($userAbo);
|
||||
} else {
|
||||
// Temporär next_date setzen für Test
|
||||
$originalNextDate = $userAbo->next_date;
|
||||
if ($userAbo->next_date != $testDate) {
|
||||
$userAbo->next_date = $testDate;
|
||||
$userAbo->save();
|
||||
$this->info("Temporär next_date auf {$testDate} gesetzt");
|
||||
}
|
||||
|
||||
try {
|
||||
$shoppingOrder = $this->makeOrder($userAbo, $dryRun);
|
||||
|
||||
if ($shoppingOrder) {
|
||||
$this->info("✓ Bestellung erfolgreich erstellt: ID {$shoppingOrder->id}");
|
||||
} else {
|
||||
$this->error("✗ Bestellung konnte nicht erstellt werden");
|
||||
}
|
||||
} finally {
|
||||
// next_date zurücksetzen falls geändert
|
||||
if ($originalNextDate != $testDate) {
|
||||
$userAbo->next_date = $originalNextDate;
|
||||
$userAbo->save();
|
||||
$this->info("next_date zurückgesetzt auf {$originalNextDate}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet alle fälligen Abos
|
||||
*
|
||||
* @param string $testDate
|
||||
* @param bool $dryRun
|
||||
* @param bool $force
|
||||
* @return void
|
||||
*/
|
||||
private function testAllAbos($testDate, $dryRun, $force)
|
||||
{
|
||||
$query = UserAbo::where('next_date', '=', $testDate)
|
||||
->where('active', true);
|
||||
|
||||
if (!$force) {
|
||||
$query->whereDoesntHave('user_abo_orders', function ($q) use ($testDate) {
|
||||
$q->whereDate('created_at', $testDate);
|
||||
});
|
||||
}
|
||||
|
||||
$userAbos = $query->get();
|
||||
$count = $userAbos->count();
|
||||
|
||||
$this->info("Gefundene fällige Abos: {$count}");
|
||||
$this->newLine();
|
||||
|
||||
if ($count === 0) {
|
||||
$this->warn('Keine fälligen Abos gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->confirm("Möchten Sie {$count} Abo(s) testen?", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
|
||||
foreach ($userAbos as $userAbo) {
|
||||
$this->info("--- Abo ID: {$userAbo->id} ---");
|
||||
$this->testSingleAbo($userAbo, $testDate, $dryRun, $force);
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Informationen über ein Abo an
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @return void
|
||||
*/
|
||||
private function displayAboInfo($userAbo)
|
||||
{
|
||||
$this->table(
|
||||
['Feld', 'Wert'],
|
||||
[
|
||||
['ID', $userAbo->id],
|
||||
['User ID', $userAbo->user_id],
|
||||
['Payone UserID', $userAbo->payone_userid],
|
||||
['Aktiv', $userAbo->active ? 'Ja' : 'Nein'],
|
||||
['Status', $userAbo->status . ' (' . ($userAbo->getStatusType() ?? 'unbekannt') . ')'],
|
||||
['Intervall', $userAbo->abo_interval],
|
||||
['Next Date', $userAbo->next_date],
|
||||
['Last Date', $userAbo->last_date ?? 'Nie'],
|
||||
['Amount', number_format($userAbo->amount / 100, 2, ',', '.') . ' €'],
|
||||
['is_for', $userAbo->is_for],
|
||||
['Clearing Type', $userAbo->clearingtype],
|
||||
['Items', $userAbo->user_abo_items->count()],
|
||||
]
|
||||
);
|
||||
|
||||
// Zeige Abo-Items
|
||||
if ($userAbo->user_abo_items->count() > 0) {
|
||||
$this->info('Abo-Items:');
|
||||
$items = [];
|
||||
foreach ($userAbo->user_abo_items as $item) {
|
||||
$items[] = [
|
||||
'Product ID' => $item->product_id,
|
||||
'Qty' => $item->qty,
|
||||
'Comp' => $item->comp ?? '-',
|
||||
'Price' => number_format($item->price / 100, 2, ',', '.') . ' €',
|
||||
];
|
||||
}
|
||||
$this->table(['Product ID', 'Qty', 'Comp', 'Price'], $items);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Vorschau der Bestellung an
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @return void
|
||||
*/
|
||||
private function displayOrderPreview($userAbo)
|
||||
{
|
||||
$this->info('Bestell-Vorschau:');
|
||||
$this->info('- Shopping-User würde erstellt/aktualisiert');
|
||||
$this->info('- Bestellung würde mit folgenden Items erstellt:');
|
||||
|
||||
foreach ($userAbo->user_abo_items as $item) {
|
||||
$product = $item->product;
|
||||
$this->info(" • Product ID {$item->product_id}: {$item->qty}x");
|
||||
}
|
||||
|
||||
$this->info('- Zahlung würde durchgeführt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Bestellung für ein Abo (vereinfachte Version für Test)
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param bool $dryRun
|
||||
* @return mixed
|
||||
*/
|
||||
private function makeOrder($userAbo, $dryRun = false)
|
||||
{
|
||||
$this->info('Erstelle Shopping-User...');
|
||||
$userOrder = new UserMakeOrder($userAbo);
|
||||
|
||||
if (!$userOrder->createShoppingUser()) {
|
||||
$this->error('Konnte Shopping-User nicht erstellen');
|
||||
return null;
|
||||
}
|
||||
$this->info('✓ Shopping-User erstellt');
|
||||
|
||||
$this->info('Erstelle Bestellung...');
|
||||
$shoppingOrder = $userOrder->makeShoppingOrder();
|
||||
$shoppingOrder->mode = 'test'; //immer im test mode testen
|
||||
$shoppingOrder->save();
|
||||
if (!$shoppingOrder) {
|
||||
$this->error('Konnte Bestellung nicht erstellen');
|
||||
return null;
|
||||
}
|
||||
$this->info("✓ Bestellung erstellt: ID {$shoppingOrder->id}");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info('[DRY-RUN] Zahlung würde durchgeführt');
|
||||
$this->info('[DRY-RUN] Abo würde aktualisiert');
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
$this->info('Starte Zahlungsvorgang...');
|
||||
try {
|
||||
$response = $userOrder->makePayment();
|
||||
|
||||
if (is_object($response)) {
|
||||
$response = (array) $response;
|
||||
}
|
||||
|
||||
$this->info('Zahlungsantwort: ' . json_encode($response, JSON_PRETTY_PRINT));
|
||||
|
||||
if (!isset($response['status'])) {
|
||||
$this->warn('⚠ Kein Status in Zahlungsantwort');
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
if ($response['status'] === 'APPROVED') {
|
||||
$this->info('✓ Zahlung erfolgreich');
|
||||
$this->info('Aktualisiere Abo...');
|
||||
$this->updateAbo($userAbo, $shoppingOrder, 1);
|
||||
$this->info('✓ Abo aktualisiert');
|
||||
} elseif ($response['status'] === 'ERROR') {
|
||||
$this->error('✗ Zahlungsfehler');
|
||||
$this->warn('Abo wird beim nächsten Cron-Lauf erneut versucht');
|
||||
} else {
|
||||
$this->warn("⚠ Zahlungsstatus: {$response['status']}");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Fehler bei Zahlung: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Abo nach erfolgreicher Bestellung (vereinfachte Version)
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
* @param int $status
|
||||
* @return void
|
||||
*/
|
||||
private function updateAbo($userAbo, $shoppingOrder, $status = 1)
|
||||
{
|
||||
try {
|
||||
DB::transaction(function () use ($userAbo, $shoppingOrder, $status) {
|
||||
$updateData = [
|
||||
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
|
||||
'last_date' => now(),
|
||||
];
|
||||
|
||||
if ($status !== 1) {
|
||||
$updateData['status'] = $status;
|
||||
}
|
||||
|
||||
$userAbo->update($updateData);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => $status,
|
||||
]);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Fehler beim Aktualisieren des Abos: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Ausführungszeit
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getExecutionTime()
|
||||
{
|
||||
$diff = microtime(true) - $this->timeStart;
|
||||
$sec = intval($diff);
|
||||
$micro = $diff - $sec;
|
||||
|
||||
return $sec . ' Sekunden und ' . round($micro * 1000, 2) . ' ms';
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ use App\Services\AboHelper;
|
|||
use App\Models\UserAboOrder;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserMakeAboOrder extends Command
|
||||
{
|
||||
|
|
@ -87,12 +88,17 @@ class UserMakeAboOrder extends Command
|
|||
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Suche nach fälligen Abos für Datum', ['date' => $dateNow]);
|
||||
|
||||
// Prüfe auf bereits verarbeitete Abos am heutigen Tag (Duplikatsprüfung)
|
||||
$userAbos = UserAbo::where('next_date', '=', $dateNow)
|
||||
->where('active', true)
|
||||
->where('status', '=', 2) //abo_okay
|
||||
->whereDoesntHave('user_abo_orders', function ($query) use ($dateNow) {
|
||||
$query->whereDate('created_at', $dateNow);
|
||||
})
|
||||
->get();
|
||||
|
||||
$count = $userAbos->count();
|
||||
\Log::channel('abo_order')->info("UserMakeAboOrder: {$count} fällige Abos gefunden");
|
||||
\Log::channel('abo_order')->info("UserMakeAboOrder: {$count} fällige Abos gefunden (ohne bereits verarbeitete)");
|
||||
$this->info("Gefundene fällige Abos: {$count}");
|
||||
|
||||
foreach ($userAbos as $userAbo) {
|
||||
|
|
@ -104,7 +110,38 @@ class UserMakeAboOrder extends Command
|
|||
$this->info("Verarbeite Abo: {$userAbo->id} (PayoneUserid: {$userAbo->payone_userid})");
|
||||
|
||||
try {
|
||||
$shoppingOrder = $this->makeOrder($userAbo);
|
||||
// Locking-Mechanismus: Verhindert Race Conditions bei paralleler Ausführung
|
||||
$shoppingOrder = DB::transaction(function () use ($userAbo, $dateNow) {
|
||||
// Lock das Abo für Update, um Race Conditions zu vermeiden
|
||||
$lockedAbo = UserAbo::where('id', $userAbo->id)
|
||||
->where('next_date', '=', $dateNow)
|
||||
->where('active', true)
|
||||
->where('status', '=', 2) //abo_okay
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$lockedAbo) {
|
||||
\Log::channel('abo_order')->warning('UserMakeAboOrder: Abo wurde bereits verarbeitet oder ist nicht mehr aktiv', [
|
||||
'abo_id' => $userAbo->id
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Nochmalige Prüfung auf Duplikat innerhalb der Transaktion
|
||||
$existingOrder = UserAboOrder::where('user_abo_id', $lockedAbo->id)
|
||||
->whereDate('created_at', $dateNow)
|
||||
->first();
|
||||
|
||||
if ($existingOrder) {
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo wurde bereits heute verarbeitet', [
|
||||
'abo_id' => $lockedAbo->id,
|
||||
'existing_order_id' => $existingOrder->shopping_order_id
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->makeOrder($lockedAbo);
|
||||
}, 3); // 3 Versuche bei Deadlocks
|
||||
|
||||
if ($shoppingOrder) {
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Bestellung erstellt', [
|
||||
|
|
@ -163,6 +200,11 @@ class UserMakeAboOrder extends Command
|
|||
$response = $userOrder->makePayment();
|
||||
$this->info('makePayment response: ' . json_encode($response));
|
||||
|
||||
// Prüfe ob Response ein Array ist (kann auch Objekt sein)
|
||||
if (is_object($response)) {
|
||||
$response = (array) $response;
|
||||
}
|
||||
|
||||
if (!isset($response['status'])) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Ungültige Zahlungsantwort', [
|
||||
'abo_id' => $userAbo->id,
|
||||
|
|
@ -170,6 +212,10 @@ class UserMakeAboOrder extends Command
|
|||
'response' => $response
|
||||
]);
|
||||
$this->error("Ungültige Zahlungsantwort für Abo {$userAbo->id}");
|
||||
|
||||
// Bei fehlender Status-Information: Abo nicht aktualisieren, damit es beim nächsten Lauf erneut versucht wird
|
||||
// Aber Bestellung speichern für Nachverfolgung
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Ungültige Zahlungsantwort - kein Status');
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +226,7 @@ class UserMakeAboOrder extends Command
|
|||
'response' => $response
|
||||
]);
|
||||
$this->info("Zahlung erfolgreich für Abo {$userAbo->id}");
|
||||
// Nur bei erfolgreicher Zahlung: next_date aktualisieren
|
||||
$this->updateAbo($userAbo, $shoppingOrder, 1);
|
||||
} elseif ($response['status'] === 'ERROR') {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Zahlungsfehler', [
|
||||
|
|
@ -196,24 +243,39 @@ class UserMakeAboOrder extends Command
|
|||
$response
|
||||
);
|
||||
|
||||
$this->updateAbo($userAbo, $shoppingOrder, 3);
|
||||
// Bei Zahlungsfehler: Status setzen, aber next_date NICHT aktualisieren
|
||||
// Damit wird das Abo beim nächsten Cron-Lauf erneut versucht
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 3, $response);
|
||||
|
||||
$shoppingPayment = $userOrder->getShoppingPayment();
|
||||
$data = [
|
||||
'mode' => $shoppingPayment->mode,
|
||||
'txaction' => 'error',
|
||||
'send_link' => false,
|
||||
'payment_error' => $response,
|
||||
];
|
||||
if ($shoppingPayment) {
|
||||
$data = [
|
||||
'mode' => $shoppingPayment->mode,
|
||||
'txaction' => 'error',
|
||||
'send_link' => false,
|
||||
'payment_error' => $response,
|
||||
];
|
||||
|
||||
Payment::paymentStatusSendMail($shoppingOrder, $shoppingPayment, $data);
|
||||
Payment::paymentStatusSendMail($shoppingOrder, $shoppingPayment, $data);
|
||||
}
|
||||
} elseif ($response['status'] === 'PENDING' || $response['status'] === 'REDIRECT') {
|
||||
// Pending/Redirect Status: Bestellung speichern, aber Abo nicht aktualisieren
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Zahlung ausstehend/weiterleitung', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'status' => $response['status']
|
||||
]);
|
||||
$this->info("Zahlung ausstehend für Abo {$userAbo->id}: {$response['status']}");
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Zahlung ausstehend: ' . $response['status']);
|
||||
} else {
|
||||
// Unbekannter Status: Bestellung speichern, aber Abo nicht aktualisieren
|
||||
\Log::channel('abo_order')->warning('UserMakeAboOrder: Unbekannter Zahlungsstatus', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'status' => $response['status']
|
||||
]);
|
||||
$this->warn("Unbekannter Zahlungsstatus für Abo {$userAbo->id}: {$response['status']}");
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Unbekannter Status: ' . $response['status']);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Ausnahme bei der Bestellungserstellung', [
|
||||
|
|
@ -222,13 +284,19 @@ class UserMakeAboOrder extends Command
|
|||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
$this->error("Ausnahme bei Abo {$userAbo->id}: " . $e->getMessage());
|
||||
|
||||
// Bei Exception: Bestellung speichern falls vorhanden, aber Abo nicht aktualisieren
|
||||
if ($shoppingOrder) {
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Exception: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Abo nach einer Bestellung
|
||||
* Aktualisiert das Abo nach einer erfolgreichen Bestellung
|
||||
* Aktualisiert next_date für den nächsten Abo-Zyklus
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
|
|
@ -237,7 +305,7 @@ class UserMakeAboOrder extends Command
|
|||
*/
|
||||
private function updateAbo($userAbo, $shoppingOrder, $status = 1)
|
||||
{
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo', [
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo nach erfolgreicher Zahlung', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'status' => $status
|
||||
|
|
@ -245,34 +313,98 @@ class UserMakeAboOrder extends Command
|
|||
|
||||
$this->info("Aktualisiere Abo: {$userAbo->id} mit Status {$status}");
|
||||
|
||||
$updateData = [
|
||||
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
|
||||
'last_date' => now(),
|
||||
];
|
||||
|
||||
if ($status !== 1) {
|
||||
$updateData['status'] = $status;
|
||||
}
|
||||
|
||||
try {
|
||||
$userAbo->update($updateData);
|
||||
DB::transaction(function () use ($userAbo, $shoppingOrder, $status) {
|
||||
$updateData = [
|
||||
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
|
||||
'last_date' => now(),
|
||||
];
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => $status,
|
||||
]);
|
||||
if ($status !== 1) {
|
||||
$updateData['status'] = $status;
|
||||
}
|
||||
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo erfolgreich aktualisiert', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'next_date' => $updateData['next_date']
|
||||
]);
|
||||
$userAbo->update($updateData);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => $status,
|
||||
'paid' => false,
|
||||
]);
|
||||
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo erfolgreich aktualisiert', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'next_date' => $updateData['next_date']
|
||||
]);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler beim Aktualisieren des Abos', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
|
||||
throw $e; // Re-throw für besseres Error-Handling
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Abo bei Fehlern - OHNE next_date zu aktualisieren
|
||||
* Damit wird das Abo beim nächsten Cron-Lauf erneut versucht
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
* @param int|string $status Status-Code oder Fehlermeldung
|
||||
* @param array|null $errorResponse Optionale Fehlerantwort von Payment
|
||||
* @return void
|
||||
*/
|
||||
private function updateAboOnError($userAbo, $shoppingOrder, $status, $errorResponse = null)
|
||||
{
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo bei Fehler (ohne next_date)', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'status' => $status
|
||||
]);
|
||||
|
||||
$this->info("Aktualisiere Abo bei Fehler: {$userAbo->id} (Status: {$status})");
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($userAbo, $shoppingOrder, $status) {
|
||||
// Nur last_date aktualisieren, next_date bleibt unverändert
|
||||
// Damit wird das Abo beim nächsten Cron-Lauf erneut versucht
|
||||
$updateData = [
|
||||
'last_date' => now(),
|
||||
];
|
||||
|
||||
// Status nur setzen wenn es ein numerischer Wert ist
|
||||
if (is_numeric($status)) {
|
||||
$updateData['status'] = $status;
|
||||
}
|
||||
|
||||
$userAbo->update($updateData);
|
||||
|
||||
// UserAboOrder mit Fehlerstatus speichern
|
||||
$orderStatus = is_numeric($status) ? $status : 3; // Default zu 3 (abo_hold) wenn String
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => $orderStatus,
|
||||
'paid' => false,
|
||||
]);
|
||||
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo bei Fehler aktualisiert (next_date unverändert)', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'next_date' => $userAbo->next_date,
|
||||
'status' => $status
|
||||
]);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler beim Aktualisieren des Abos bei Fehler', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
|
||||
// Bei Fehler hier nicht re-throw, damit der Hauptprozess fortgesetzt werden kann
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
namespace App\Console;
|
||||
|
||||
use App\Console\Commands\BusinessStore;
|
||||
use App\Console\Commands\BusinessStoreOptimized;
|
||||
use App\Console\Commands\CheckPaymentsAccount;
|
||||
use App\Console\Commands\UserMakeAboOrder;
|
||||
use App\Console\Commands\DhlUpdateTracking;
|
||||
use App\Console\Commands\UserCleanup;
|
||||
use App\Console\Commands\UserMakeAboOrder;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
|
|
@ -18,15 +20,16 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected $commands = [
|
||||
BusinessStore::class,
|
||||
BusinessStoreOptimized::class,
|
||||
CheckPaymentsAccount::class,
|
||||
UserMakeAboOrder::class,
|
||||
UserCleanup::class,
|
||||
DhlUpdateTracking::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
|
|
@ -36,10 +39,19 @@ class Kernel extends ConsoleKernel
|
|||
// Jobs 2, 3, 4: Die Befehle aus deinem alten Shell-Skript.
|
||||
// Werden nacheinander täglich zu unterschiedlichen Zeiten ausgeführt,
|
||||
// um die Serverlast zu verteilen.
|
||||
$schedule->command('store-optimized 0 0')->dailyAt('03:00');
|
||||
$schedule->command('business:store-optimized 0 0')->dailyAt('03:00');
|
||||
|
||||
$schedule->command('user:cleanup')->dailyAt('03:30');
|
||||
$schedule->command('user:make_abo_order')->dailyAt('04:00');
|
||||
|
||||
// Cleanup old log files weekly (keeps logs for 30 days)
|
||||
$schedule->command('logs:cleanup --days=30')->weekly()->sundays()->at('05:00');
|
||||
|
||||
// DHL Tracking Update: Täglich um 06:00 Uhr, automatische E-Mails bei Transit-Status
|
||||
$schedule->command('dhl:update-tracking --days=14 --send-emails')
|
||||
->dailyAt('06:00')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,7 +61,7 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue