381 lines
15 KiB
PHP
381 lines
15 KiB
PHP
<?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;
|
|
}
|
|
}
|
|
}
|