update 20.10.2025
This commit is contained in:
parent
8c11130b5d
commit
a939cd51ef
616 changed files with 84821 additions and 4121 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = ' €';
|
||||
}
|
||||
|
||||
|
||||
$totalFormatted = $type === 'points' ? $total : formatNumber($total);
|
||||
$individualFormatted = $type === 'points' ? $individual : formatNumber($individual);
|
||||
$shopFormatted = $type === 'points' ? $shop : formatNumber($shop);
|
||||
$suffix = $type === 'points' ? '' : ' €';
|
||||
|
||||
|
||||
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' ? '' : ' €';
|
||||
|
||||
|
||||
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 .= ' <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 .= ' <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'] ?? [],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
432
app/Services/DhlTrackingService.php
Normal file
432
app/Services/DhlTrackingService.php
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
232
app/Services/LevelReportService.php
Normal file
232
app/Services/LevelReportService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
0
app/Services/dbip/dbip-convert.php
Executable file → Normal file
0
app/Services/dbip/dbip-update.php
Executable file → Normal file
0
app/Services/dbip/dbip-update.php
Executable file → Normal file
0
app/Services/dbip/dbip.class.php
Executable file → Normal file
0
app/Services/dbip/dbip.class.php
Executable file → Normal file
0
app/Services/dbip/import.php
Executable file → Normal file
0
app/Services/dbip/import.php
Executable file → Normal file
0
app/Services/dbip/lookup-example.php
Executable file → Normal file
0
app/Services/dbip/lookup-example.php
Executable file → Normal file
Loading…
Add table
Add a link
Reference in a new issue