mivita/dev/buinessPlan/_bak/GrowthBonusCalculator.php
2026-01-23 17:35:23 +01:00

318 lines
13 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: Nutze getQualifiedGrowthBonus() für das ERREICHTE Qualifikations-Level des Monats
// Nicht getActiveGrowthBonus() verwenden, da das das aktuelle Karriere-Level wäre!
$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) {
// WICHTIG: Der effektive Schutz ist das MAXIMUM aus:
// - Schutz von oben ($incomingProtection)
// - Eigener Schutz des Users ($userProtection)
// Der User schützt sein EIGENES Volumen mit seinem eigenen Growth Bonus!
$effectiveProtection = max($incomingProtection, $userProtection);
$diffPercent = max(0, $myPercent - $effectiveProtection);
$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): array
{
// WICHTIG: Nur calcQualPP aufrufen wenn KEINE gespeicherten Daten vorhanden sind
// Bei gespeicherten Daten ist qual_user_level bereits vorhanden, auch wenn qualificationCalculated=false
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: Nutze getQualifiedGrowthBonus() für das ERREICHTE Qualifikations-Level des Monats
// Nicht getActiveGrowthBonus() verwenden, da das das aktuelle Karriere-Level wäre!
$myProtectionPercent = $item->getQualifiedGrowthBonus();
// Debug-Logging für Troubleshooting
Log::debug("GrowthBonusCalculator: User {$item->user_id} - qualifiedGrowthBonus={$myProtectionPercent}%, activeGrowthBonus={$item->getActiveGrowthBonus()}%, isQualLevel=" . ($item->isQualLevel() ? 'true' : 'false'));
// 3. Rekursive Aggregation der Kinder
if (!empty($item->businessUserItems)) {
foreach ($item->businessUserItems as $childItem) {
// Rekursiver Aufruf
$childVolumes = $this->getVolumeByProtectionLevel($childItem);
// 4. 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;
}
}