update 20.10.2025

This commit is contained in:
Kevin Adametz 2025-10-20 17:42:08 +02:00
parent 8c11130b5d
commit a939cd51ef
616 changed files with 84821 additions and 4121 deletions

View file

@ -7,6 +7,8 @@ use stdClass;
use Carbon\Carbon;
use App\Models\UserLevel;
use App\Models\UserBusiness;
use App\Models\UserAccount;
use App\Models\UserSalesVolume;
use App\Services\TranslationHelper;
use App\Models\UserBusinessStructure;
use Illuminate\Support\Facades\Log;
@ -26,12 +28,14 @@ class BusinessUserItemOptimized
private $date;
private $b_user;
private ?TreeCalcBotOptimized $treeCalcBot = null;
private $user_level_active_pos;
private $needsQualificationRecalculation = false;
public function __construct($date)
public function __construct($date, ?TreeCalcBotOptimized $treeCalcBot = null)
{
$this->date = $date;
$this->treeCalcBot = $treeCalcBot;
$this->businessUserItems = []; // Initialize array
return $this;
}
@ -51,22 +55,22 @@ class BusinessUserItemOptimized
->where('month', $this->date->month)
->where('year', $this->date->year)
->first();
if ($this->b_user !== null) {
\Log::debug("BusinessUserItem: Using stored data for user {$user_id} ({$this->date->month}/{$this->date->year})");
// WICHTIG: Auch bei gespeicherten Daten User-Model laden für Grunddaten
$user = User::with(['account', 'user_level'])->find($user_id);
if ($user) {
$this->enrichStoredDataWithUserModel($user);
// Prüfe ob Level-Qualifikationsdaten nachberechnet werden müssen
if ($this->needsQualificationRecalculation) {
\Log::debug("BusinessUserItem: Triggering qualification recalculation for user {$user_id}");
$this->calcQualPP(); // Berechne fehlende Level-Qualifikationsdaten
}
}
return; // Bereits gespeicherte Daten verwenden
}
} else {
@ -75,20 +79,19 @@ class BusinessUserItemOptimized
// Lade User mit Relations (weniger effizient als makeUserFromModel)
$user = User::with(['account', 'user_level'])->find($user_id);
if (!$user) {
\Log::warning("BusinessUserItem: User not found: {$user_id}");
return;
}
$this->initializeFromUserModel($user);
// WICHTIG: Bei Live-Berechnung auch Level-Qualifikationsdaten berechnen
// (nicht bei forceLiveCalculation=false, da dort gespeicherte Daten bevorzugt werden)
if ($forceLiveCalculation) {
$this->calcQualPP();
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error creating user {$user_id}: " . $e->getMessage());
throw $e;
@ -115,19 +118,19 @@ class BusinessUserItemOptimized
->where('month', $this->date->month)
->where('year', $this->date->year)
->first();
if ($this->b_user !== null) {
\Log::debug("BusinessUserItem: Using stored data for user {$user->id} ({$this->date->month}/{$this->date->year})");
// WICHTIG: Auch bei gespeicherten Daten User-Grunddaten anreichern
$this->enrichStoredDataWithUserModel($user);
// Prüfe ob Level-Qualifikationsdaten nachberechnet werden müssen
if ($this->needsQualificationRecalculation) {
\Log::debug("BusinessUserItem: Triggering qualification recalculation for user {$user->id}");
$this->calcQualPP(); // Berechne fehlende Level-Qualifikationsdaten
}
return; // Bereits berechnete Daten verwenden
}
} else {
@ -136,13 +139,12 @@ class BusinessUserItemOptimized
// Erstelle neuen User und führe Live-Berechnung durch
$this->initializeFromUserModel($user);
// WICHTIG: Bei Live-Berechnung auch Level-Qualifikationsdaten berechnen
// (nicht bei forceLiveCalculation=false, da dort gespeicherte Daten bevorzugt werden)
if ($forceLiveCalculation) {
$this->calcQualPP();
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error creating user from model {$user->id}: " . $e->getMessage());
throw $e;
@ -157,7 +159,7 @@ class BusinessUserItemOptimized
// Nutze geladene Relations wenn verfügbar
$user_level_active = null;
if ($user->relationLoaded('user_level')) {
$user_level_active = $user->user_level;
$user_level_active = $user->user_level;
} else {
$user_level_active = $user->user_level; // Fallback auf Original-Relation
}
@ -166,11 +168,8 @@ class BusinessUserItemOptimized
// Neues UserBusiness Objekt erstellen
$this->b_user = new UserBusiness();
// Account-Daten (mit Error-Handling)
$account = $user->relationLoaded('account') ? $user->account : null;
if (!$account) {
\Log::warning("BusinessUserItem: No account found for user {$user->id}");
}
// Account-Daten (mit intelligentem Laden und Error-Handling)
$account = $this->getAccountForUser($user);
$fill = [
'user_id' => $user->id,
'month' => $this->date->month,
@ -180,12 +179,12 @@ class BusinessUserItemOptimized
'active_account' => $this->calculateActiveAccount($user),
'payment_account_date' => $user->payment_account ? $user->getPaymentAccountDateFormat(false) : null,
'active_date' => $user->active_date,
// Account-Daten mit Fallback
'm_account' => $account ? $account->m_account : '',
// Account-Daten mit korrekten Fallback-Werten
'm_account' => $account ? ($account->m_account ?? null) : null,
'email' => $user->email,
'first_name' => $account ? $account->first_name : '',
'last_name' => $account ? $account->last_name : '',
'first_name' => $account ? ($account->first_name ?? '') : '',
'last_name' => $account ? ($account->last_name ?? '') : '',
'user_birthday' => $account ? $account->birthday : null,
'user_phone' => $account ? ($account->getPhoneNumber() ?? '') : '',
@ -212,17 +211,18 @@ class BusinessUserItemOptimized
'commission_growth_total' => 0,
'version' => 2,
];
$this->b_user->fill($fill);
$this->b_user->business_lines = [];
$this->b_user->user_items = [];
// Shop-Provision berechnen (mit Boundary-Check)
// Shop-Provision berechnen (mit verbessertem Logging)
$shopVolume = (float) $this->b_user->sales_volume_total_shop;
$shopMargin = (float) $this->b_user->margin_shop;
$this->b_user->commission_shop_sales = round($shopVolume / 100 * $shopMargin, 2);
$calculatedCommission = round($shopVolume / 100 * $shopMargin, 2);
$this->b_user->commission_shop_sales = $calculatedCommission;
\Log::debug("BusinessUserItem: Created optimized user {$user->id} for {$this->date->month}/{$this->date->year}");
\Log::debug("BusinessUserItem: Created optimized user {$user->id} for {$this->date->month}/{$this->date->year} - Shop commission: {$calculatedCommission} (Volume: {$shopVolume}, Margin: {$shopMargin}%)");
}
/**
@ -232,38 +232,92 @@ class BusinessUserItemOptimized
private function enrichStoredDataWithUserModel(User $user): void
{
try {
$account = $user->account;
$account = $this->getAccountForUser($user);
// Ergänze fehlende User-Grunddaten in gespeicherten UserBusiness-Daten
$this->b_user->user_id = $user->id;
$this->b_user->email = $user->email;
$this->b_user->first_name = $account ? $account->first_name : '';
$this->b_user->last_name = $account ? $account->last_name : '';
$this->b_user->first_name = $account ? ($account->first_name ?? '') : '';
$this->b_user->last_name = $account ? ($account->last_name ?? '') : '';
$this->b_user->user_birthday = $account ? $account->birthday : null;
$this->b_user->user_phone = $account ? ($account->getPhoneNumber() ?? '') : '';
$this->b_user->m_account = $account ? $account->m_account : '';
$this->b_user->m_account = $account ? ($account->m_account ?? null) : null;
// Berechne aktiven Account-Status
$this->b_user->active_account = $this->calculateActiveAccount($user);
$this->b_user->payment_account_date = $user->payment_account;
// User-Level Informationen
$user_level_active = $user->user_level;
if ($user_level_active) {
$this->b_user->user_level_name = $user_level_active->name;
$this->user_level_active_pos = $user_level_active->pos;
}
// WICHTIG: Validiere Level-Qualifikationsdaten für Struktur-Ansicht
$this->validateLevelQualificationData();
// Prüfe ob Sales Volume Felder aktualisiert werden müssen
$this->updateSalesVolumeFields($user);
\Log::debug("BusinessUserItem: Enriched stored data for user {$user->id} with current user model data");
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error enriching stored data for user {$user->id}: " . $e->getMessage());
}
}
/**
* Aktualisiert Sales Volume und Commission Felder bei gespeicherten Daten
*/
private function updateSalesVolumeFields(User $user): void
{
try {
// Prüfe ob Sales Volume Felder leer sind
$fieldsToUpdate = [
'sales_volume_KP_points',
'sales_volume_TP_points',
'sales_volume_points_shop',
'sales_volume_points_KP_sum',
'sales_volume_points_TP_sum',
'sales_volume_total',
'sales_volume_total_shop',
'sales_volume_total_sum'
];
$needsUpdate = false;
foreach ($fieldsToUpdate as $field) {
if (!isset($this->b_user->{$field}) || $this->b_user->{$field} === null || $this->b_user->{$field} === 0) {
$newValue = $this->getUserSalesVolumeOptimized($user, $field);
$this->b_user->{$field} = $newValue;
if ($newValue > 0) {
$needsUpdate = true;
\Log::debug("BusinessUserItem: Updated {$field} for user {$user->id}: {$newValue}");
}
}
}
// Aktualisiere Shop Commission falls nötig
if (!isset($this->b_user->commission_shop_sales) || $this->b_user->commission_shop_sales === 0) {
$shopVolume = (float) $this->b_user->sales_volume_total_shop;
$shopMargin = (float) $this->b_user->margin_shop;
if ($shopVolume > 0 && $shopMargin > 0) {
$calculatedCommission = round($shopVolume / 100 * $shopMargin, 2);
$this->b_user->commission_shop_sales = $calculatedCommission;
$needsUpdate = true;
\Log::debug("BusinessUserItem: Updated commission_shop_sales for user {$user->id}: {$calculatedCommission}");
}
}
if ($needsUpdate) {
\Log::info("BusinessUserItem: Updated sales volume fields for user {$user->id}");
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error updating sales volume fields for user {$user->id}: " . $e->getMessage());
}
}
/**
* Validiert und aktualisiert Level-Qualifikationsdaten wenn nötig
* Stellt sicher, dass next_qual_user_level und next_can_user_level für Struktur-Ansicht verfügbar sind
@ -278,11 +332,10 @@ class BusinessUserItemOptimized
// Wenn Level-Qualifikationsdaten fehlen, führe Neuberechnung durch
if (!$hasNextQual && !$hasNextCan && !$hasQualUserLevel) {
\Log::debug("BusinessUserItem: Level qualification data missing for user {$this->b_user->user_id}, triggering recalculation");
// Setze Flag für notwendige Neuberechnung
$this->needsQualificationRecalculation = true;
}
} catch (\Exception $e) {
\Log::warning("BusinessUserItem: Error validating level qualification data for user {$this->b_user->user_id}: " . $e->getMessage());
}
@ -307,17 +360,41 @@ class BusinessUserItemOptimized
}
/**
* Optimierte Sales Volume Abfrage (mit potenziellem Caching)
* Optimierte Sales Volume Abfrage mit detailliertem Logging
*/
private function getUserSalesVolumeOptimized(User $user, string $field)
{
try {
// Hier könnte Caching implementiert werden
$cacheKey = "sales_volume_{$user->id}_{$this->date->month}_{$this->date->year}_{$field}";
// Für jetzt: Direkter Aufruf (später durch Cache ersetzen)
return $user->getUserSalesVolumeBy($this->date->month, $this->date->year, $field);
// Direkter Aufruf mit detailliertem Logging
$value = $user->getUserSalesVolumeBy($this->date->month, $this->date->year, $field);
// Log nur bei ersten Aufruf für diesen User (Performance)
static $loggedUsers = [];
if (!isset($loggedUsers[$user->id])) {
$loggedUsers[$user->id] = true;
// Prüfe ob UserSalesVolume Daten existieren
$userSalesVolume = $user->getUserSalesVolume($this->date->month, $this->date->year, 'first');
if (!$userSalesVolume) {
\Log::info("BusinessUserItem: No UserSalesVolume found for user {$user->id} in {$this->date->month}/{$this->date->year}");
// Prüfe neueste verfügbare Daten
$latestVolume = \App\Models\UserSalesVolume::where('user_id', $user->id)
->orderBy('year', 'desc')
->orderBy('month', 'desc')
->first();
if ($latestVolume) {
\Log::info("BusinessUserItem: Latest UserSalesVolume for user {$user->id}: {$latestVolume->month}/{$latestVolume->year}");
} else {
\Log::warning("BusinessUserItem: No UserSalesVolume records found for user {$user->id} at all");
}
} else {
\Log::debug("BusinessUserItem: UserSalesVolume found for user {$user->id} in {$this->date->month}/{$this->date->year}");
}
}
return $value;
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error getting sales volume {$field} for user {$user->id}: " . $e->getMessage());
return 0; // Sicherer Fallback
@ -333,7 +410,12 @@ class BusinessUserItemOptimized
public function addUserID()
{
TreeCalcBotOptimized::addUserID($this->b_user->user_id);
if ($this->treeCalcBot) {
$this->treeCalcBot->addProcessedUserId($this->b_user->user_id);
} else {
// Fallback für Rückwärtskompatibilität - sollte in Logs sichtbar sein
\Log::warning("BusinessUserItemOptimized: TreeCalcBotOptimized Referenz fehlt für User ID: " . $this->b_user->user_id);
}
}
public function getBUser()
@ -345,7 +427,7 @@ class BusinessUserItemOptimized
{
$this->b_user->business_lines[$line] = $obj;
}
public function addBusinessLinePoints($line, $points)
{
if (!isset($this->b_user->business_lines[$line])) {
@ -354,7 +436,7 @@ class BusinessUserItemOptimized
}
$obj = $this->b_user->business_lines[$line];
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($obj)) {
$obj['points'] = ($obj['points'] ?? 0) + (float) $points;
@ -365,7 +447,7 @@ class BusinessUserItemOptimized
}
$obj->points = ($obj->points ?? 0) + (float) $points;
}
$this->b_user->business_lines[$line] = $obj;
}
@ -373,7 +455,7 @@ class BusinessUserItemOptimized
{
$this->b_user->total_pp += (float) $points; // Type-Safety
}
public function isQualKP(): bool
{
return ($this->b_user->sales_volume_points_KP_sum >= $this->b_user->qual_kp);
@ -409,9 +491,9 @@ class BusinessUserItemOptimized
public function getCommissionTotal(): float
{
return round(
$this->b_user->commission_shop_sales +
$this->b_user->commission_pp_total +
$this->b_user->commission_growth_total,
$this->b_user->commission_shop_sales +
$this->b_user->commission_pp_total +
$this->b_user->commission_growth_total,
2
);
}
@ -452,8 +534,8 @@ class BusinessUserItemOptimized
for ($i = 1; $i <= $qualUserLevel->paylines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$object = $this->b_user->business_lines[$i];
$margin = (float) $this->b_user->qual_user_level['pr_line_'.$i];
$margin = (float) $this->b_user->qual_user_level['pr_line_' . $i];
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($object)) {
$points = (float) ($object['points'] ?? 0);
@ -468,7 +550,7 @@ class BusinessUserItemOptimized
$object->payline = true;
$commission_pp_total += $object->commission;
}
$this->b_user->business_lines[$i] = $object;
}
}
@ -482,7 +564,7 @@ class BusinessUserItemOptimized
for ($i = $payline; $i <= $maxlines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$object = $this->b_user->business_lines[$i];
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($object)) {
$points = (float) ($object['points'] ?? 0);
@ -497,7 +579,7 @@ class BusinessUserItemOptimized
$object->growth_bonus = true;
$commission_growth_total += $object->commission;
}
$this->b_user->business_lines[$i] = $object;
}
}
@ -519,7 +601,7 @@ class BusinessUserItemOptimized
foreach ($qualUserLevels as $qualUserLevel) {
$payline_points = $this->getPointsforPayline($qualUserLevel->paylines);
$payline_points_qual_kp = $payline_points + $this->getRestQualKP();
if ($payline_points_qual_kp >= $qualUserLevel->qual_pp) {
$this->b_user->payline_points = $payline_points;
$this->b_user->payline_points_qual_kp = $payline_points_qual_kp;
@ -535,7 +617,7 @@ class BusinessUserItemOptimized
for ($i = 1; $i <= $paylines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$line = $this->b_user->business_lines[$i];
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($line)) {
$payline_points += (float) ($line['points'] ?? 0);
@ -561,17 +643,17 @@ class BusinessUserItemOptimized
->first();
if ($qualUserLevelNext) {
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
}else{
} else {
$this->b_user->qual_user_level_next = null;
}
}else{
} else {
$this->b_user->qual_user_level_next = null;
}
}
private function setNextUserLevel($force = false): void
{
//sucht den nächsten level, der mehr points hat als das aktuelle level
//sucht den nächsten level, der mehr points hat als das aktuelle level
$nextQualUserLevel = UserLevel::where('qual_pp', '<=', $this->b_user->payline_points_qual_kp)
->where('pos', '>', $this->user_level_active_pos)
->orderBy('qual_pp', 'desc')
@ -608,7 +690,7 @@ class BusinessUserItemOptimized
if (isset($this->b_user->$name)) {
return $this->b_user->$name;
}
// Legacy-Properties
$legacyMap = [
'sales_volume_points_KP_sum' => 'sales_volume_points_KP_sum',
@ -616,11 +698,11 @@ class BusinessUserItemOptimized
'business_lines' => 'business_lines',
'user_id' => 'user_id'
];
if (isset($legacyMap[$name]) && isset($this->b_user->{$legacyMap[$name]})) {
return $this->b_user->{$legacyMap[$name]};
}
return null;
}
@ -648,13 +730,15 @@ class BusinessUserItemOptimized
if ($user->user_sponsor) {
$sponsor->is_sponsor = true;
$sponsor->user_id = $user->user_sponsor->id;
if ($user->user_sponsor->account) {
$sponsor->full_name = substr(
'Sponsor: ' . $user->user_sponsor->account->first_name . ' ' .
$user->user_sponsor->account->last_name . ' | ' .
$user->user_sponsor->email . ' | ' .
$user->user_sponsor->account->m_account, 0, 250
'Sponsor: ' . $user->user_sponsor->account->first_name . ' ' .
$user->user_sponsor->account->last_name . ' | ' .
$user->user_sponsor->email . ' | ' .
$user->user_sponsor->account->m_account,
0,
250
);
$sponsor->first_name = $user->user_sponsor->account->first_name;
$sponsor->last_name = $user->user_sponsor->account->last_name;
@ -676,9 +760,17 @@ class BusinessUserItemOptimized
/**
* Lädt Parent Business Users rekursiv (Original-Implementation mit Optimierungen)
* BUGFIX: Schutz vor unendlicher Rekursion durch zirkuläre Referenzen
*/
public function readParentsBusinessUsers($forceLiveCalculation = false): void
public function readParentsBusinessUsers($forceLiveCalculation = false, $depth = 0): void
{
// Schutz vor zu tiefer Rekursion (maximale Tiefe: 20 Levels)
$maxDepth = 20;
if ($depth > $maxDepth) {
Log::warning("BusinessUserItem: Maximale Rekursionstiefe ({$maxDepth}) erreicht für User {$this->b_user->user_id}");
return;
}
try {
// Optimiert: Lade mit Relations
$users = User::with(['account'])
@ -694,45 +786,64 @@ class BusinessUserItemOptimized
if ($users->isNotEmpty()) {
foreach ($users as $user) {
$businessUserItem = new BusinessUserItemOptimized($this->date);
// KRITISCHER BUGFIX: Prüfe ob User bereits verarbeitet wurde
if ($this->treeCalcBot && $this->treeCalcBot->isUserProcessed($user->id)) {
Log::debug("BusinessUserItem: Überspringe bereits verarbeiteten User {$user->id} (zirkuläre Referenz verhindert)");
continue;
}
$businessUserItem = new BusinessUserItemOptimized($this->date, $this->treeCalcBot);
$businessUserItem->makeUserFromModel($user, $forceLiveCalculation);
$businessUserItem->addUserID();
$this->businessUserItems[] = $businessUserItem;
}
}
// Rekursiver Aufruf für alle Child-Items
// Rekursiver Aufruf für alle Child-Items mit Tiefenprüfung
foreach ($this->businessUserItems as $businessUserItem) {
$businessUserItem->readParentsBusinessUsers($forceLiveCalculation);
$businessUserItem->readParentsBusinessUsers($forceLiveCalculation, $depth + 1);
}
} catch (\Exception $e) {
Log::error("BusinessUserItem: Error reading parent users for {$this->b_user->user_id}: " . $e->getMessage());
Log::error("BusinessUserItem: Error reading parent users for {$this->b_user->user_id} at depth {$depth}: " . $e->getMessage());
}
}
/**
* Lädt Parent Business Users aus gespeicherter Struktur (Original-Implementation)
* BUGFIX: Schutz vor unendlicher Rekursion durch zirkuläre Referenzen
*/
public function readStoredParentsBusinessUsers($structure): void
public function readStoredParentsBusinessUsers($structure, $depth = 0): void
{
// Schutz vor zu tiefer Rekursion (maximale Tiefe: 50 Levels)
$maxDepth = 50;
if ($depth > $maxDepth) {
Log::warning("BusinessUserItem: Maximale Rekursionstiefe ({$maxDepth}) erreicht für gespeicherte User {$this->b_user->user_id}");
return;
}
try {
$parents = $this->findParentsBusinessOnStored($this->b_user->user_id, $structure);
if ($parents) {
foreach ($parents as $obj) {
$businessUserItem = new BusinessUserItemOptimized($this->date);
// KRITISCHER BUGFIX: Prüfe ob User bereits verarbeitet wurde
if ($this->treeCalcBot && $this->treeCalcBot->isUserProcessed($obj->user_id)) {
Log::debug("BusinessUserItem: Überspringe bereits verarbeiteten gespeicherten User {$obj->user_id} (zirkuläre Referenz verhindert)");
continue;
}
$businessUserItem = new BusinessUserItemOptimized($this->date, $this->treeCalcBot);
$businessUserItem->makeUser($obj->user_id);
$businessUserItem->addUserID();
$this->businessUserItems[] = $businessUserItem;
}
foreach ($this->businessUserItems as $businessUserItem) {
$businessUserItem->readStoredParentsBusinessUsers($parents);
$businessUserItem->readStoredParentsBusinessUsers($parents, $depth + 1);
}
}
} catch (\Exception $e) {
Log::error("BusinessUserItem: Error reading stored parent users: " . $e->getMessage());
Log::error("BusinessUserItem: Error reading stored parent users at depth {$depth}: " . $e->getMessage());
}
}
@ -749,7 +860,7 @@ class BusinessUserItemOptimized
if ($user_id === $obj->user_id) {
return $obj->parents ?? null;
}
if (!empty($obj->parents)) {
$result = $this->findParentsBusinessOnStored($user_id, $obj->parents);
if ($result) {
@ -757,7 +868,7 @@ class BusinessUserItemOptimized
}
}
}
return null;
}
@ -794,4 +905,43 @@ class BusinessUserItemOptimized
}
return false;
}
}
/**
* Intelligentes Laden des UserAccount für einen User
* Prüft zuerst geladene Relations, lädt nach wenn nötig
*/
private function getAccountForUser(User $user): ?UserAccount
{
try {
// Prüfe ob Account-Relation bereits geladen ist
if ($user->relationLoaded('account')) {
$account = $user->account;
if ($account instanceof UserAccount) {
\Log::debug("BusinessUserItem: Using pre-loaded account for user {$user->id}");
return $account;
}
}
// Wenn User keine account_id hat, gibt es definitiv kein Account
if (!$user->account_id) {
\Log::info("BusinessUserItem: User {$user->id} has no account_id - no account available");
return null;
}
// Account nachladen falls nötig
\Log::info("BusinessUserItem: Loading account for user {$user->id} (account_id: {$user->account_id})");
$account = UserAccount::find($user->account_id);
if (!$account) {
\Log::warning("BusinessUserItem: Account {$user->account_id} not found for user {$user->id}");
return null;
}
\Log::debug("BusinessUserItem: Successfully loaded account {$account->id} for user {$user->id}");
return $account;
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error loading account for user {$user->id}: " . $e->getMessage());
return null;
}
}
}

View file

@ -24,10 +24,12 @@ class BusinessUserRepository
{
$this->month = $month;
$this->year = $year;
$date = Carbon::parse($year.'-'.$month.'-1');
$date = Carbon::parse($year . '-' . $month . '-1');
$this->startDate = $date->format('Y-m-d H:i:s');
$this->endDate = $date->endOfMonth()->format('Y-m-d H:i:s');
\Log::info("BusinessUserRepository: Start Date: " . $this->startDate);
\Log::info("BusinessUserRepository: End Date: " . $this->endDate);
}
/**
@ -36,27 +38,26 @@ class BusinessUserRepository
public function getRootUsers(): Collection
{
$cacheKey = "root_users_{$this->month}_{$this->year}";
return cache()->remember($cacheKey, 3600, function() {
\Log::info("BusinessUserRepository: Loading root users from database (cache miss)");
//root hat keinen parent m_sponsor, hat
return cache()->remember($cacheKey, 3600, function () {
return User::with([
'account',
'account',
'user_level',
'userBusiness' => function($query) {
'userBusiness' => function ($query) {
$query->where('month', $this->month)
->where('year', $this->year);
->where('year', $this->year);
}
])
->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', '<', 4)
->where('users.m_level', '!=', null)
->where('users.m_sponsor', '=', null)
->where('users.payment_account', '!=', null)
->where('users.active_date', '<=', $this->endDate)
->get();
->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', '<', 4)
->where('users.m_level', '!=', null)
->where('users.m_sponsor', '=', null)
->where('users.payment_account', '!=', null)
->where('users.active_date', '<=', $this->endDate)
->where('users.payment_account', '>', $this->endDate)
->get();
});
}
@ -66,19 +67,19 @@ class BusinessUserRepository
public function getParentlessUsers(array $excludeUserIds = []): LazyCollection
{
$query = User::with([
'account',
'account',
'user_level',
'userBusiness' => function($query) {
'userBusiness' => function ($query) {
$query->where('month', $this->month)
->where('year', $this->year);
->where('year', $this->year);
}
])
->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', '<', 4)
->where('users.payment_account', '!=', null)
->where('users.active_date', '<=', $this->endDate);
->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', '<', 4)
->where('users.payment_account', '!=', null)
->where('users.active_date', '<=', $this->endDate);
if (!empty($excludeUserIds)) {
$query->whereNotIn('users.id', $excludeUserIds);
@ -93,16 +94,16 @@ class BusinessUserRepository
public function getUserWithRelations(int $userId): ?User
{
$cacheKey = "user_relations_{$userId}_{$this->month}_{$this->year}";
return cache()->remember($cacheKey, 1800, function() use ($userId) {
return cache()->remember($cacheKey, 1800, function () use ($userId) {
\Log::debug("BusinessUserRepository: Loading user {$userId} with relations (cache miss)");
return User::with([
'account',
'account',
'user_level',
'userBusiness' => function($query) {
'userBusiness' => function ($query) {
$query->where('month', $this->month)
->where('year', $this->year);
->where('year', $this->year);
}
])->find($userId);
});
@ -114,7 +115,7 @@ class BusinessUserRepository
public function getSponsorForUser(int $userId): ?User
{
$user = $this->getUserWithRelations($userId);
if (!$user || !$user->m_sponsor) {
return null;
}
@ -128,10 +129,10 @@ class BusinessUserRepository
public function getStoredStructure(): ?UserBusinessStructure
{
$cacheKey = "stored_structure_{$this->month}_{$this->year}";
return cache()->remember($cacheKey, 7200, function() {
return cache()->remember($cacheKey, 7200, function () {
\Log::debug("BusinessUserRepository: Loading stored structure (cache miss)");
$structure = UserBusinessStructure::where('year', $this->year)
->where('month', $this->month)
->first();
@ -167,32 +168,48 @@ class BusinessUserRepository
{
foreach ($structure as $item) {
$userIds[] = $item->user_id;
if (isset($item->parents) && is_array($item->parents)) {
$this->extractUserIdsFromStructure($item->parents, $userIds);
}
}
}
/**
* Löscht alle Cache-Einträge für den aktuellen Monat/Jahr
*/
public function clearCache(): void
{
$cacheKeys = [
"root_users_{$this->month}_{$this->year}",
"stored_structure_{$this->month}_{$this->year}"
];
foreach ($cacheKeys as $key) {
cache()->forget($key);
\Log::info("BusinessUserRepository: Cache cleared for key: {$key}");
}
}
/**
* Batch-Loading für User-Kollektionen
*/
public function loadUsersInBatches(array $userIds, int $batchSize = 100): \Generator
{
$chunks = array_chunk($userIds, $batchSize);
foreach ($chunks as $chunk) {
yield User::with([
'account',
'account',
'user_level',
'userBusiness' => function($query) {
'userBusiness' => function ($query) {
$query->where('month', $this->month)
->where('year', $this->year);
->where('year', $this->year);
}
])
->whereIn('id', $chunk)
->get()
->keyBy('id');
->whereIn('id', $chunk)
->get()
->keyBy('id');
}
}
}
}

View file

@ -35,8 +35,8 @@ class TreeCalcBotOptimized
private LoggerInterface $logger;
public function __construct(
int $month,
int $year,
int $month,
int $year,
string $initFrom = 'member',
bool $forceLiveCalculation = false,
?BusinessUserRepository $repository = null,
@ -47,7 +47,7 @@ class TreeCalcBotOptimized
$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);
@ -65,13 +65,13 @@ class TreeCalcBotOptimized
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();
@ -83,7 +83,6 @@ class TreeCalcBotOptimized
$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;
@ -100,7 +99,7 @@ class TreeCalcBotOptimized
{
try {
$this->forceLiveCalculation = $forceLiveCalculation;
if ($forceLiveCalculation) {
$this->logger->info("Initializing structure for user: {$userId} with forced live calculation");
} else {
@ -114,11 +113,11 @@ class TreeCalcBotOptimized
return;
}
$businessUserItem = new BusinessUserItemOptimized($this->date);
$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
@ -127,7 +126,7 @@ class TreeCalcBotOptimized
$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) {
@ -139,13 +138,13 @@ class TreeCalcBotOptimized
}
$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");
@ -156,7 +155,6 @@ class TreeCalcBotOptimized
//$this->calculateQualPPForAllUsers(); // Auch für alle Sub-User
}
}
} catch (\Exception $e) {
$this->logger->error("Error initializing user structure for {$userId}: " . $e->getMessage());
throw $e;
@ -173,11 +171,11 @@ class TreeCalcBotOptimized
{
try {
$this->logger->info("Initializing business user details for: {$user->id}");
$this->businessUser = new BusinessUserItemOptimized($this->date);
$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
@ -185,19 +183,17 @@ class TreeCalcBotOptimized
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;
@ -223,7 +219,7 @@ class TreeCalcBotOptimized
}
return array_slice($bLines, 6);
}
return [];
}
@ -242,11 +238,11 @@ class TreeCalcBotOptimized
}
$lineData = $bLines[$line];
if ($lineData instanceof stdClass) {
return $lineData->{$key} ?? 0;
}
if (is_array($lineData)) {
return $lineData[$key] ?? 0;
}
@ -286,37 +282,37 @@ class TreeCalcBotOptimized
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;
}
@ -337,10 +333,19 @@ class TreeCalcBotOptimized
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: Wird durch Instanz-Methode ersetzt
// Bleibt für Rückwärtskompatibilität erhalten
// 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 =====
@ -381,7 +386,7 @@ class TreeCalcBotOptimized
$this->loadStoredRootUsers($structure);
$this->loadStoredParentsUsers($structure);
$this->loadStoredParentlessUsers($structure);
// Prüfe ob gespeicherte Daten vollständig sind, ansonsten berechne neu
$this->validateAndRecalculateIfNeeded();
$this->validateAndRecalculateParentlessIfNeeded();
@ -395,7 +400,7 @@ class TreeCalcBotOptimized
$this->loadRootUsers();
$this->loadParentsUsers();
$this->loadParentlessUsers();
// WICHTIG: Berechne Punkte und Qualifikationen für alle Business-Users
$this->calculateAllBusinessUsers();
$this->calculateAllParentlessUsers();
@ -408,12 +413,11 @@ class TreeCalcBotOptimized
{
$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 = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUserFromModel($user, $this->forceLiveCalculation); // ✅ Nutzt bereits geladene Relations mit forceLiveCalculation
$this->addUserIdToProcessed($user->id);
$this->businessUsers[] = $businessUserItem;
@ -421,7 +425,7 @@ class TreeCalcBotOptimized
$endMemory = memory_get_usage();
$memoryUsed = $this->formatBytes($endMemory - $startMemory);
$this->logger->info("Loaded " . count($users) . " root users with optimized relations. Memory used: {$memoryUsed}");
}
@ -431,10 +435,10 @@ class TreeCalcBotOptimized
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'));
}
}
@ -446,9 +450,9 @@ class TreeCalcBotOptimized
{
$count = 0;
$excludeIds = array_keys($this->processedUserIds);
foreach ($this->repository->getParentlessUsers($excludeIds) as $user) {
$businessUserItem = new BusinessUserItemOptimized($this->date);
$businessUserItem = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUserFromModel($user, $this->forceLiveCalculation); // ✅ Nutzt bereits geladene Relations mit forceLiveCalculation
$this->parentless[] = $businessUserItem;
$count++;
@ -464,24 +468,23 @@ class TreeCalcBotOptimized
{
$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");
@ -495,26 +498,25 @@ class TreeCalcBotOptimized
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");
@ -526,25 +528,24 @@ class TreeCalcBotOptimized
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");
}
@ -559,20 +560,20 @@ class TreeCalcBotOptimized
// 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);
$missingBasicData = ($salesVolumeSum === null || $salesVolumeSum === 0) &&
($qualKp === null || $qualKp === 0);
$missingLevelData = !$hasLevelQualificationData;
return $missingBasicData || $missingLevelData;
}
@ -584,25 +585,24 @@ class TreeCalcBotOptimized
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");
}
@ -615,9 +615,9 @@ class TreeCalcBotOptimized
{
try {
$sponsorUser = $this->repository->getSponsorForUser($userId);
if ($sponsorUser) {
$this->sponsor = new BusinessUserItemOptimized($this->date);
$this->sponsor = new BusinessUserItemOptimized($this->date, $this);
$this->sponsor->makeUser($sponsorUser->id);
$this->logger->info("Loaded sponsor {$sponsorUser->id} for user {$userId}");
}
@ -636,7 +636,7 @@ class TreeCalcBotOptimized
}
foreach ($structure->structure as $obj) {
$businessUserItem = new BusinessUserItemOptimized($this->date);
$businessUserItem = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUser($obj->user_id);
$this->addUserIdToProcessed($obj->user_id);
$this->businessUsers[] = $businessUserItem;
@ -664,7 +664,7 @@ class TreeCalcBotOptimized
foreach ($structure->parentless as $obj) {
if (!isset($this->processedUserIds[$obj->user_id])) {
$businessUserItem = new BusinessUserItemOptimized($this->date);
$businessUserItem = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUser($obj->user_id);
$this->parentless[] = $businessUserItem;
}
@ -676,7 +676,7 @@ class TreeCalcBotOptimized
*/
private function loadStoredSponsorUser(int $userId): void
{
$this->sponsor = new BusinessUserItemOptimized($this->date);
$this->sponsor = new BusinessUserItemOptimized($this->date, $this);
$this->sponsor->makeUser($userId);
}
@ -691,7 +691,7 @@ class TreeCalcBotOptimized
{
$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];
@ -704,11 +704,11 @@ class TreeCalcBotOptimized
$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,
'item' => $item,
'line' => $line,
'depth' => $depth,
'id' => $item->user_id ?? uniqid()
];
@ -719,8 +719,8 @@ class TreeCalcBotOptimized
$children = array_reverse($item->businessUserItems);
foreach ($children as $childItem) {
array_unshift($collectionStack, [
'item' => $childItem,
'line' => $line + 1,
'item' => $childItem,
'line' => $line + 1,
'depth' => $depth + 1
]);
}
@ -728,7 +728,7 @@ class TreeCalcBotOptimized
}
// Phase 2: Sortiere nach Tiefe (tiefste zuerst, wie bei Rekursion)
usort($processingStack, function($a, $b) {
usort($processingStack, function ($a, $b) {
return $b['depth'] <=> $a['depth']; // Tiefste zuerst
});
@ -753,7 +753,6 @@ class TreeCalcBotOptimized
}
$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());
}
@ -771,9 +770,9 @@ class TreeCalcBotOptimized
}
/**
* Prüft ob User bereits verarbeitet wurde
* Prüft ob User bereits verarbeitet wurde (Public für BusinessUserItemOptimized)
*/
private function isUserProcessed(int $id): bool
public function isUserProcessed(int $id): bool
{
return isset($this->processedUserIds[$id]);
}
@ -790,7 +789,7 @@ class TreeCalcBotOptimized
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,
@ -809,13 +808,16 @@ class TreeCalcBotOptimized
private function parseMemoryLimit(string $limit): int
{
$limit = trim($limit);
$last = strtolower($limit[strlen($limit)-1]);
$last = strtolower($limit[strlen($limit) - 1]);
$number = (int) $limit;
switch($last) {
case 'g': $number *= 1024;
case 'm': $number *= 1024;
case 'k': $number *= 1024;
switch ($last) {
case 'g':
$number *= 1024;
case 'm':
$number *= 1024;
case 'k':
$number *= 1024;
}
return $number;
@ -859,21 +861,21 @@ class TreeCalcBotOptimized
{
$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) {
@ -884,13 +886,13 @@ class TreeCalcBotOptimized
} 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;
}
@ -907,4 +909,4 @@ class TreeCalcBotOptimized
throw new \InvalidArgumentException("Property {$name} cannot be set");
}
}
}
}

View file

@ -20,17 +20,17 @@ class TreeHelperOptimized
if (!$userBusiness->m_level_id) {
return '-';
}
$qualKP = (int) $userBusiness->qual_kp;
$pointsSum = (int) $userBusiness->sales_volume_points_KP_sum;
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-danger';
return '<span class="badge ' . $badgeClass . '"> KU ' . $qualKP . '</span>';
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-info';
return '<span class="badge ' . $badgeClass . '"> KU ' . $qualKP . "/" . $pointsSum . '</span>';
}
/**
/**
* Generiert QualKP Badge für User
*/
public static function generateQualKPBadgeForUser(User $user, int $month, int $year): string
@ -38,17 +38,17 @@ class TreeHelperOptimized
if (!$user->user_level) {
return '-';
}
$qualKP = (int) $user->user_level->qual_kp;
$pointsSum = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-warning-dark';
return '<span class="badge ' . $badgeClass . '"> KU ' . $qualKP . '</span>';
}
/**
/**
* Generiert Sales Volume Display für UserBusiness
*/
public static function generateSalesVolumeDisplay(UserBusiness $userBusiness, string $type): string
@ -63,17 +63,17 @@ class TreeHelperOptimized
$shop = (float) $userBusiness->sales_volume_total_shop;
$suffix = ' &euro;';
}
$totalFormatted = $type === 'points' ? $total : formatNumber($total);
$individualFormatted = $type === 'points' ? $individual : formatNumber($individual);
$shopFormatted = $type === 'points' ? $shop : formatNumber($shop);
$suffix = $type === 'points' ? '' : ' &euro;';
return '<div class="no-line-break">' . $totalFormatted . $suffix . '</div>' .
'<span class="small no-line-break">E: ' . $individualFormatted . ' | S: ' . $shopFormatted . $suffix . '</span>';
'<span class="small no-line-break">E: ' . $individualFormatted . ' | S: ' . $shopFormatted . $suffix . '</span>';
}
/**
/**
* Generiert Sales Volume Display für User
*/
public static function generateSalesVolumeDisplayForUser(User $user, string $type, int $month, int $year): string
@ -87,42 +87,43 @@ class TreeHelperOptimized
$individual = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total');
$shop = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total_shop');
}
$totalFormatted = $type === 'points' ? $total : formatNumber($total);
$individualFormatted = $type === 'points' ? $individual : formatNumber($individual);
$shopFormatted = $type === 'points' ? $shop : formatNumber($shop);
$suffix = $type === 'points' ? '' : ' &euro;';
return '<div class="no-line-break">' . $totalFormatted . $suffix . '</div>' .
'<span class="small no-line-break">E: ' . $individualFormatted . ' | S: ' . $shopFormatted . $suffix . '</span>';
'<span class="small no-line-break">E: ' . $individualFormatted . ' | S: ' . $shopFormatted . $suffix . '</span>';
}
/**
/**
* Generiert Action Buttons (mit XSS-Schutz)
*/
public static function generateActionButtons($userId): string
{
$userId = (int) $userId; // Sicherheit: Nur Integer
$html = '<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . $userId . '"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-optimized="true"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button>';
if (config('app.debug') === true) {
$html .= '<a href="' . route('admin_business_optimized_user_detail', [$userId]) . '" class="btn icon-btn btn-xs btn-primary"><span class="fa fa-calculator"></span></a>';
}
return $html;
}
/**
/**
* Generiert Sponsor Display für UserBusiness
*/
public static function generateSponsorDisplay(UserBusiness $userBusiness): string
@ -130,22 +131,23 @@ class TreeHelperOptimized
if (!$userBusiness->sponsor || !$userBusiness->sponsor->is_sponsor) {
return '-';
}
$sponsor = $userBusiness->sponsor;
$html = e($sponsor->first_name . ' ' . $sponsor->last_name);
$html .= ' &nbsp;<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . (int) $sponsor->user_id . '"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-optimized="true"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button><br>';
$html .= '<span class="small no-line-break">' . e($sponsor->email);
$html .= ' | ' . e($sponsor->m_account);
$html .= '</span>';
return $html;
}
@ -157,10 +159,10 @@ class TreeHelperOptimized
if (!$user->user_sponsor) {
return '-';
}
$sponsor = $user->user_sponsor;
$html = '';
if ($sponsor->account) {
$html .= e($sponsor->account->first_name . ' ' . $sponsor->account->last_name);
$html .= ' &nbsp;<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
@ -171,13 +173,13 @@ class TreeHelperOptimized
data-init_from="admin"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button><br>';
}
$html .= '<span class="small no-line-break">' . e($sponsor->email);
if ($sponsor->account) {
$html .= ' | ' . e($sponsor->account->m_account);
}
$html .= '</span>';
return $html;
}
}
}

View file

@ -39,6 +39,7 @@ class DhlDataHelper
$settingController = new SettingController();
$dhlConfig = $settingController->getDhlConfig();
}
$dimensions = isset($dhlConfig['dimensions'][$options['product_code']]) ? $dhlConfig['dimensions'][$options['product_code']] : $dhlConfig['dimensions']['default'];
return [
'order_id' => $order->id,
'weight_kg' => $weight,
@ -60,9 +61,9 @@ class DhlDataHelper
'phone' => $dhlConfig['sender']['phone'] ?? '+49 123 456789',
],
// Consignee data (recipient) - from order
// Consignee data (recipient) - from modal form (can be modified)
'consignee' => [
'name' => $shippingAddress['firstname'] ?? '' . ' ' . $shippingAddress['lastname'] ?? '',
'name' => trim(($shippingAddress['firstname'] ?? '') . ' ' . ($shippingAddress['lastname'] ?? '')),
'name2' => $shippingAddress['company'] ?? '',
'street' => $shippingAddress['address'] ?? '',
'houseNumber' => $shippingAddress['houseNumber'] ?? '',
@ -71,13 +72,12 @@ class DhlDataHelper
'country' => $shippingAddress['country']?->code ?? 'DE',
'email' => $shippingAddress['email'] ?? '',
'phone' => $shippingAddress['phone'] ?? '',
// Store individual fields for easier access
'firstname' => $shippingAddress['firstname'] ?? '',
'lastname' => $shippingAddress['lastname'] ?? '',
],
// Package dimensions from options or defaults
'dimensions' => [
'length' => $options['length'] ?? 30,
'width' => $options['width'] ?? 25,
'height' => $options['height'] ?? 10,
],
'dimensions' => $dimensions,
// Additional services
'services' => $options['services'] ?? [],

View file

@ -45,7 +45,9 @@ class DhlModalService
'availableCountries' => $this->getAvailableCountries(),
'productCodes' => $this->getAvailableProductCodes(),
'errors' => [],
'warnings' => []
'warnings' => [],
'existingShipments' => [],
'modalMode' => 'search' // 'search', 'create', 'info'
];
// If no order ID or 'new', return empty data for order selection
@ -63,26 +65,44 @@ class DhlModalService
$result['order'] = $order;
// Calculate order weight
$result['orderWeight'] = $this->calculateOrderWeight($order);
// Check for existing DHL shipments
$existingShipments = $this->getExistingShipments($order);
$result['existingShipments'] = $existingShipments;
// Process and validate shipping address
$result['shippingAddress'] = $this->processShippingAddress($order);
// Check if force_create is requested
$forceCreate = isset($data['force_create']) && $data['force_create'];
// Validate address completeness
$addressValidation = $this->validateAddress($result['shippingAddress']);
if (!$addressValidation['valid']) {
$result['errors'] = array_merge($result['errors'], $addressValidation['errors']);
// Determine modal mode based on existing shipments and force_create
if (!empty($existingShipments) && !$forceCreate) {
$result['modalMode'] = 'info';
Log::info('[DHL Modal] Order has existing shipments, showing info mode', [
'order_id' => $order->id,
'shipment_count' => count($existingShipments)
]);
} else {
$result['modalMode'] = 'create';
// Calculate order weight
$result['orderWeight'] = $this->calculateOrderWeight($order);
// Process and validate shipping address
$result['shippingAddress'] = $this->processShippingAddress($order);
// Validate address completeness
$addressValidation = $this->validateAddress($result['shippingAddress']);
if (!$addressValidation['valid']) {
$result['errors'] = array_merge($result['errors'], $addressValidation['errors']);
}
if (!empty($addressValidation['warnings'])) {
$result['warnings'] = array_merge($result['warnings'], $addressValidation['warnings']);
}
Log::info('[DHL Modal] Prepared modal data for creation', [
'order_id' => $order->id,
'weight' => $result['orderWeight'],
'address_valid' => empty($result['errors'])
]);
}
if (!empty($addressValidation['warnings'])) {
$result['warnings'] = array_merge($result['warnings'], $addressValidation['warnings']);
}
Log::info('[DHL Modal] Prepared modal data successfully', [
'order_id' => $order->id,
'weight' => $result['orderWeight'],
'address_valid' => empty($result['errors'])
]);
} catch (Exception $e) {
Log::error('[DHL Modal] Error preparing modal data', [
'order_id' => $id,
@ -106,9 +126,45 @@ class DhlModalService
return ShoppingOrder::with([
'shopping_order_items',
'shopping_user',
'dhlShipments' // Include DHL shipments
])->find($id);
}
/**
* Get existing DHL shipments for the order
*
* @param ShoppingOrder $order
* @return array
*/
private function getExistingShipments(ShoppingOrder $order): array
{
$shipments = $order->dhlShipments()
->orderBy('created_at', 'desc')
->get();
return $shipments->map(function ($shipment) {
return [
'id' => $shipment->id,
'shipment_number' => $shipment->dhl_shipment_no,
'tracking_number' => $shipment->routing_code,
'type' => $shipment->type,
'status' => $shipment->status,
'status_translated' => $shipment->getStatusTranslation(),
'type_translated' => $shipment->getTypeTranslation(),
'product_code_translated' => $shipment->getProductCodeTranslation(),
'weight' => $shipment->weight_kg,
'product_code' => $shipment->product_code,
'label_path' => $shipment->label_path,
'created_at' => $shipment->created_at->toDateTimeString(),
'tracking_status' => $shipment->tracking_status,
'tracking_status_translated' => $shipment->tracking_status ? \Acme\Dhl\Models\DhlShipment::getStatusTranslationFor($shipment->tracking_status) : null,
'last_tracked_at' => $shipment->last_tracked_at,
'can_cancel' => $shipment->canCancel(),
'is_delivered' => $shipment->isDelivered()
];
})->toArray();
}
/**
* Calculate order weight in kg
*
@ -117,7 +173,7 @@ class DhlModalService
*/
private function calculateOrderWeight(ShoppingOrder $order): float
{
return $order->weight / 100;
return $order->weight / 1000; //from grams to kg
/*
// Default fallback weight
$defaultWeight = 1.0;
@ -433,7 +489,9 @@ class DhlModalService
'zipcode' => trim($formData['shipping_zipcode'] ?? ''),
'city' => trim($formData['shipping_city'] ?? ''),
'country_id' => $country?->id,
'phone' => trim($formData['shipping_phone'] ?? '')
'country' => $country, // Store country object for DhlDataHelper
'phone' => trim($formData['shipping_phone'] ?? ''),
'email' => trim($formData['shipping_email'] ?? '') // Add email if available
];
}
}

View file

@ -98,7 +98,7 @@ class DhlShipmentService
'weight' => $weight
]);
// Create DHL client directly
// Create DHL client directly with correct base URL
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],

View file

@ -0,0 +1,432 @@
<?php
namespace App\Services;
use Acme\Dhl\Models\DhlShipment;
use App\Http\Controllers\SettingController;
use App\Jobs\TrackShipmentJob;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* DHL Tracking Service
*
* Handles DHL tracking using both Unified Tracking API and Parcel DE Tracking API
* with support for synchronous and asynchronous tracking updates
*/
class DhlTrackingService
{
private string $apiKey;
private string $apiSecret;
private bool $isSandbox;
public function __construct()
{
$settingController = new SettingController;
$dhlConfig = $settingController->getDhlConfig();
$this->apiKey = $dhlConfig['api_key'] ?? config('dhl.api_key');
$this->apiSecret = $dhlConfig['api_secret'] ?? config('dhl.legacy.api_secret');
$this->isSandbox = ($dhlConfig['sandbox'] ?? config('dhl.legacy.sandbox', true));
}
/**
* Track shipment using DHL Unified Tracking API (recommended for international)
*/
public function trackShipment(string $trackingNumber, array $options = []): array
{
try {
Log::info('[DHL Tracking Service] Tracking shipment with Unified API', [
'tracking_number' => $trackingNumber,
'is_sandbox' => $this->isSandbox,
]);
$response = Http::withHeaders([
'DHL-API-Key' => $this->apiKey,
'Accept' => 'application/json',
])
->withOptions([
'verify' => config('dhl.ssl.verify_peer', true),
'http_errors' => false,
'curl' => [
CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true),
CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0,
CURLOPT_SSLVERSION => $this->getSslVersion(),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10),
CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30),
CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0',
]
])
->get('https://api.dhl.com/track/shipments', [
'trackingNumber' => $trackingNumber,
'service' => 'express,parcel',
'requesterCountryCode' => 'DE',
'originCountryCode' => 'DE',
'language' => 'de',
]);
if ($response->successful()) {
$data = $response->json();
if (isset($data['shipments']) && count($data['shipments']) > 0) {
$shipment = $data['shipments'][0];
return [
'success' => true,
'tracking_number' => $shipment['id'],
'status' => $shipment['status']['statusCode'] ?? 'unknown',
'status_text' => $shipment['status']['status'] ?? 'Unbekannt',
'description' => $shipment['status']['description'] ?? '',
'last_update' => $shipment['status']['timestamp'] ?? null,
'origin' => $shipment['origin']['address']['addressLocality'] ?? null,
'destination' => $shipment['destination']['address']['addressLocality'] ?? null,
'events' => $shipment['events'] ?? [],
'api_used' => 'unified',
];
}
}
// If Unified API fails, try Parcel DE API
return $this->trackShipmentDE($trackingNumber, $options);
} catch (Exception $e) {
Log::error('[DHL Tracking Service] Unified API failed', [
'tracking_number' => $trackingNumber,
'error' => $e->getMessage(),
]);
// Fallback to Parcel DE API
return $this->trackShipmentDE($trackingNumber, $options);
}
}
/**
* Track shipment using DHL Parcel DE Tracking API (optimized for Germany)
*/
public function trackShipmentDE(string $trackingNumber, array $options = []): array
{
try {
Log::info('[DHL Tracking Service] Tracking shipment with Parcel DE API', [
'tracking_number' => $trackingNumber,
'is_sandbox' => $this->isSandbox,
]);
$response = Http::withBasicAuth($this->apiKey, $this->apiSecret)
->withHeaders([
'Accept' => 'application/json',
'dhl-api-key' => $this->apiKey,
])
->withOptions([
'verify' => config('dhl.ssl.verify_peer', true),
'http_errors' => false,
'curl' => [
CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true),
CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0,
CURLOPT_SSLVERSION => $this->getSslVersion(),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10),
CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30),
CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0',
]
])
->get('https://api.dhl.com/parcel/de/tracking/v1/shipments', [
'trackingNumber' => $trackingNumber,
'language' => 'de',
]);
if ($response->successful()) {
$data = $response->json();
if (isset($data['shipments']) && count($data['shipments']) > 0) {
$shipment = $data['shipments'][0];
return [
'success' => true,
'tracking_number' => $shipment['id'],
'status' => $shipment['status']['statusCode'] ?? 'unknown',
'status_text' => $shipment['status']['description'] ?? 'Unbekannt',
'description' => $shipment['status']['description'] ?? '',
'last_update' => $shipment['status']['timestamp'] ?? null,
'events' => $shipment['events'] ?? [],
'api_used' => 'parcel_de',
];
}
}
return [
'success' => false,
'message' => 'Sendung nicht gefunden oder noch nicht im System erfasst.',
'tracking_number' => $trackingNumber,
'api_used' => 'parcel_de',
];
} catch (Exception $e) {
Log::error('[DHL Tracking Service] Parcel DE API failed', [
'tracking_number' => $trackingNumber,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'Fehler beim Abrufen der Tracking-Informationen: ' . $e->getMessage(),
'tracking_number' => $trackingNumber,
'api_used' => 'parcel_de',
];
}
}
/**
* Track multiple shipments at once (up to 10 for Unified API)
*/
public function trackMultipleShipments(array $trackingNumbers): array
{
if (count($trackingNumbers) > 10) {
return [
'success' => false,
'message' => 'Maximal 10 Sendungen können gleichzeitig getrackt werden.',
];
}
try {
$response = Http::withHeaders([
'DHL-API-Key' => $this->apiKey,
'Accept' => 'application/json',
])
->withOptions([
'verify' => config('dhl.ssl.verify_peer', true),
'http_errors' => false,
'curl' => [
CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true),
CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0,
CURLOPT_SSLVERSION => $this->getSslVersion(),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10),
CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30),
CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0',
]
])
->get('https://api.dhl.com/track/shipments', [
'trackingNumber' => implode(',', $trackingNumbers),
'service' => 'parcel',
'requesterCountryCode' => 'DE',
'language' => 'de',
]);
if ($response->successful()) {
$data = $response->json();
$results = [];
foreach ($data['shipments'] ?? [] as $shipment) {
$results[] = [
'tracking_number' => $shipment['id'],
'status' => $shipment['status']['statusCode'] ?? 'unknown',
'status_text' => $shipment['status']['status'] ?? 'Unbekannt',
'last_update' => $shipment['status']['timestamp'] ?? null,
'events' => $shipment['events'] ?? [],
];
}
return [
'success' => true,
'shipments' => $results,
'api_used' => 'unified',
];
}
return [
'success' => false,
'message' => 'Fehler beim Abrufen der Tracking-Informationen.',
];
} catch (Exception $e) {
Log::error('[DHL Tracking Service] Multiple tracking failed', [
'tracking_numbers' => $trackingNumbers,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'Fehler beim Abrufen der Tracking-Informationen: ' . $e->getMessage(),
];
}
}
/**
* Update tracking for a DHL shipment (sync or async based on config)
*/
public function updateTracking(DhlShipment $shipment, array $options = []): array
{
// Get DHL configuration
$settingController = new SettingController;
$dhlConfig = $settingController->getDhlConfig();
// Check if queue should be used
$useQueue = $dhlConfig['use_queue'] ?? false;
if ($useQueue) {
return $this->updateTrackingAsync($shipment, $options, $dhlConfig);
} else {
return $this->updateTrackingSync($shipment, $options, $dhlConfig);
}
}
/**
* Update tracking asynchronously using queue
*/
private function updateTrackingAsync(DhlShipment $shipment, array $options, array $dhlConfig): array
{
try {
// Dispatch job with pre-loaded config
TrackShipmentJob::dispatch($shipment, $options);
Log::info('[DHL Tracking Service] Tracking update dispatched to queue', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
]);
return [
'success' => true,
'message' => 'Tracking-Update wird verarbeitet. Sie erhalten eine Benachrichtigung, sobald die Informationen aktualisiert sind.',
'queued' => true,
'shipment_id' => $shipment->id,
];
} catch (Exception $e) {
Log::error('[DHL Tracking Service] Failed to dispatch tracking update', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return [
'success' => false,
'message' => 'Fehler beim Einreihen des Tracking-Updates: ' . $e->getMessage(),
'queued' => false,
];
}
}
/**
* Update tracking synchronously using new DHL APIs
*/
private function updateTrackingSync(DhlShipment $shipment, array $options, array $dhlConfig): array
{
try {
Log::info('[DHL Tracking Service] Updating tracking synchronously', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
]);
// Check if shipment has tracking number
if (! $shipment->dhl_shipment_no) {
return [
'success' => false,
'message' => 'Keine DHL-Sendungsnummer verfügbar für Tracking.',
'queued' => false,
'shipment_id' => $shipment->id,
];
}
// Use new tracking API
$result = $this->trackShipment($shipment->dhl_shipment_no);
if ($result['success']) {
// Update shipment with tracking data
$shipment->update([
'status' => $this->mapDhlStatusToInternal($result['status']),
'tracking_status' => $result['status_text'],
'last_tracked_at' => now(),
]);
Log::info('[DHL Tracking Service] Tracking updated successfully (sync)', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'tracking_status' => $result['status'],
'api_used' => $result['api_used'],
]);
return [
'success' => true,
'message' => 'Tracking-Informationen erfolgreich aktualisiert!',
'queued' => false,
'shipment_id' => $shipment->id,
'tracking_status' => $result['status'],
'tracking_details' => $result,
];
} else {
return [
'success' => false,
'message' => $result['message'] ?? 'Fehler beim Abrufen der Tracking-Informationen.',
'queued' => false,
'shipment_id' => $shipment->id,
];
}
} catch (Exception $e) {
Log::error('[DHL Tracking Service] Tracking update failed (sync)', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'Fehler beim Aktualisieren der Tracking-Informationen: ' . $e->getMessage(),
'queued' => false,
'shipment_id' => $shipment->id,
];
}
}
/**
* Map DHL status codes to internal status
*/
private function mapDhlStatusToInternal(string $dhlStatus): string
{
$statusMap = [
'pre-transit' => 'created',
'transit' => 'in_transit',
'out-for-delivery' => 'out_for_delivery',
'delivered' => 'delivered',
'failure' => 'failed',
'returned' => 'returned',
'exception' => 'exception',
];
return $statusMap[$dhlStatus] ?? 'unknown';
}
/**
* Get status description in German
*/
public function getStatusDescription(string $statusCode): string
{
$descriptions = [
'pre-transit' => 'Auftrag elektronisch übermittelt',
'transit' => 'Sendung in Zustellung',
'out-for-delivery' => 'Wird heute zugestellt',
'delivered' => 'Erfolgreich zugestellt',
'failure' => 'Zustellung fehlgeschlagen',
'returned' => 'Sendung wird zurückgeschickt',
'exception' => 'Zustellausnahme',
];
return $descriptions[$statusCode] ?? 'Unbekannter Status';
}
/**
* Get SSL version constant based on configuration
*/
private function getSslVersion(): int
{
$sslVersion = config('dhl.ssl.ssl_version', 'TLSv1_2');
return match ($sslVersion) {
'TLSv1_0' => CURL_SSLVERSION_TLSv1_0,
'TLSv1_1' => CURL_SSLVERSION_TLSv1_1,
'TLSv1_2' => CURL_SSLVERSION_TLSv1_2,
'TLSv1_3' => defined('CURL_SSLVERSION_TLSv1_3') ? CURL_SSLVERSION_TLSv1_3 : CURL_SSLVERSION_TLSv1_2,
default => CURL_SSLVERSION_TLSv1_2,
};
}
}

View file

@ -1,298 +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 CACHE_TAG_USER_SHOPS = 'user_shops';
private const CACHE_TAG_DOMAIN_PARSING = 'domain_parsing';
private const FIXED_SUBDOMAINS = ['my', 'in', 'checkout'];
private array $domainConfig;
public function __construct(?array $domainConfig = null)
{
$this->domainConfig = $domainConfig ?? config('domains');
}
/**
* Determine the type of subdomain
*/
public function getSubdomainType(string $subdomain): string
{
// Frühe Validierung: Prüfe reservierte Subdomains aus Konfiguration
$reservedSubdomains = $this->domainConfig['reserved_subdomains'] ?? self::FIXED_SUBDOMAINS;
if (in_array($subdomain, $reservedSubdomains)) {
return match($subdomain) {
'my' => 'crm',
'in' => 'portal',
'checkout' => 'checkout',
default => 'unknown' // Andere reservierte Subdomains sind ungültig
};
}
// Frühe Validierung: Prüfe auf ungültige Zeichen für UserShop-Slugs
if (!preg_match('/^[a-z0-9-]+$/', $subdomain) || strlen($subdomain) < 3) {
return '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) {
// Optimierte Query mit allen Validierungen in einem DB-Call
$userShop = UserShop::where('slug', $slug)
->where('active', true)
->whereHas('user', function ($query) {
$query->whereNotNull('payment_shop')
->where('payment_shop', '>', now());
})
->exists();
return $userShop;
});
}
/**
* Get user shop by slug with caching
*/
public function getUserShop(string $slug): ?UserShop
{
$cacheKey = "user_shop_{$slug}";
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($slug) {
// Optimierte Query mit allen Validierungen in einem DB-Call
return UserShop::where('slug', $slug)
->where('active', true)
->whereHas('user', function ($query) {
$query->whereNotNull('payment_shop')
->where('payment_shop', '>', now());
})
->with('user')
->first();
});
}
/**
* Parse domain from request and determine context
*/
public function parseDomain(string $host): array
{
// Normalisiere den Host (lowercase)
$host = strtolower(trim($host));
$parts = explode('.', $host);
// Handle different TLD scenarios
if (count($parts) < 2) {
\Log::warning('Invalid host format', ['host' => $host]);
return [
'type' => 'invalid',
'domain' => $host,
'subdomain' => null,
'tld' => null,
'host' => $host
];
}
// Extract TLD and domain
$tld = '.' . end($parts);
$domain = $parts[count($parts) - 2];
// Check for subdomain
$subdomain = null;
if (count($parts) > 2) {
$subdomain = $parts[0];
if (config('app.debug')) {
\Log::debug('DomainService: Using extracted subdomain', ['subdomain' => $subdomain, 'host' => $host]);
}
}
// Determine domain type based on subdomain and host
$type = $this->determineDomainType($host, $subdomain);
return [
'type' => $type,
'domain' => $domain,
'subdomain' => $subdomain,
'tld' => $tld,
'host' => $host,
'default_user_shop' => $this->domainConfig['domains']['shop']['default_user_shop'] ?? null
];
}
/**
* Determine domain type based on full host and subdomain
*/
private function determineDomainType(string $host, ?string $subdomain): string
{
// Check against configured domains
foreach ($this->domainConfig['domains'] as $type => $config) {
if (isset($config['host'])) {
// Handle wildcard user-shop pattern
if ($type === 'user-shop') {
$pattern = str_replace('{subdomain}', '([a-z0-9-]+)', $config['host']);
if (preg_match("/^{$pattern}$/", $host)) {
return 'user-shop';
}
} else {
// Exact match for other domains
if ($host === $config['host']) {
return $type;
}
}
}
}
// Additional check for subdomain-based detection
if ($subdomain) {
$subdomainType = $this->getSubdomainType($subdomain);
if ($subdomainType !== 'unknown') {
return $subdomainType;
}
}
return 'unknown';
}
/**
* Build URL for specific domain type
*/
public function buildUrl(string $type, ?string $path = null, ?string $slug = null): string
{
$protocol = $this->domainConfig['protocol'] ?? 'https://';
$domainConfig = $this->domainConfig['domains'][$type] ?? null;
if (!$domainConfig) {
throw new \InvalidArgumentException("Unknown domain type: {$type}");
}
$host = $domainConfig['host'];
// Handle user-shop wildcard
if ($type === 'user-shop') {
if (!$slug) {
throw new \InvalidArgumentException('Slug required for user-shop URLs');
}
$host = str_replace('{subdomain}', $slug, $host);
}
$url = $protocol . $host;
if ($path) {
$url .= '/' . ltrim($path, '/');
}
return $url;
}
/**
* 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}");
}
/**
* Clear all user shop caches
*/
public function clearAllUserShopCaches(): void
{
// In Laravel mit Cache-Tags würde das eleganter funktionieren
// Für jetzt eine einfache Lösung für häufig verwendete Shops
$commonSlugs = ['aloevera']; // Füge häufig verwendete Slugs hinzu
foreach ($commonSlugs as $slug) {
$this->clearUserShopCache($slug);
}
}
/**
* Get default user shop for main domain (fallback)
*/
public function getDefaultUserShop(): ?UserShop
{
$defaultSlug = $this->domainConfig['domains']['shop']['default_user_shop'] ?? 'aloevera';
return $this->getUserShop($defaultSlug);
}
/**
* Validate domain configuration
*/
public function validateConfiguration(): array
{
$errors = [];
// Validate main domains
$requiredDomains = ['main', 'shop', 'crm', 'portal', 'checkout', 'user-shop'];
foreach ($requiredDomains as $domain) {
if (empty($this->domainConfig['domains'][$domain]['host'])) {
$errors[] = "Domain '{$domain}' not configured";
}
}
// Validate protocol
if (empty($this->domainConfig['protocol'])) {
$errors[] = 'Protocol not configured';
}
// Validate reserved subdomains
if (empty($this->domainConfig['reserved_subdomains'])) {
$errors[] = 'Reserved subdomains not configured';
}
// Validate shop default
$defaultShop = $this->domainConfig['domains']['shop']['default_user_shop'] ?? null;
if (!$defaultShop) {
$errors[] = 'Default user shop not configured for shop domain';
}
return $errors;
}
/**
* Check if domain configuration is valid
*/
public function isConfigurationValid(): bool
{
return empty($this->validateConfiguration());
}
}

View file

@ -0,0 +1,232 @@
<?php
namespace App\Services;
use App\Models\UserBusiness;
use App\Models\UserLevel;
use App\User;
use Illuminate\Support\Collection;
class LevelReportService
{
public function getLevelPromotions(array $filters = []): Collection
{
$month = $filters['month'] ?? null;
$year = $filters['year'] ?? null;
$userId = $filters['user_id'] ?? null;
$onlyNotUpdated = $filters['only_not_updated'] ?? false;
// Lade UserLevels für Referenz
$userLevels = UserLevel::where('active', 1)->orderBy('pos')->get()->keyBy('id');
// Query UserBusiness Einträge mit Level-Aufstiegen
$query = UserBusiness::whereNotNull('next_qual_user_level')
->whereRaw("JSON_LENGTH(next_qual_user_level) > 0")
->orderBy('year', 'desc')
->orderBy('month', 'desc')
->orderBy('user_id');
// Filter anwenden
if ($month) {
$query->where('month', $month);
}
if ($year) {
$query->where('year', $year);
}
if ($userId) {
$query->where('user_id', $userId);
}
$userBusinesses = $query->get();
return $this->processLevelPromotions($userBusinesses, $userLevels, $onlyNotUpdated);
}
public function processLevelPromotions($userBusinesses, $userLevels, $onlyNotUpdated = false): Collection
{
$promotions = [];
// Lade User-Daten für alle Level-Vergleiche
$userIds = $userBusinesses->pluck('user_id')->unique();
$users = User::whereIn('id', $userIds)->get(['id', 'm_level']);
$currentUserLevels = $users->keyBy('id');
foreach ($userBusinesses as $userBusiness) {
$nextQualUserLevel = $userBusiness->next_qual_user_level;
if (is_array($nextQualUserLevel) && !empty($nextQualUserLevel)) {
// next_qual_user_level kann sowohl ein einzelnes Level-Objekt als auch ein Array von Level-Objekten sein
$levelArray = isset($nextQualUserLevel['id']) ? [$nextQualUserLevel] : $nextQualUserLevel;
foreach ($levelArray as $newLevelData) {
// Überprüfe ob es ein vollständiges Level-Objekt ist
if (is_array($newLevelData) && isset($newLevelData['id'])) {
$currentLevel = $userLevels->get($userBusiness->m_level_id);
$newLevelId = $newLevelData['id'];
// Lade aktuellen User Level
$currentUser = $currentUserLevels->get($userBusiness->user_id);
$currentUserLevelName = 'Unbekannt';
if ($currentUser && $currentUser->m_level) {
$currentUserLevel = $userLevels->get($currentUser->m_level);
$currentUserLevelName = $currentUserLevel ? $currentUserLevel->name : 'Level ID: ' . $currentUser->m_level;
}
// Filter: Nur User die noch nicht auf das neue Level umgestellt wurden
if ($onlyNotUpdated) {
if (!$currentUser || $currentUser->m_level == $newLevelId) {
continue; // Skip - User ist bereits auf das neue Level umgestellt
}
}
$promotions[] = [
'user_id' => $userBusiness->user_id,
'email' => $userBusiness->email,
'first_name' => $userBusiness->first_name,
'last_name' => $userBusiness->last_name,
'month' => $userBusiness->month,
'year' => $userBusiness->year,
'date' => sprintf('%04d-%02d', $userBusiness->year, $userBusiness->month),
'from_level_id' => $userBusiness->m_level_id,
'from_level_name' => $currentLevel ? $currentLevel->name : 'Unbekannt',
'to_level_id' => $newLevelId,
'to_level_name' => $newLevelData['name'] ?? 'Unbekannt',
'to_level_margin' => $newLevelData['margin'] ?? 0,
'to_level_margin_shop' => $newLevelData['margin_shop'] ?? 0,
'to_level_qual_kp' => $newLevelData['qual_kp'] ?? 0,
'to_level_qual_pp' => $newLevelData['qual_pp'] ?? 0,
'to_level_growth_bonus' => $newLevelData['growth_bonus'] ?? 0,
'to_level_pos' => $newLevelData['pos'] ?? 0,
'current_user_level_id' => $currentUser ? $currentUser->m_level : null,
'current_user_level_name' => $currentUserLevelName,
'level_updated' => $onlyNotUpdated ? 'Nein' : ($currentUser && $currentUser->m_level == $newLevelId ? 'Ja' : 'Nein'),
'total_pp' => $userBusiness->total_pp ?? 0,
'total_qual_pp' => $userBusiness->total_qual_pp ?? 0,
'payline_points_qual_kp' => $userBusiness->payline_points_qual_kp ?? 0,
'sales_volume_points_sum' => $userBusiness->sales_volume_points_KP_sum ?? 0,
'active_account' => $userBusiness->active_account ? 'Ja' : 'Nein',
];
}
}
}
}
// Sortiere nach Datum (neueste zuerst) und dann nach User ID
usort($promotions, function ($a, $b) {
if ($a['year'] !== $b['year']) {
return $b['year'] - $a['year'];
}
if ($a['month'] !== $b['month']) {
return $b['month'] - $a['month'];
}
return $a['user_id'] - $b['user_id'];
});
return collect($promotions);
}
public function getStatistics(Collection $promotions): array
{
$stats = [
'total_count' => $promotions->count(),
'level_stats' => [],
'period_stats' => []
];
// Statistik nach Level
foreach ($promotions as $promotion) {
$levelName = $promotion['to_level_name'];
if (!isset($stats['level_stats'][$levelName])) {
$stats['level_stats'][$levelName] = 0;
}
$stats['level_stats'][$levelName]++;
}
// Statistik nach Monat/Jahr
foreach ($promotions as $promotion) {
$period = $promotion['date'];
if (!isset($stats['period_stats'][$period])) {
$stats['period_stats'][$period] = 0;
}
$stats['period_stats'][$period]++;
}
return $stats;
}
public function exportToCsv(Collection $promotions, string $filename = null): string
{
if (!$filename) {
$filename = 'level_promotions_' . date('Y-m-d_H-i-s') . '.csv';
}
$filepath = storage_path('app/reports/' . $filename);
// Erstelle Verzeichnis falls nicht vorhanden
if (!file_exists(dirname($filepath))) {
mkdir(dirname($filepath), 0755, true);
}
$file = fopen($filepath, 'w');
// UTF-8 BOM für korrekte Darstellung in Excel
fwrite($file, "\xEF\xBB\xBF");
// CSV Headers
$headers = [
'Datum',
'User ID',
'Vorname',
'Nachname',
'E-Mail',
'Von Level ID',
'Von Level Name',
'Zu Level ID',
'Zu Level Name',
'Zu Level KP Anforderung',
'Zu Level PP Anforderung',
'User PP Total',
'User PP Qualifiziert',
'Aktueller User Level ID',
'Aktueller User Level Name',
'Level bereits geupdatet',
'Aktiver Account',
'Monat',
'Jahr'
];
fputcsv($file, $headers, ';');
// Daten schreiben
foreach ($promotions as $promotion) {
$row = [
$promotion['date'],
$promotion['user_id'],
$promotion['first_name'],
$promotion['last_name'],
$promotion['email'],
$promotion['from_level_id'],
$promotion['from_level_name'],
$promotion['to_level_id'],
$promotion['to_level_name'],
$promotion['to_level_qual_kp'],
$promotion['to_level_qual_pp'],
$promotion['sales_volume_points_sum'],
$promotion['payline_points_qual_kp'],
$promotion['current_user_level_id'] ?? 'N/A',
$promotion['current_user_level_name'],
$promotion['level_updated'],
$promotion['active_account'],
$promotion['month'],
$promotion['year']
];
fputcsv($file, $row, ';');
}
fclose($file);
return $filepath;
}
}

View file

@ -265,6 +265,15 @@ class Util
return false;
}
public static function getCustomerUserShopDomain()
{
if (\Auth::guard('customers')->check()) {
if (\Auth::guard('customers')->user()->user_shop_domain) {
return \Auth::guard('customers')->user()->user_shop_domain;
}
}
return self::getMyMivitaShopUrl();
}
public static function getMyMivitaShopUrl($add_url = "")
{

0
app/Services/dbip/dbip-convert.php Executable file → Normal file
View file

0
app/Services/dbip/dbip-update.php Executable file → Normal file
View file

0
app/Services/dbip/dbip.class.php Executable file → Normal file
View file

0
app/Services/dbip/import.php Executable file → Normal file
View file

0
app/Services/dbip/lookup-example.php Executable file → Normal file
View file