Cocker Setup

This commit is contained in:
Kevin Adametz 2025-08-29 14:48:03 +02:00
parent 8fdaa0ba1d
commit 8c11130b5d
191 changed files with 8152 additions and 18186 deletions

View file

@ -1,599 +0,0 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use Carbon\Carbon;
use App\Models\UserBusinessStructure;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Logging\Log as LogContract;
/**
* 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 ?BusinessUserItem $businessUser = null;
private ?BusinessUserItem $sponsor = null;
private array $processedUserIds = [];
private BusinessUserRepository $repository;
private TreeHtmlRenderer $renderer;
private LogContract $logger;
public function __construct(
int $month,
int $year,
string $initFrom = 'member',
?BusinessUserRepository $repository = null,
?TreeHtmlRenderer $renderer = null,
?LogContract $logger = null
) {
$this->validateInput($month, $year);
$this->initializeDate($month, $year);
$this->initFrom = $initFrom;
// Dependency Injection mit Fallback
$this->repository = $repository ?? new BusinessUserRepository($month, $year);
$this->renderer = $renderer ?? new TreeHtmlRenderer($initFrom);
$this->logger = $logger ?? app(LogContract::class);
}
/**
* Initialisiert die Business-Struktur für Admin-Ansicht
*/
public function initStructureAdmin(bool $check = true): void
{
try {
$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);
} else {
$this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year}");
$this->buildFreshStructure();
}
} catch (\Exception $e) {
$this->logger->error("Error initializing admin structure: " . $e->getMessage());
throw $e;
}
}
/**
* Initialisiert die Struktur für einen spezifischen User
*/
public function initStructureUser(int $userId): void
{
try {
$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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$this->addUserIdToProcessed($userId);
$this->businessUsers[] = $businessUserItem;
$storedStructure = $this->repository->getStoredStructure();
if ($storedStructure) {
$this->loadStoredParentsUsers($storedStructure);
if (isset($this->businessUsers[0]) && $this->businessUsers[0]->sponsor) {
$this->loadStoredSponsorUser($this->businessUsers[0]->sponsor->user_id);
}
} else {
$this->loadParentsUsers();
$this->loadSponsorUser($userId);
}
} catch (\Exception $e) {
$this->logger->error("Error initializing user structure for {$userId}: " . $e->getMessage());
throw $e;
}
}
/**
* Initialisiert detaillierte Business-User-Informationen
*/
public function initBusinesslUserDetail(User $user): void
{
try {
$this->logger->info("Initializing business user details for: {$user->id}");
$this->businessUser = new BusinessUserItemOptimized($this->date);
$this->businessUser->makeUserFromModel($user); // ✅ Nutzt bereits User-Objekt
$this->businessUser->checkSponsor($user);
if (!$this->businessUser->isSave()) {
// Aufbau der Struktur für den User in die unendliche Tiefe
$this->businessUser->readParentsBusinessUsers();
// Calculate Points in Lines (optimiert für Memory-Effizienz)
if (count($this->businessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($this->businessUser->businessUserItems, 1);
}
// 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)
*/
public function getGrowthBonus(): array
{
if (!$this->businessUser || !$this->businessUser->business_lines) {
return [];
}
if (count($this->businessUser->business_lines) > 6) {
$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;
}
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;
}
public static function addUserID(int $id): void
{
// Deprecated: Wird durch Instanz-Methode ersetzt
// Bleibt für Rückwärtskompatibilität erhalten
}
// ===== 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);
}
/**
* Baut frische Struktur auf
*/
private function buildFreshStructure(): void
{
$this->loadRootUsers();
$this->loadParentsUsers();
$this->loadParentlessUsers();
}
/**
* 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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$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
{
foreach ($this->businessUsers as $businessUser) {
$businessUser->readParentsBusinessUsers();
}
}
/**
* 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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$this->parentless[] = $businessUserItem;
$count++;
}
$this->logger->info("Loaded {$count} parentless users with optimized relations");
}
/**
* Lädt Sponsor für User
*/
private function loadSponsorUser(int $userId): void
{
try {
$sponsorUser = $this->repository->getSponsorForUser($userId);
if ($sponsorUser) {
$this->sponsor = new BusinessUserItem($this->date);
$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 BusinessUserItem($this->date);
$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 BusinessUserItem($this->date);
$businessUserItem->makeUser($obj->user_id);
$this->parentless[] = $businessUserItem;
}
}
}
/**
* Gespeicherten Sponsor laden
*/
private function loadStoredSponsorUser(int $userId): void
{
$this->sponsor = new BusinessUserItem($this->date);
$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): 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 3: Verarbeite in korrekter Reihenfolge (von tief zu flach)
foreach ($processingStack as $current) {
$item = $current['item'];
$line = $current['line'];
try {
// Business Line initialisieren falls nötig
if (!isset($this->businessUser->business_lines[$line])) {
$obj = new stdClass();
$obj->points = 0;
$this->businessUser->addBusinessLineToUser($line, $obj);
}
// Punkte hinzufügen (mit Validierung)
$points = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($points > 0) {
$this->businessUser->addBusinessLinePoints($line, $points);
$this->businessUser->addTotalTP($points);
}
$this->logger->debug("Processed user {$current['id']} at line {$line} with {$points} points");
} catch (\Exception $e) {
$this->logger->error("Error processing user points for {$current['id']}: " . $e->getMessage());
}
}
$this->logger->info("Processed " . count($processingStack) . " business user items in depth-first order");
}
/**
* User-ID zu verarbeiteten IDs hinzufügen
*/
private function addUserIdToProcessed(int $id): void
{
$this->processedUserIds[$id] = true;
}
/**
* Prüft ob User bereits verarbeitet wurde
*/
private 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");
}
}
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");
}
}
}

View file

@ -1,272 +0,0 @@
<?php
namespace App\Services;
use App\Models\UserShop;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
/**
* Domain Service - Centralized domain and subdomain management
*
* This service provides a centralized way to handle domain resolution,
* subdomain validation, and domain-specific configuration management.
*/
class DomainService
{
private const CACHE_TTL = 3600; // 1 hour
private const FIXED_SUBDOMAINS = ['my', 'in', 'checkout'];
private array $domainConfig;
public function __construct()
{
$this->domainConfig = $this->loadDomainConfiguration();
}
/**
* Load domain configuration from config files
*/
private function loadDomainConfiguration(): array
{
return [
'main_domain' => config('app.domain'),
'main_tld' => config('app.tld_care'),
'shop_tld' => config('app.tld_shop'),
'protocol' => config('app.protocol'),
'subdomains' => [
'crm' => config('app.pre_url_crm', 'my.'),
'portal' => config('app.pre_url_portal', 'in.'),
'checkout' => config('app.pre_url_checkout', 'checkout.'),
]
];
}
/**
* Determine the type of subdomain
*/
public function getSubdomainType(string $subdomain): string
{
// Check if it's a fixed subdomain
if (in_array($subdomain, self::FIXED_SUBDOMAINS)) {
return match($subdomain) {
'my' => 'crm',
'in' => 'portal',
'checkout' => 'checkout',
default => 'unknown'
};
}
// Check if it's a valid user shop
if ($this->isValidUserShop($subdomain)) {
return 'user-shop';
}
return 'unknown';
}
/**
* Check if a subdomain represents a valid user shop
*/
public function isValidUserShop(string $slug): bool
{
$cacheKey = "user_shop_valid_{$slug}";
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($slug) {
$userShop = UserShop::where('slug', $slug)->first();
if (!$userShop) {
return false;
}
if (!$userShop->active) {
return false;
}
if (!$userShop->user) {
return false;
}
if (!$userShop->user->isActiveShop()) {
return false;
}
return true;
});
}
/**
* Get user shop by slug with caching
*/
public function getUserShop(string $slug): ?UserShop
{
if (!$this->isValidUserShop($slug)) {
return null;
}
$cacheKey = "user_shop_{$slug}";
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($slug) {
return UserShop::where('slug', $slug)
->with('user')
->first();
});
}
/**
* Parse domain from request and determine context
*/
public function parseDomain(string $host): array
{
$parts = explode('.', $host);
// Handle different TLD scenarios
if (count($parts) < 2) {
return [
'type' => 'invalid',
'domain' => $host,
'subdomain' => null,
'tld' => null
];
}
// Extract TLD and domain
$tld = '.' . end($parts);
$domain = $parts[count($parts) - 2];
// Check for subdomain
$subdomain = null;
if (count($parts) > 2) {
$subdomain = $parts[0];
}
// Determine domain type
$type = $this->determineDomainType($domain, $subdomain, $tld);
return [
'type' => $type,
'domain' => $domain,
'subdomain' => $subdomain,
'tld' => $tld,
'full_domain' => $host
];
}
/**
* Determine domain type based on domain, subdomain, and TLD
*/
private function determineDomainType(string $domain, ?string $subdomain, string $tld): string
{
// Check if it's the main domain
if ($domain === $this->domainConfig['main_domain']) {
if ($subdomain === null) {
// Main domain without subdomain
if ($tld === $this->domainConfig['shop_tld']) {
return 'main-shop'; // mivita.shop
}
return 'main'; // mivita.care
}
// Main domain with subdomain
return $this->getSubdomainType($subdomain);
}
return 'unknown';
}
/**
* Build URL for specific domain type
*/
public function buildUrl(string $type, ?string $path = null, ?string $slug = null): string
{
$base = $this->domainConfig['protocol'];
switch ($type) {
case 'main':
$base .= $this->domainConfig['main_domain'] . $this->domainConfig['main_tld'];
break;
case 'main-shop':
$base .= $this->domainConfig['main_domain'] . $this->domainConfig['shop_tld'];
break;
case 'crm':
$base .= rtrim($this->domainConfig['subdomains']['crm'], '.') .
$this->domainConfig['main_domain'] . $this->domainConfig['main_tld'];
break;
case 'portal':
$base .= rtrim($this->domainConfig['subdomains']['portal'], '.') .
$this->domainConfig['main_domain'] . $this->domainConfig['main_tld'];
break;
case 'checkout':
$base .= rtrim($this->domainConfig['subdomains']['checkout'], '.') .
$this->domainConfig['main_domain'] . $this->domainConfig['main_tld'];
break;
case 'user-shop':
if (!$slug) {
throw new \InvalidArgumentException('Slug required for user-shop URLs');
}
$base .= $slug . '.' . $this->domainConfig['main_domain'] . $this->domainConfig['main_tld'];
break;
default:
throw new \InvalidArgumentException("Unknown domain type: {$type}");
}
if ($path) {
$base .= '/' . ltrim($path, '/');
}
return $base;
}
/**
* Get domain configuration
*/
public function getDomainConfiguration(): array
{
return $this->domainConfig;
}
/**
* Clear user shop cache
*/
public function clearUserShopCache(string $slug): void
{
Cache::forget("user_shop_valid_{$slug}");
Cache::forget("user_shop_{$slug}");
}
/**
* Get default user shop for main domain (fallback)
*/
public function getDefaultUserShop(): ?UserShop
{
return $this->getUserShop('aloevera'); // Current hardcoded fallback
}
/**
* Validate domain configuration
*/
public function validateConfiguration(): array
{
$errors = [];
if (empty($this->domainConfig['main_domain'])) {
$errors[] = 'Main domain not configured';
}
if (empty($this->domainConfig['main_tld'])) {
$errors[] = 'Main TLD not configured';
}
if (empty($this->domainConfig['protocol'])) {
$errors[] = 'Protocol not configured';
}
return $errors;
}
}