23-01-2026
This commit is contained in:
parent
a939cd51ef
commit
a8b395e20d
248 changed files with 29342 additions and 4805 deletions
381
app/Services/BusinessPlan/GrowthBonusCalculator.php
Normal file
381
app/Services/BusinessPlan/GrowthBonusCalculator.php
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\BusinessPlan;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Service für die Berechnung des Growth Bonus (Tiefenbonus)
|
||||
* Implementiert die Differenz-Bonus-Logik ab Ebene 1
|
||||
*/
|
||||
class GrowthBonusCalculator
|
||||
{
|
||||
/**
|
||||
* Berechnet den Growth Bonus für einen BusinessUserItemOptimized
|
||||
*
|
||||
* @param BusinessUserItemOptimized $userItem Der User, für den der Bonus berechnet wird
|
||||
* @param object $qualUserLevel Das Qualifikations-Level-Objekt des Users
|
||||
* @return float Der berechnete Bonus
|
||||
*/
|
||||
public function calculate(BusinessUserItemOptimized $userItem, $qualUserLevel): float
|
||||
{
|
||||
// Basis-Check: Hat der User überhaupt Anspruch auf Growth Bonus?
|
||||
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Falls keine direkte Downline-Struktur geladen ist, kann kein Growth Bonus berechnet werden
|
||||
if (empty($userItem->businessUserItems) && !empty($userItem->business_lines)) {
|
||||
Log::warning("GrowthBonusCalculator: Growth Bonus calculation requires loaded child structure (businessUserItems is empty for user {$userItem->user_id})");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return $this->calculateRecursive($userItem, $qualUserLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die eigentliche Berechnung basierend auf der Differenz-Logik durch
|
||||
*/
|
||||
private function calculateRecursive(BusinessUserItemOptimized $userItem, $qualUserLevel): float
|
||||
{
|
||||
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
|
||||
$totalGrowthBonus = 0.0;
|
||||
|
||||
// Iteriere über alle direkten Beine (Firstlines)
|
||||
foreach ($userItem->businessUserItems as $childItem) {
|
||||
|
||||
// Hole die Volumen-Verteilung aus diesem Bein
|
||||
// Array-Format: ['0.0' => 1000, '1.5' => 5000]
|
||||
// Bedeutung: 1000 Punkte sind mit 0% geschützt, 5000 Punkte mit 1.5%
|
||||
$volumeDistribution = $this->getVolumeByProtectionLevel($childItem);
|
||||
|
||||
foreach ($volumeDistribution as $protectedPercentStr => $volume) {
|
||||
$alreadyDistributedPercent = (float) $protectedPercentStr;
|
||||
|
||||
// Differenz berechnen: Mein Anspruch MINUS was schon verteilt wurde
|
||||
$mySharePercent = max(0, $myGrowthPercent - $alreadyDistributedPercent);
|
||||
|
||||
if ($mySharePercent > 0) {
|
||||
$commission = round($volume / 100 * $mySharePercent, 2);
|
||||
$totalGrowthBonus += $commission;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $totalGrowthBonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert detaillierte Informationen zur Berechnung für die Anzeige
|
||||
*
|
||||
* @return array Detaillierte Aufschlüsselung pro Bein
|
||||
*/
|
||||
public function getCalculationDetails(BusinessUserItemOptimized $userItem, $qualUserLevel): array
|
||||
{
|
||||
$details = [];
|
||||
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
|
||||
return $details;
|
||||
}
|
||||
|
||||
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
|
||||
|
||||
// Iteriere über alle direkten Beine (Firstlines)
|
||||
foreach ($userItem->businessUserItems as $childItem) {
|
||||
$legDetails = [
|
||||
'user_id' => $childItem->user_id,
|
||||
'first_name' => $childItem->first_name,
|
||||
'last_name' => $childItem->last_name,
|
||||
'level_name' => $childItem->user_level_name,
|
||||
'volume_distribution' => [],
|
||||
'total_commission' => 0.0,
|
||||
'total_volume' => 0.0
|
||||
];
|
||||
|
||||
$volumeDistribution = $this->getVolumeByProtectionLevel($childItem);
|
||||
|
||||
foreach ($volumeDistribution as $protectedPercentStr => $volume) {
|
||||
$alreadyDistributedPercent = (float) $protectedPercentStr;
|
||||
$mySharePercent = max(0, $myGrowthPercent - $alreadyDistributedPercent);
|
||||
$commission = 0.0;
|
||||
|
||||
if ($mySharePercent > 0) {
|
||||
$commission = round($volume / 100 * $mySharePercent, 2);
|
||||
}
|
||||
|
||||
$legDetails['volume_distribution'][] = [
|
||||
'protected_percent' => $alreadyDistributedPercent,
|
||||
'volume' => $volume,
|
||||
'my_share_percent' => $mySharePercent,
|
||||
'commission' => $commission
|
||||
];
|
||||
|
||||
$legDetails['total_commission'] += $commission;
|
||||
$legDetails['total_volume'] += $volume;
|
||||
}
|
||||
|
||||
// Sortiere nach Protection Level
|
||||
usort($legDetails['volume_distribution'], function ($a, $b) {
|
||||
return $a['protected_percent'] <=> $b['protected_percent'];
|
||||
});
|
||||
|
||||
if ($legDetails['total_volume'] > 0) {
|
||||
$details[] = $legDetails;
|
||||
}
|
||||
}
|
||||
|
||||
// Sortiere Beine nach höchster Provision
|
||||
usort($details, function ($a, $b) {
|
||||
return $b['total_commission'] <=> $a['total_commission'];
|
||||
});
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert eine Matrix-Sicht für die detaillierte Darstellung
|
||||
* Zeilen = Beine (Legs), Spalten = Ebenen (Levels)
|
||||
*/
|
||||
public function getMatrixDetails(BusinessUserItemOptimized $userItem, $qualUserLevel): array
|
||||
{
|
||||
$details = [];
|
||||
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
|
||||
return $details;
|
||||
}
|
||||
|
||||
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
|
||||
|
||||
foreach ($userItem->businessUserItems as $childItem) {
|
||||
|
||||
$legData = [
|
||||
'user' => [
|
||||
'id' => $childItem->user_id,
|
||||
'name' => $childItem->first_name . ' ' . $childItem->last_name,
|
||||
'level' => $childItem->user_level_name
|
||||
],
|
||||
'levels' => [],
|
||||
'total_commission' => 0.0,
|
||||
'total_volume' => 0.0
|
||||
];
|
||||
|
||||
// Rekursiv die Ebenen dieses Beins einsammeln
|
||||
// Start bei Ebene 1 (das ist das Kind selbst)
|
||||
// Initial Protection ist 0 (vom Upline/Mir kommt kein Schutz, der relevant wäre, da ICH ja der Empfänger bin)
|
||||
$this->collectLegLevels($childItem, 1, 0.0, $myGrowthPercent, $legData);
|
||||
|
||||
if (!empty($legData['levels'])) {
|
||||
// Sortieren nach Ebenen-Index
|
||||
ksort($legData['levels']);
|
||||
$details[] = $legData;
|
||||
}
|
||||
}
|
||||
|
||||
// Sortieren nach Gesamt-Provision
|
||||
usort($details, function ($a, $b) {
|
||||
return $b['total_commission'] <=> $a['total_commission'];
|
||||
});
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rekursive Hilfsfunktion für Matrix-Daten
|
||||
*/
|
||||
private function collectLegLevels(BusinessUserItemOptimized $item, int $level, float $incomingProtection, float $myPercent, array &$legData)
|
||||
{
|
||||
// 1. Eigenen Status ermitteln (Schutz für Downline)
|
||||
// WICHTIG: getQualifiedGrowthBonus() funktioniert sowohl für:
|
||||
// - Live-berechnete Daten (qualificationCalculated = true)
|
||||
// - Gespeicherte Daten aus DB (qual_user_level bereits vorhanden)
|
||||
$userProtection = $item->getQualifiedGrowthBonus();
|
||||
$userLevelName = '';
|
||||
|
||||
if ($userProtection > 0) {
|
||||
$qual = $item->getQualUserLevel();
|
||||
$userLevelName = is_array($qual) ? ($qual['name'] ?? '') : ($qual->name ?? '');
|
||||
}
|
||||
|
||||
// Berechnung für diesen User (Ebene)
|
||||
$volume = (float) ($item->sales_volume_points_TP_sum ?? 0);
|
||||
|
||||
// Auch User ohne Volumen in die Matrix aufnehmen, wenn sie einen Status haben (Blocker sichtbar machen)
|
||||
// Aber wir brauchen Volumen für die Relevanz. Wenn Volumen 0, dann ist der Block hier (noch) egal,
|
||||
// wirkt aber auf die Ebenen darunter.
|
||||
|
||||
if ($volume > 0 || $userProtection > 0) {
|
||||
// Differenz: Mein Anspruch - Schutz von oben
|
||||
$diffPercent = max(0, $myPercent - $incomingProtection);
|
||||
$commission = round($volume / 100 * $diffPercent, 2);
|
||||
|
||||
if (!isset($legData['levels'][$level])) {
|
||||
$legData['levels'][$level] = [
|
||||
'volume' => 0.0,
|
||||
'commission' => 0.0,
|
||||
'details' => [],
|
||||
'has_blocker' => false, // Flag für UI
|
||||
'blocker_name' => ''
|
||||
];
|
||||
}
|
||||
|
||||
$legData['levels'][$level]['volume'] += $volume;
|
||||
$legData['levels'][$level]['commission'] += $commission;
|
||||
|
||||
// Markiere Blocker
|
||||
if ($userProtection > 0) {
|
||||
$legData['levels'][$level]['has_blocker'] = true;
|
||||
$legData['levels'][$level]['blocker_name'] = $userLevelName . ' (' . $userProtection . '%)';
|
||||
}
|
||||
|
||||
// Detail-Information für Hover/Debug
|
||||
$legData['levels'][$level]['details'][] = [
|
||||
'u' => $item->user_id,
|
||||
'n' => $item->first_name . ' ' . $item->last_name, // Name für Tooltip
|
||||
'v' => $volume,
|
||||
'p_in' => $incomingProtection,
|
||||
'p_own' => $userProtection,
|
||||
'pct' => $diffPercent
|
||||
];
|
||||
|
||||
$legData['total_volume'] += $volume;
|
||||
$legData['total_commission'] += $commission;
|
||||
}
|
||||
|
||||
// Protection für nächste Ebene: Maximum aus was von oben kam und was dieser User beansprucht
|
||||
$nextProtection = max($incomingProtection, $userProtection);
|
||||
|
||||
// Rekursion (Begrenzt auf 30 Ebenen für Anzeige)
|
||||
if ($level < 30 && !empty($item->businessUserItems)) {
|
||||
foreach ($item->businessUserItems as $child) {
|
||||
$this->collectLegLevels($child, $level + 1, $nextProtection, $myPercent, $legData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert das Volumen der Downline eines Users gruppiert nach dem "bereits verteilten Prozentsatz" (Protection Level).
|
||||
* Rekursive Funktion, die die "Differenz-Logik" vorbereitet.
|
||||
*
|
||||
* @param BusinessUserItemOptimized $item
|
||||
* @return array<string, float> Key = Protected Percent, Value = Volume Points
|
||||
*/
|
||||
public function getVolumeByProtectionLevel(BusinessUserItemOptimized $item, int $depth = 0): array
|
||||
{
|
||||
// Schutz vor zu tiefer Rekursion (Performance)
|
||||
$maxDepth = 20;
|
||||
if ($depth > $maxDepth) {
|
||||
Log::warning("GrowthBonusCalculator: Max recursion depth reached for user {$item->user_id}");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Bei Live-Berechnung: Qualifikation berechnen falls nötig
|
||||
// Bei gespeicherten Daten: qual_user_level ist bereits vorhanden
|
||||
if (!$item->isQualificationCalculated() && !$item->isQualLevel()) {
|
||||
$item->calcQualPP();
|
||||
}
|
||||
|
||||
$volumes = [];
|
||||
|
||||
// 1. Eigenes Volumen (Unprotected / GAP)
|
||||
// Man selbst schützt seinen eigenen Umsatz NICHT vor der Upline.
|
||||
// Daher Start mit Protection Level 0.0
|
||||
$ownVolume = (float) ($item->sales_volume_points_TP_sum ?? 0);
|
||||
|
||||
if ($ownVolume > 0) {
|
||||
$key = '0.0';
|
||||
$volumes[$key] = $ownVolume;
|
||||
}
|
||||
|
||||
// 2. Mein Schutz-Level ermitteln (für das Volumen, das durch mich hindurch fließt)
|
||||
// WICHTIG: getQualifiedGrowthBonus() funktioniert sowohl für:
|
||||
// - Live-berechnete Daten (qualificationCalculated = true)
|
||||
// - Gespeicherte Daten aus DB (qual_user_level bereits vorhanden)
|
||||
$myProtectionPercent = $item->getQualifiedGrowthBonus();
|
||||
|
||||
// 3. Kinder laden falls nicht vorhanden (für gespeicherte Daten)
|
||||
if (empty($item->businessUserItems) && $item->user_id) {
|
||||
$this->loadChildrenFromDatabase($item, $depth);
|
||||
}
|
||||
|
||||
// 4. Rekursive Aggregation der Kinder
|
||||
if (!empty($item->businessUserItems)) {
|
||||
foreach ($item->businessUserItems as $childItem) {
|
||||
|
||||
// Rekursiver Aufruf
|
||||
$childVolumes = $this->getVolumeByProtectionLevel($childItem, $depth + 1);
|
||||
|
||||
// Schutz-Level anwenden und aggregieren
|
||||
foreach ($childVolumes as $protectedPercentStr => $vol) {
|
||||
$incomingProtection = (float) $protectedPercentStr;
|
||||
|
||||
// Das Volumen ist bereits mit $incomingProtection geschützt.
|
||||
// Da es nun durch diesen User fließt, erhöht sich der Schutz auf dessen Level (falls höher).
|
||||
$effectiveProtection = max($incomingProtection, $myProtectionPercent);
|
||||
|
||||
$newKey = (string) $effectiveProtection;
|
||||
|
||||
if (!isset($volumes[$newKey])) {
|
||||
$volumes[$newKey] = 0.0;
|
||||
}
|
||||
$volumes[$newKey] += $vol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $volumes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Kinder eines Users aus der Datenbank für die Growth Bonus Berechnung
|
||||
*
|
||||
* WICHTIG: Diese Methode wird nur aufgerufen, wenn businessUserItems leer ist
|
||||
* (typischerweise bei gespeicherten Daten)
|
||||
*/
|
||||
private function loadChildrenFromDatabase(BusinessUserItemOptimized $item, int $depth): void
|
||||
{
|
||||
// Lade Sponsor-Beziehungen aus der User-Tabelle
|
||||
$childIds = \App\User::where('m_sponsor', $item->user_id)
|
||||
->where('deleted_at', null)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
if (empty($childIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hole das Date-Objekt vom Item
|
||||
$date = $item->getDate();
|
||||
if (!$date || !isset($date->month) || !isset($date->year)) {
|
||||
Log::warning("GrowthBonusCalculator: No valid date for loading children of user {$item->user_id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lade UserBusiness-Daten für alle Kinder
|
||||
$childBusinesses = \App\Models\UserBusiness::whereIn('user_id', $childIds)
|
||||
->where('month', $date->month)
|
||||
->where('year', $date->year)
|
||||
->get()
|
||||
->keyBy('user_id');
|
||||
|
||||
foreach ($childIds as $childId) {
|
||||
$childBusiness = $childBusinesses->get($childId);
|
||||
|
||||
// Nur Kinder mit Daten und Volumen berücksichtigen
|
||||
if (!$childBusiness) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$childTPSum = (float) ($childBusiness->sales_volume_points_TP_sum ?? 0);
|
||||
|
||||
// Nur relevante Kinder (mit Volumen oder qualifiziertem Level)
|
||||
if ($childTPSum <= 0 && empty($childBusiness->qual_user_level)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Erstelle ein BusinessUserItem aus den gespeicherten Daten
|
||||
$childItem = new BusinessUserItemOptimized($date, null);
|
||||
$childItem->makeUser($childId, false); // Aus DB laden
|
||||
$childItem->addUserID();
|
||||
|
||||
$item->businessUserItems[] = $childItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue