23-01-2026

This commit is contained in:
Kevin Adametz 2026-01-23 17:35:23 +01:00
parent a939cd51ef
commit a8b395e20d
248 changed files with 29342 additions and 4805 deletions

View file

@ -8,9 +8,7 @@ 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;
/**
@ -31,6 +29,7 @@ class BusinessUserItemOptimized
private ?TreeCalcBotOptimized $treeCalcBot = null;
private $user_level_active_pos;
private $needsQualificationRecalculation = false;
private $qualificationCalculated = false;
public function __construct($date, ?TreeCalcBotOptimized $treeCalcBot = null)
{
@ -40,6 +39,12 @@ class BusinessUserItemOptimized
return $this;
}
public function isQualificationCalculated(): bool
{
return $this->qualificationCalculated;
}
/**
* Erstellt BusinessUser aus User-ID (Original-Methode für Rückwärtskompatibilität)
*
@ -107,6 +112,7 @@ class BusinessUserItemOptimized
*/
public function makeUserFromModel(User $user, bool $forceLiveCalculation = false): void
{
\Log::debug("BusinessUserItemOptimized: makeUserFromModel for user {$user->id} ({$this->date->month}/{$this->date->year})");
try {
if (!$user || !$user->id) {
throw new \InvalidArgumentException('Invalid user model provided');
@ -121,7 +127,6 @@ class BusinessUserItemOptimized
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);
@ -134,19 +139,18 @@ class BusinessUserItemOptimized
return; // Bereits berechnete Daten verwenden
}
} else {
\Log::debug("BusinessUserItem: Force live calculation for user {$user->id} ({$this->date->month}/{$this->date->year})");
\Log::debug("BusinessUserItemOptimized: Force live calculation for user {$user->id} ({$this->date->month}/{$this->date->year})");
}
// 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();
//$this->calcQualPP();
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error creating user from model {$user->id}: " . $e->getMessage());
\Log::error("BusinessUserItemOptimized: Error creating user from model {$user->id}: " . $e->getMessage());
throw $e;
}
}
@ -204,6 +208,9 @@ class BusinessUserItemOptimized
'qual_kp' => $user_level_active ? max(0, $user_level_active->qual_kp) : 0,
'qual_pp' => $user_level_active ? max(0, $user_level_active->qual_pp) : 0,
'active_growth_bonus' => $user_level_active ? (float)$user_level_active->growth_bonus : 0,
'growth_bonus_details' => null,
// Initialisierung
'payline_points' => 0,
'commission_pp_total' => 0,
@ -223,6 +230,7 @@ class BusinessUserItemOptimized
$this->b_user->commission_shop_sales = $calculatedCommission;
\Log::debug("BusinessUserItem: Created optimized user {$user->id} for {$this->date->month}/{$this->date->year} - Shop commission: {$calculatedCommission} (Volume: {$shopVolume}, Margin: {$shopMargin}%)");
\Log::debug("BusinessUserItemOptimized: b_user: " . json_encode($this->b_user));
}
/**
@ -428,6 +436,24 @@ class BusinessUserItemOptimized
$this->b_user->business_lines[$line] = $obj;
}
/**
* Initialisiert leere business_lines für diesen User
*/
public function initBusinessLines(): void
{
if (!isset($this->b_user->business_lines) || !is_array($this->b_user->business_lines)) {
$this->b_user->business_lines = [];
}
}
/**
* Prüft ob eine business_line existiert
*/
public function hasBusinessLine(int $line): bool
{
return isset($this->b_user->business_lines[$line]);
}
public function addBusinessLinePoints($line, $points)
{
if (!isset($this->b_user->business_lines[$line])) {
@ -451,6 +477,78 @@ class BusinessUserItemOptimized
$this->b_user->business_lines[$line] = $obj;
}
/**
* Gibt Details zur Growth Bonus Berechnung zurück (für die View)
* Nur für Monate ab November 2025 verfügbar (neue Logik)
*/
public function getGrowthBonusBreakdown(): array
{
// Prüfe ob Legacy-Monat (vor November 2025)
$isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11);
if ($isLegacy) {
return [];
}
if (!$this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) {
return [];
}
try {
$calculator = new GrowthBonusCalculator();
// Array zu Object konvertieren für Calculator
$qualData = (object) $this->b_user->qual_user_level;
return $calculator->getCalculationDetails($this, $qualData);
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error getting growth bonus breakdown: " . $e->getMessage());
return [];
}
}
/**
* Gibt Matrix-Details zur Growth Bonus Berechnung zurück (für die View)
* Nur für Monate ab November 2025 verfügbar (neue Logik)
*/
public function getGrowthBonusMatrix(): array
{
// Prüfe ob Legacy-Monat (vor November 2025)
$isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11);
if ($isLegacy) {
return [];
}
if (!$this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) {
return [];
}
// Use stored details if available (avoid recalculation)
if (!empty($this->b_user->growth_bonus_details)) {
if (is_object($this->b_user->growth_bonus_details) && method_exists($this->b_user->growth_bonus_details, 'toArray')) {
return $this->b_user->growth_bonus_details->toArray();
}
if (is_array($this->b_user->growth_bonus_details)) {
return $this->b_user->growth_bonus_details;
}
// Fallback for standard object
if (is_object($this->b_user->growth_bonus_details)) {
return json_decode(json_encode($this->b_user->growth_bonus_details), true);
}
}
try {
$calculator = new GrowthBonusCalculator();
// Array zu Object konvertieren für Calculator
$qualData = (object) $this->b_user->qual_user_level;
return $calculator->getMatrixDetails($this, $qualData);
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error getting growth bonus matrix: " . $e->getMessage());
return [];
}
}
public function addTotalTP($points)
{
$this->b_user->total_pp += (float) $points; // Type-Safety
@ -466,6 +564,67 @@ class BusinessUserItemOptimized
return !empty($this->b_user->qual_user_level);
}
/**
* Methode für Zugriff auf qual_user_level (auch für GrowthBonusCalculator)
*/
public function getQualUserLevel()
{
return $this->b_user->qual_user_level ?? null;
}
public function getActiveGrowthBonus()
{
return $this->active_growth_bonus;
}
/**
* Gibt das Date-Objekt zurück (für GrowthBonusCalculator)
*/
public function getDate()
{
return $this->date;
}
/**
* Gibt den Growth Bonus basierend auf dem ERREICHTEN Qualifikations-Level zurück.
*
* WICHTIG: Diese Methode gibt den Growth Bonus nur zurück, wenn der Partner
* in dem Monat tatsächlich das entsprechende Level qualifiziert hat.
* Das ist entscheidend für die korrekte Differenz-Berechnung im GrowthBonusCalculator.
*
* Die Methode funktioniert sowohl für:
* - Live-berechnete Daten (qualificationCalculated = true)
* - Gespeicherte/geladene Daten aus UserBusiness (qual_user_level bereits vorhanden)
*
* @return float Der Growth Bonus des erreichten Qualifikations-Levels (0 wenn nicht qualifiziert)
*/
public function getQualifiedGrowthBonus(): float
{
// Prüfen ob b_user existiert
if (empty($this->b_user)) {
return 0.0;
}
// Prüfen ob ein Qualifikations-Level erreicht wurde
// Dies funktioniert sowohl für live-berechnete als auch für gespeicherte Daten
if (empty($this->b_user->qual_user_level)) {
return 0.0;
}
// Handle array und object Zugriff (JSON-Deserialisierung kann beides liefern)
$qualLevel = $this->b_user->qual_user_level;
if (is_array($qualLevel)) {
return (float) ($qualLevel['growth_bonus'] ?? 0.0);
}
if (is_object($qualLevel)) {
return (float) ($qualLevel->growth_bonus ?? 0.0);
}
return 0.0;
}
public function isQualEqualLevel(): bool
{
if (!$this->b_user->qual_user_level) {
@ -502,11 +661,26 @@ class BusinessUserItemOptimized
public function calcQualPP($force = false): void
{
if ($this->qualificationCalculated && !$force) {
return;
}
// Mark as calculated immediately to prevent potential recursion loops
$this->qualificationCalculated = true;
try {
$qualUserLevel = $this->calcuQualLevel();
\Log::debug("BusinessUserItemOptimized: calcQualPP for user {$this->b_user->user_id}: " . json_encode($qualUserLevel));
if ($qualUserLevel !== null) {
//das erreichte level setzen
$this->b_user->qual_user_level = $qualUserLevel->toArray();
// Wichtig: Setze die qual_kp und qual_pp des erreichten Levels im b_user Objekt
// Diese Werte ändern sich je nach erreichtem Level und müssen hier aktualisiert werden
$this->b_user->qual_kp = $qualUserLevel->qual_kp;
$this->b_user->qual_pp = $qualUserLevel->qual_pp;
\Log::debug("BusinessUserItemOptimized: Set qual_kp={$qualUserLevel->qual_kp}, qual_pp={$qualUserLevel->qual_pp} for user {$this->b_user->user_id}");
//next_qual_user_level nächster qualifizierten level
$this->setNextUserLevel($force);
//qual_user_level_next nächste Provisions-Stufe,
@ -557,30 +731,31 @@ class BusinessUserItemOptimized
// Growth Bonus
if (!empty($qualUserLevel->growth_bonus)) {
$payline = (int) $this->b_user->qual_user_level['paylines'] + 1;
$maxlines = count($this->b_user->business_lines) + 1;
$growth_bonus = (float) $this->b_user->qual_user_level['growth_bonus'];
// Fallback für alte Monate (vor November 2025)
// Stichtag: 01.11.2025 - Alles davor nutzt die Legacy-Berechnung
$isLegacy = ($this->date->year < 2025) || ($this->date->year == 2025 && $this->date->month < 11);
for ($i = $payline; $i <= $maxlines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$object = $this->b_user->business_lines[$i];
if ($isLegacy) {
$commission_growth_total = $this->calculateLegacyGrowthBonus($qualUserLevel);
\Log::debug("BusinessUserItem: Used LEGACY growth bonus calculation for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year})");
} else {
// Neue Logik ab Dezember 2025 - delegated to new Calculator service
try {
$growthCalculator = new GrowthBonusCalculator();
$commission_growth_total = $growthCalculator->calculate($this, $qualUserLevel);
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($object)) {
$points = (float) ($object['points'] ?? 0);
$object['margin'] = $growth_bonus;
$object['commission'] = round($points / 100 * $growth_bonus, 2);
$object['growth_bonus'] = true;
$commission_growth_total += $object['commission'];
} else {
$points = (float) ($object->points ?? 0);
$object->margin = $growth_bonus;
$object->commission = round($points / 100 * $growth_bonus, 2);
$object->growth_bonus = true;
$commission_growth_total += $object->commission;
}
$this->b_user->business_lines[$i] = $object;
// Calculate matrix details for storage and total sum
// This ensures that the stored details match the calculated total exactly
$matrixDetails = $growthCalculator->getMatrixDetails($this, $qualUserLevel);
// Store details in the model so they can be retrieved later without recalculation
$this->b_user->growth_bonus_details = $matrixDetails;
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error calculating growth bonus for user {$this->b_user->user_id}: " . $e->getMessage());
// Fallback to 0 if calculation fails
$commission_growth_total = 0;
$this->b_user->growth_bonus_details = null;
}
}
}
@ -589,30 +764,96 @@ class BusinessUserItemOptimized
$this->b_user->commission_growth_total = $commission_growth_total;
}
/**
* Alte Berechnungsmethode für Growth Bonus (Kompatibilität für vergangene Monate)
* Berechnet pauschal ab einer bestimmten Ebene ohne Differenz-Prüfung
*/
private function calculateLegacyGrowthBonus($qualUserLevel): float
{
$commission_growth_total = 0;
// Payline aus Level-Daten + 1 (Start des Bonus)
$payline = (int) ($this->b_user->qual_user_level['paylines'] ?? 0) + 1;
$maxlines = count($this->b_user->business_lines ?? []) + 1;
$growth_bonus = (float) ($this->b_user->qual_user_level['growth_bonus'] ?? 0);
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
if (is_array($object)) {
$points = (float) ($object['points'] ?? 0);
$object['margin'] = $growth_bonus;
$object['commission'] = round($points / 100 * $growth_bonus, 2);
$object['growth_bonus'] = true;
$commission_growth_total += $object['commission'];
} else {
if (!is_object($object)) {
$object = (object) $object;
}
$points = (float) ($object->points ?? 0);
$object->margin = $growth_bonus;
$object->commission = round($points / 100 * $growth_bonus, 2);
$object->growth_bonus = true;
$commission_growth_total += $object->commission;
}
$this->b_user->business_lines[$i] = $object;
}
}
return $commission_growth_total;
}
// ===== WEITERE ORIGINAL-METHODEN (gekürzt, vollständige Implementation in Original) =====
//aktuelles level berechnen, max das eigene level, wenn weniger Points dann darunter
/**
* Berechnet das aktuell erreichte Level
* Durchläuft alle möglichen Levels (max. bis zur eigenen User-Level-Position)
* und prüft dynamisch die Qualifikation basierend auf den spezifischen qual_kp und qual_pp des jeweiligen Levels
*/
public function calcuQualLevel()
{
\Log::debug("BusinessUserItemOptimized: calcuQualLevel for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year})");
// Hole alle möglichen Levels bis zur eigenen Position, sortiert nach Position absteigend
// um vom höchsten zum niedrigsten zu prüfen
$qualUserLevels = UserLevel::where('qual_kp', '<=', $this->b_user->sales_volume_points_KP_sum)
->where('pos', '<=', $this->user_level_active_pos)
->orderBy('qual_pp', 'desc')
->orderBy('pos', 'desc') // Sortiere nach Position DESC, um das höchste Level zuerst zu prüfen
->get();
foreach ($qualUserLevels as $qualUserLevel) {
// Berechne die Payline-Punkte für die spezifischen Paylines dieses Levels
$payline_points = $this->getPointsforPayline($qualUserLevel->paylines);
$payline_points_qual_kp = $payline_points + $this->getRestQualKP();
\Log::debug("BusinessUserItemOptimized: payline_points: " . $payline_points);
// WICHTIG: Berechne die Rest-KP basierend auf der qual_kp DES AKTUELL GEPRÜFTEN LEVELS
// nicht der qual_kp des bereits gesetzten Levels (das war der Fehler!)
$rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevel->qual_kp);
$payline_points_qual_kp = $payline_points + $rest_kp;
// Prüfe ob die Qualifikation für diesen spezifischen Level erfüllt ist
if ($payline_points_qual_kp >= $qualUserLevel->qual_pp) {
// Setze die berechneten Werte
$this->b_user->calc_qual_kp = $rest_kp > 0 ? $qualUserLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$this->b_user->payline_points = $payline_points;
$this->b_user->payline_points_qual_kp = $payline_points_qual_kp;
$qualUserLevel->_calculated_qual_kp = $rest_kp > 0 ? $qualUserLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$qualUserLevel->_calculated_payline_points = $payline_points;
$qualUserLevel->_calculated_payline_points_qual_kp = $payline_points_qual_kp;
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} qualifies for level {$qualUserLevel->name} (pos: {$qualUserLevel->pos}) - Payline Points: {$payline_points}, Rest KP: {$rest_kp}, Total: {$payline_points_qual_kp} >= {$qualUserLevel->qual_pp}");
return $qualUserLevel;
}
}
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not qualify for any level");
return null;
}
private function getPointsforPayline($paylines): float
{
\Log::debug("BusinessUserItemOptimized: getPointsforPayline for user {$this->b_user->user_id} ({$this->date->month}/{$this->date->year}) with paylines: " . $paylines . " and business_lines: " . json_encode($this->b_user->business_lines));
$payline_points = 0;
for ($i = 1; $i <= $paylines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
@ -642,7 +883,18 @@ class BusinessUserItemOptimized
->orderBy('qual_pp', 'asc')
->first();
if ($qualUserLevelNext) {
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
// Berechne die spezifischen Werte für diesen Level
$payline_points = $this->getPointsforPayline($qualUserLevelNext->paylines);
$rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevelNext->qual_kp);
$payline_points_qual_kp = $payline_points + $rest_kp;
// Speichere Level-Daten mit berechneten Werten
$levelData = $qualUserLevelNext->toArray();
$levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $qualUserLevelNext->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$levelData['_calculated_payline_points'] = $payline_points;
$levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp;
$this->b_user->qual_user_level_next = $levelData;
} else {
$this->b_user->qual_user_level_next = null;
}
@ -653,24 +905,50 @@ class BusinessUserItemOptimized
private function setNextUserLevel($force = false): void
{
//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')
// Hole nur den direkt nächsten Level (keine Level überspringen!)
$nextLevel = UserLevel::where('pos', '=', $this->user_level_active_pos + 1)
->first();
//wenn der nächste level qualifiziert ist und die KP-Qualifikation erfüllt ist, dann setzt es den nächsten level
if ($nextQualUserLevel && $this->isQualKP()) {
$this->b_user->next_qual_user_level = $nextQualUserLevel->toArray();
$this->b_user->next_can_user_level = null;
} else {
//wenn der nächste level nicht qualifiziert ist, dann sucht es den nächsten level, nach pos
$nextCanUserLevel = UserLevel::where('pos', '>', $this->user_level_active_pos)
->orderBy('qual_pp', 'asc')
->first();
if ($nextCanUserLevel) {
$this->b_user->next_can_user_level = $nextCanUserLevel->toArray();
}
// Wenn kein nächster Level existiert, beende
if (!$nextLevel) {
$this->b_user->next_qual_user_level = null;
$this->b_user->next_can_user_level = null;
\Log::debug("BusinessUserItemOptimized: No next level found for user {$this->b_user->user_id} (already at highest level)");
return;
}
// Berechne die Payline-Punkte für die spezifischen Paylines des nächsten Levels
$payline_points = $this->getPointsforPayline($nextLevel->paylines);
// Berechne die Rest-KP basierend auf dem nächsten Level
$rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $nextLevel->qual_kp);
$payline_points_qual_kp = $payline_points + $rest_kp;
// Erstelle Level-Daten mit berechneten Werten
$levelData = $nextLevel->toArray();
$levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $nextLevel->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$levelData['_calculated_payline_points'] = $payline_points;
$levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp;
// Prüfe die KP-Qualifikation für den nächsten Level
if ($this->b_user->sales_volume_points_KP_sum < $nextLevel->qual_kp) {
// KP-Qualifikation nicht erfüllt - zeige als "next_can_user_level"
$this->b_user->next_can_user_level = $levelData;
$this->b_user->next_qual_user_level = null;
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not meet KP requirement for next level {$nextLevel->name} ({$this->b_user->sales_volume_points_KP_sum} < {$nextLevel->qual_kp})");
return;
}
// Prüfe ob die PP-Qualifikation erfüllt ist
if ($payline_points_qual_kp >= $nextLevel->qual_pp) {
// Qualifiziert für den nächsten Level
$this->b_user->next_qual_user_level = $levelData;
$this->b_user->next_can_user_level = null;
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} qualifies for next level {$nextLevel->name} (Payline Points: {$payline_points}, Rest KP: {$rest_kp}, Total: {$payline_points_qual_kp} >= {$nextLevel->qual_pp})");
} else {
// PP-Qualifikation nicht erfüllt - zeige als "next_can_user_level"
$this->b_user->next_can_user_level = $levelData;
$this->b_user->next_qual_user_level = null;
\Log::debug("BusinessUserItemOptimized: User {$this->b_user->user_id} does not meet PP requirement for next level {$nextLevel->name} ({$payline_points_qual_kp} < {$nextLevel->qual_pp})");
}
}
@ -680,7 +958,15 @@ class BusinessUserItemOptimized
->orderBy('qual_pp', 'asc')
->first();
if ($qualUserLevelNext) {
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
$payline_points = $this->getPointsforPayline($qualUserLevelNext->paylines);
// Berechne die Rest-KP basierend auf dem nächsten Level
$rest_kp = max(0, $this->b_user->sales_volume_points_KP_sum - $qualUserLevelNext->qual_kp);
$payline_points_qual_kp = $payline_points + $rest_kp;
$levelData = $qualUserLevelNext->toArray();
$levelData['_calculated_qual_kp'] = $rest_kp > 0 ? $qualUserLevelNext->qual_kp : $this->b_user->sales_volume_points_KP_sum;
$levelData['_calculated_payline_points'] = $payline_points;
$levelData['_calculated_payline_points_qual_kp'] = $payline_points_qual_kp;
$this->b_user->qual_user_level_next = $levelData;
}
}