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