mivita/dev/buinessPlan/_bak/TreeCalcBotOptimized.php
2026-01-23 17:35:23 +01:00

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");
}
}
}