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

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\Product;
@ -29,81 +30,124 @@ class AboHelper
];
public static function userHasAbo(User $user){
public static function userHasAbo(User $user)
{
$user = $user ? $user : \Auth::user();
return UserAbo::where('user_id', $user->id)->where('is_for', 'me')->where('status', '>', 1)->first() === null ? false : true;
}
public static function memberHasAbo(ShoppingUser $shopping_user){
if(!$shopping_user){
public static function memberHasAbo(ShoppingUser $shopping_user)
{
if (!$shopping_user) {
return false;
}
return UserAbo::where('email', $shopping_user->billing_email)->where('is_for', 'ot')->where('status', '>', 1)->first() === null ? false : true;
}
public static function hasAboByEmail($email){
public static function hasAboByEmail($email)
{
return UserAbo::where('email', $email)->where('status', '>', 1)->first() === null ? false : true;
}
public static function setAboStatus(ShoppingOrder $shopping_order, $status){
public static function setAboStatus(ShoppingOrder $shopping_order, $status, $paid = false)
{
$user_abo = $shopping_order->getUserAbo();
if($user_abo && $user_abo->status < 2){ //status < 2 is not active
if ($user_abo && $user_abo->status < 2) { //status < 2 is not active
$user_abo->update(['status' => $status]);
}
UserAboOrder::where('user_abo_id', $user_abo->id)->where('shopping_order_id', $shopping_order->id)->update(['status' => $status]);
UserAboOrder::where('user_abo_id', $user_abo->id)->where('shopping_order_id', $shopping_order->id)->update(['status' => $status, 'paid' => $paid]);
}
public static function setAboActive(ShoppingOrder $shopping_order, $status){
self::setAboStatus($shopping_order, $status);
public static function setAboActive(ShoppingOrder $shopping_order, $status, $paid = false)
{
self::setAboStatus($shopping_order, $status, $paid);
//delete UserAbo is not active status = 1
//is_for = me
UserAbo::where('user_id', $shopping_order->auth_user_id)->where('is_for', 'me')->where('status', 1)->delete();
//is_for = ot
UserAbo::where('member_id', $shopping_order->member_id)->where('email', $shopping_order->shopping_user->billing_email)->where('is_for', 'ot')->where('status', 1)->delete();
}
public static function aboHasBaseProduct($yard_products){
foreach($yard_products as $product){
if(is_array($product->options->show_on)){
if(in_array('12', $product->options->show_on)){
public static function getAboMinDuration()
{
return \App\Models\Setting::getContentBySlug('abo-min-duration');
}
public static function canCancelAbo(UserAbo $user_abo, $view = 'user')
{
$minDuration = self::getAboMinDuration();
if ($view === 'admin') {
return true;
}
$paidOrdersCount = $user_abo->getCountPaidOrders();
return $paidOrdersCount >= (int) $minDuration;
}
public static function canEditAbo($user_abo, $view = 'user')
{
if ($view !== 'admin' && ($user_abo->user_id != \Auth::user()->id && $user_abo->member_id != \Auth::user()->id)) {
return false;
}
return true;
}
public static function aboHasBaseProduct($yard_products)
{
foreach ($yard_products as $product) {
if (is_array($product->options->show_on)) {
if (in_array('12', $product->options->show_on)) {
return true;
}
}
}
return false;
}
public static function getAboShowOn(Product $product){
public static function getAboShowOn(Product $product)
{
$show_on = $product->show_on;
if(in_array('12', $show_on)){
if (in_array('12', $show_on)) {
return 'base';
}
if(in_array('13', $show_on)){
if (in_array('13', $show_on)) {
return 'upgrade';
}
return false;
}
public static function getAboTypeBadge($abo_type){
if($abo_type === 'base'){
return '<span class="badge badge-pill badge-warning"><i class="fas fa-star"></i> '.__('abo.'.$abo_type).'</span></a>';
public static function getAboTypeBadge($abo_type)
{
if ($abo_type === 'base') {
return '<span class="badge badge-pill badge-warning"><i class="fas fa-star"></i> ' . __('abo.' . $abo_type) . '</span></a>';
}
if($abo_type === 'upgrade'){
return '<span class="badge badge-pill badge-info"><i class="far fa-star"></i> '.__('abo.'.$abo_type).'</span></a>';
if ($abo_type === 'upgrade') {
return '<span class="badge badge-pill badge-info"><i class="far fa-star"></i> ' . __('abo.' . $abo_type) . '</span></a>';
}
return '';
}
public static function setNextDate($date, $abo_interval){
public static function setNextDate($date, $abo_interval)
{
$nextDate = Carbon::parse($date)->firstOfMonth();
$nextDate->addDays($abo_interval-1);
$nextDate->addDays($abo_interval - 1);
return $nextDate->gt($date) ? $nextDate : $nextDate->addMonth(1);
}
public static function createNewAbo(ShoppingPayment $shopping_payment){
public static function getFirstAboDate($date, $abo_interval)
{
$nextDate = Carbon::parse($date)->firstOfMonth()->addMonth(1);
$nextDate->addDays($abo_interval - 1);
return $nextDate->gt($date) ? $nextDate : $nextDate->addMonth(1);
}
public static function createNewAbo(ShoppingPayment $shopping_payment)
{
//is Abo - create init Abo from PP or else
if($shopping_payment->shopping_order->is_abo && $shopping_payment->shopping_order->abo_interval > 0){
if ($shopping_payment->shopping_order->is_abo && $shopping_payment->shopping_order->abo_interval > 0) {
$payment_transaction = $shopping_payment->payment_transactions->last();
//next_date immer im nächsten Monat starten
//is auth_user_id = Berater bestellung
//is member_id = Kunden bestellung
//is for = me = mich oder ot = kunde
$user_abo = UserAbo::create([
'user_id' => $shopping_payment->shopping_order->auth_user_id,
'member_id' => $shopping_payment->shopping_order->member_id,
@ -119,10 +163,10 @@ class AboHelper
'abo_interval' => $shopping_payment->abo_interval,
'start_date' => now(),
'last_date' => now(),
'next_date' => self::setNextDate(now(), $shopping_payment->abo_interval),
'next_date' => self::getFirstAboDate(now(), $shopping_payment->abo_interval),
]);
if($user_abo){
if ($user_abo) {
self::createAboItems($user_abo, $shopping_payment);
UserAboOrder::create([
'user_abo_id' => $user_abo->id,
@ -131,11 +175,11 @@ class AboHelper
]);
}
}
}
public static function createAboItems($user_abo, ShoppingPayment $shopping_payment){
foreach($shopping_payment->shopping_order->shopping_order_items as $item){
public static function createAboItems($user_abo, ShoppingPayment $shopping_payment)
{
foreach ($shopping_payment->shopping_order->shopping_order_items as $item) {
UserAboItem::create([
'user_abo_id' => $user_abo->id,
'product_id' => $item->product_id,
@ -147,12 +191,12 @@ class AboHelper
}
public static function getTransStatusFilterText(){
$ret = [];
foreach(self::$txaction_filter_text as $key=>$val){
$ret[$key] = trans('payment.'.$val);
}
return $ret;
public static function getTransStatusFilterText()
{
$ret = [];
foreach (self::$txaction_filter_text as $key => $val) {
$ret[$key] = trans('payment.' . $val);
}
return $ret;
}
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use Yard;
@ -20,102 +21,178 @@ class AboOrderCart
private static $customer_detail;
public static function initYard($user_abo){
public static function initYard($user_abo)
{
// WICHTIG: Statische Variablen zurücksetzen, um sicherzustellen, dass keine Daten
// aus vorherigen Abos verwendet werden
self::$user_abo = $user_abo;
Yard::instance('shopping')->destroy();
self::$is_for = null;
self::$customer_detail = null;
// Yard komplett leeren - wichtig für Batch-Verarbeitung mehrerer Abos
$yard = Yard::instance('shopping');
$itemsBeforeDestroy = $yard->content()->count();
$yard->destroy();
\Log::info('AboOrderCart::initYard: Yard geleert', [
'abo_id' => $user_abo->id,
'items_vor_destroy' => $itemsBeforeDestroy
]);
$itemsAfterDestroy = $yard->content()->count();
\Log::info('AboOrderCart::initYard: Yard geleert', [
'abo_id' => $user_abo->id,
'items_after_destroy' => $itemsAfterDestroy
]);
self::$customer_detail = self::makeCustomerDetail($user_abo);
if($user_abo->is_for === 'me'){
if ($user_abo->is_for === 'me') {
self::$is_for = 'abo-me';
if($user_abo->user && $user_abo->user->account->same_as_billing){
if ($user_abo->user && $user_abo->user->account->same_as_billing) {
$country_id = $user_abo->user->account->country_id;
}else{
} else {
$country_id = $user_abo->user->account->shipping_country_id;
}
if($country_id && $shipping_country = ShippingCountry::whereCountryId($country_id)->first()){
if($shipping_country->shipping && $shipping_country->shipping->active){
if ($country_id && $shipping_country = ShippingCountry::whereCountryId($country_id)->first()) {
if ($shipping_country->shipping && $shipping_country->shipping->active) {
UserService::initUserYard($user_abo->user, $shipping_country->id, 'abo-me');
return true;
return true;
}
}
abort(403, 'Fehler: Versandland nicht gefunden');
}
if($user_abo->is_for === 'ot'){
if ($user_abo->is_for === 'ot') {
self::$is_for = 'abo-ot-customer';
UserService::initCustomerYard(self::$customer_detail, 'abo-ot-customer');
return true;
}
return false;
}
public static function makeOrderYard($user_abo)
{
{
// WICHTIG: Statische Variablen explizit setzen für dieses Abo
self::$user_abo = $user_abo;
if($user_abo->is_for === 'ot'){
if ($user_abo->is_for === 'ot') {
self::$is_for = 'abo-ot-customer';
}
if($user_abo->is_for === 'me'){
if ($user_abo->is_for === 'me') {
self::$is_for = 'abo-me';
}
foreach($user_abo->user_abo_items as $abo_item){
// WICHTIG: Yard IMMER leeren, um sicherzustellen, dass keine Produkte aus vorherigen Aufrufen vorhanden sind
// Dies ist besonders wichtig bei wiederholten Aufrufen der detail-Funktion (z.B. durch AJAX-Requests)
$yard = Yard::instance('shopping');
$itemsBefore = $yard->content()->count();
$yard->destroy();
if ($itemsBefore > 0) {
\Log::warning('AboOrderCart::makeOrderYard: Yard war nicht leer vor makeOrderYard und wurde geleert', [
'abo_id' => $user_abo->id,
'items_before' => $itemsBefore
]);
}
// Sicherstellen, dass die Items für dieses spezifische Abo geladen werden
// Verwende fresh() um sicherzustellen, dass wir die aktuellen Daten haben
$abo_items = $user_abo->user_abo_items()->get();
\Log::info('AboOrderCart::makeOrderYard: Füge Produkte zum Cart hinzu', [
'abo_id' => $user_abo->id,
'item_count' => $abo_items->count(),
'items' => $abo_items->map(function ($item) {
return [
'id' => $item->id,
'product_id' => $item->product_id,
'qty' => $item->qty,
'comp' => $item->comp
];
})->toArray()
]);
foreach ($abo_items as $abo_item) {
self::addProductToCart($abo_item);
}
Yard::instance('shopping')->reCalculateShippingPrice();
$user_abo->amount = Yard::instance('shopping')->totalWithShipping(2, '.', '')*100;
$user_abo->amount = Yard::instance('shopping')->totalWithShipping(2, '.', '') * 100;
$user_abo->save();
}
private static function addProductToCart($item){
private static function addProductToCart($item)
{
$product = Product::find($item->product_id);
$tax_free = Yard::instance('shopping')->getUserTaxFree();
$user_country = Yard::instance('shopping')->getUserCountry();
if($product){
if($item->comp){
$cartItem = Yard::instance('shopping')->add($product->id, $product->getLang('name'), 1, 0, false, false,
['image' => '', 'slug' => $product->slug, 'weight' => 0, 'points' => 0,
'comp' => $item->comp, 'product_id' => $product->id]);
if ($product) {
if ($item->comp) {
$cartItem = Yard::instance('shopping')->add(
$product->id,
$product->getLang('name'),
1,
0,
false,
false,
[
'image' => '',
'slug' => $product->slug,
'weight' => 0,
'points' => 0,
'comp' => $item->comp,
'product_id' => $product->id
]
);
Yard::setTax($cartItem->rowId, 0);
return true;
}
if(self::$is_for === 'ot-customer' || self::$is_for === 'abo-ot-customer'){
if (self::$is_for === 'ot-customer' || self::$is_for === 'abo-ot-customer') {
$cartItem = Yard::instance('shopping')
->add($product->id, $product->getLang('name'), $item->qty,
round($product->getPriceWith($tax_free, false, $user_country, false, self::$user_abo->user), 1), false, false,
['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
}else{
->add(
$product->id,
$product->getLang('name'),
$item->qty,
round($product->getPriceWith($tax_free, false, $user_country, false, self::$user_abo->user), 1),
false,
false,
['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]
);
} else {
$cartItem = Yard::instance('shopping')
->add($product->id, $product->getLang('name'), $item->qty,
$product->getPriceWith($tax_free, true, $user_country, false, self::$user_abo->user), false, false,
['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
->add(
$product->id,
$product->getLang('name'),
$item->qty,
$product->getPriceWith($tax_free, true, $user_country, false, self::$user_abo->user),
false,
false,
['image' => '', 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]
);
}
if($tax_free){
if ($tax_free) {
Yard::setTax($cartItem->rowId, 0);
}else{
} else {
Yard::setTax($cartItem->rowId, $product->getTaxWith($user_country));
}
}
}
public static function checkNumOfCompProducts($user_abo){
public static function checkNumOfCompProducts($user_abo)
{
if($user_abo->is_for === 'me'){
if ($user_abo->is_for === 'me') {
$needNumComp = Yard::instance('shopping')->getNumComp();
if($needNumComp > 0){
if ($needNumComp > 0) {
$UserAboItems = UserAboItem::where('user_abo_id', $user_abo->id)->where('comp', '>', 0)->get();
if(count($UserAboItems) === $needNumComp){
if (count($UserAboItems) === $needNumComp) {
return true;
}
//need to add
if(count($UserAboItems) < $needNumComp){
if (count($UserAboItems) < $needNumComp) {
$product = Product::whereActive(true)->where('shipping_addon', true)->whereJsonContains('show_on', '12')->orderBy('pos', 'DESC')->first();
for($i = count($UserAboItems); $i <= $needNumComp; $i++){
for ($i = count($UserAboItems); $i <= $needNumComp; $i++) {
$UserAboItem = UserAboItem::create([
'user_abo_id' => $user_abo->id,
'product_id' => $product->id,
@ -127,14 +204,14 @@ class AboOrderCart
}
}
//need to remove
if(count($UserAboItems) > $needNumComp){
foreach($UserAboItems as $UserAboItem){
if($UserAboItem->comp > $needNumComp){
if (count($UserAboItems) > $needNumComp) {
foreach ($UserAboItems as $UserAboItem) {
if ($UserAboItem->comp > $needNumComp) {
$UserAboItem->delete();
}
}
foreach (Yard::instance('shopping')->content() as $row) {
if($row->options->comp > $needNumComp) {
if ($row->options->comp > $needNumComp) {
Yard::instance('shopping')->remove($row->rowId);
}
}
@ -143,17 +220,33 @@ class AboOrderCart
}
}
public static function getCustomerDetail(){
public static function getCustomerDetail()
{
return self::$customer_detail;
}
/* Need this, can change the address */
public static function makeCustomerDetail($user_abo){
/* Need this, can change the address */
public static function makeCustomerDetail($user_abo)
{
if($user_abo->is_for === 'me'){
if ($user_abo->is_for === 'me') {
//only on Abo!
$user = $user_abo->user;
$shopping_user = new ShoppingUser();
// WICHTIG: Wenn bereits ein shopping_user existiert, diesen replizieren um alle Felder zu behalten
// Ansonsten neues Objekt erstellen
if ($user_abo->shopping_user) {
$shopping_user = $user_abo->shopping_user->replicate();
\Log::info('AboOrderCart::makeCustomerDetail: ShoppingUser repliziert für Abo ID: ' . $user_abo->id, [
'abo_id' => $user_abo->id,
'original_shopping_user_id' => $user_abo->shopping_user->id
]);
} else {
$shopping_user = new ShoppingUser();
\Log::info('AboOrderCart::makeCustomerDetail: Neuer ShoppingUser erstellt für Abo ID: ' . $user_abo->id);
}
// Account-Daten überschreiben/aktualisieren
$shopping_user->billing_salutation = $user->account->salutation;
$shopping_user->billing_company = $user->account->company;
$shopping_user->billing_firstname = $user->account->first_name;
@ -164,8 +257,13 @@ class AboOrderCart
$shopping_user->billing_city = $user->account->city;
$shopping_user->billing_country_id = $user->account->country_id;
$shopping_user->billing_phone = $user->account->phone;
$shopping_user->billing_email = $user->email ?? null;
if($user->account->same_as_billing){
// Auth User ID setzen falls noch nicht gesetzt
if (!$shopping_user->auth_user_id) {
$shopping_user->auth_user_id = $user->id;
}
if ($user->account->same_as_billing) {
$shopping_user->shipping_salutation = $user->account->salutation;
$shopping_user->shipping_company = $user->account->company;
$shopping_user->shipping_firstname = $user->account->first_name;
@ -176,7 +274,9 @@ class AboOrderCart
$shopping_user->shipping_city = $user->account->city;
$shopping_user->shipping_country_id = $user->account->country_id;
$shopping_user->shipping_phone = $user->account->phone;
}else{
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
$shopping_user->same_as_billing = 1;
} else {
$shopping_user->shipping_salutation = $user->account->shipping_salutation;
$shopping_user->shipping_company = $user->account->shipping_company;
$shopping_user->shipping_firstname = $user->account->shipping_firstname;
@ -187,15 +287,15 @@ class AboOrderCart
$shopping_user->shipping_city = $user->account->shipping_city;
$shopping_user->shipping_country_id = $user->account->shipping_country_id;
$shopping_user->shipping_phone = $user->account->shipping_phone;
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
$shopping_user->same_as_billing = 0;
}
}
if($user_abo->is_for === 'ot'){
if ($user_abo->is_for === 'ot') {
//look for the primary user of this abo
$shopping_user = $user_abo->shopping_user->replicate();
}
return $shopping_user;
}
}
}

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;
}
}

View file

@ -0,0 +1,136 @@
# Ergänzung: Block-Status Erkennung
Das Problem ist aktuell, dass wir zwar den `growth_bonus` aus dem Qualifikations-Level auslesen, aber nicht explizit wissen, ob dieser User für den Upline-Partner als "Blocker" gilt oder nicht.
## Die Logik:
Ein User gilt als "Blocker" (bzw. er beansprucht Schutz), wenn:
1. Er ein Level erreicht hat (`isQualLevel()`).
2. Dieses Level einen `growth_bonus > 0` hat.
Das ist bereits in `GrowthBonusCalculator::getVolumeByProtectionLevel` und `collectLegLevels` implementiert:
```php
if ($item->isQualLevel()) {
$qual = $item->qual_user_level;
$growthBonus = is_array($qual) ? ($qual['growth_bonus'] ?? 0) : ($qual->growth_bonus ?? 0);
if ($growthBonus > 0) {
$myProtectionPercent = (float) $growthBonus;
}
}
```
## Was fehlt / optimiert werden muss:
Wenn du sagst "User die einen Level haben wo tiefebonus und der level auch erreicht ist ... wird der bonus geblockt oder geteilt", dann meinst du wahrscheinlich, dass wir sicherstellen müssen, dass `qual_user_level` korrekt gesetzt ist **UND** wir diesen Status auch in der Matrix-Ansicht korrekt als "Block" visualisieren.
Aktuell zeigt die Matrix "Block (0%)" nur an, wenn die rechnerische Differenz <= 0 ist.
Es wäre hilfreich, in der Matrix anzuzeigen, **WARUM** blockiert wird (z.B. "Blocked by Silver").
### Anpassung in `GrowthBonusCalculator::getMatrixDetails` (bzw. `collectLegLevels`)
Wir erweitern die Detail-Informationen in der Matrix um den Status des Users, der den Schutz erhöht hat.
In `collectLegLevels`:
Wir müssen erkennen, ob der aktuelle User den Schutz erhöht hat.
```php
// Protection Check
$userProtection = 0.0;
$blockerLevelName = null;
if ($item->isQualLevel()) {
$qual = $item->qual_user_level;
$growth = is_array($qual) ? ($qual['growth_bonus'] ?? 0) : ($qual->growth_bonus ?? 0);
if ($growth > 0) {
$userProtection = (float) $growth;
$blockerLevelName = is_array($qual) ? ($qual['name'] ?? '') : ($qual->name ?? '');
}
}
// Hat dieser User den Schutz erhöht?
$isBlocker = $userProtection > $incomingProtection;
$effectiveProtection = max($incomingProtection, $userProtection);
```
Diese Info speichern wir in `$legData['levels'][$level]`.
## Code-Anpassung
Ich werde `GrowthBonusCalculator.php` anpassen, um diese Meta-Daten zu erfassen, damit wir sie in der View anzeigen können.
### Schritt 1: `GrowthBonusCalculator.php` - `collectLegLevels` erweitern
```php
private function collectLegLevels(BusinessUserItemOptimized $item, int $level, float $incomingProtection, float $myPercent, array &$legData)
{
// 1. Eigenen Status ermitteln
$userProtection = 0.0;
$userLevelName = '';
if ($item->isQualLevel()) {
$qual = $item->qual_user_level;
$growth = is_array($qual) ? ($qual['growth_bonus'] ?? 0) : ($qual->growth_bonus ?? 0);
if ($growth > 0) {
$userProtection = (float) $growth;
$userLevelName = is_array($qual) ? ($qual['name'] ?? '') : ($qual->name ?? '');
}
}
// Berechnung
$volume = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($volume > 0) {
$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' => []
];
}
$legData['levels'][$level]['volume'] += $volume;
$legData['levels'][$level]['commission'] += $commission;
// Erweiterte Details speichern
$legData['levels'][$level]['details'][] = [
'u' => $item->user_id,
'name' => $item->first_name . ' ' . $item->last_name,
'level' => $userLevelName, // Welchen Status hat dieser User?
'prot_own' => $userProtection, // Welchen Schutz baut er selbst auf?
'prot_in' => $incomingProtection, // Welcher Schutz kam von oben?
'percent' => $diffPercent
];
$legData['total_volume'] += $volume;
$legData['total_commission'] += $commission;
}
// Protection für nächste Ebene
$nextProtection = max($incomingProtection, $userProtection);
// Rekursion ...
}
```
### Schritt 2: View Update
In der View können wir nun anzeigen, wenn ein User auf einer Ebene einen Status hat (Tooltip oder Icon).
Da wir in der Matrix pro Ebene aggregieren (falls ein Bein sich verzweigt, was hier in `collectLegLevels` durch die Rekursion über `businessUserItems` passiert - Moment, ein "Bein" ist hier linear in der Tiefe, aber in jeder Tiefe kann es Breite geben).
Warte, `collectLegLevels` geht `foreach ($item->businessUserItems as $child)`.
Das bedeutet, ein "Leg" in der Matrix ist eigentlich ein ganzer Unterbaum.
In Ebene 2 können also mehrere User sein (alle Enkel in diesem Bein).
Wenn wir in der Matrix nur EINE Zelle pro Ebene haben, müssen wir entscheiden, was wir anzeigen.
Die Summe (€/Volumen) ist korrekt.
Aber der Status ("Block") kann unterschiedlich sein (ein Enkel ist Silber, der andere nicht).
Lösung: Wir markieren die Zelle als "Teilweise Blockiert" oder zeigen Details im Tooltip an.
Ich passe zuerst den `GrowthBonusCalculator` an, um diese Daten bereitzustellen.

View file

@ -0,0 +1,193 @@
# Konzept: Erweiterte Detailansicht für Growth Bonus (Matrix-View)
Die Anforderung ist, eine **Matrix-Ansicht** zu erstellen, bei der die Ebenen (Level 1, 2, 3...) als Spalten und die einzelnen Linien (Legs/Beine) als Zeilen dargestellt werden. Dies soll auch dann geschehen, wenn der Bonus gekappt ist, um volle Transparenz zu gewährleisten.
## 1. Datenstruktur-Erweiterung (`GrowthBonusCalculator`)
Die bisherige Aggregation (`getVolumeByProtectionLevel`) gruppiert Volumen nach "Schutz-Level". Das ist gut für die Berechnung, aber für die Visualisierung "Ebene für Ebene" brauchen wir die Rohdaten pro Ebene.
Wir benötigen eine neue Methode `getMatrixDetails`, die rekursiv die Struktur traversiert und für jedes Bein eine flache Liste von Ebenen-Volumen zurückgibt, angereichert mit Status-Informationen.
### Struktur des Ergebnis-Arrays:
```php
[
// Ein Eintrag pro Firstline (Bein)
[
'user' => [ 'id' => 123, 'name' => 'Max Mustermann', 'level' => 'Gold' ],
'levels' => [
1 => [ // Ebene 1 (relativ zu mir, also der Firstline-User selbst)
'volume' => 500,
'user_level' => 'Gold',
'protection_percent' => 2.0, // Was der User für sich beansprucht
'my_percent' => 2.5, // Mein Anspruch
'diff_percent' => 0.5, // Resultierende Provision
'commission' => 2.50,
'is_blocked' => false
],
2 => [ // Ebene 2 (User unter Max)
'volume' => 1000,
'user_level' => 'Silver',
'protection_percent' => 1.5,
'my_percent' => 2.5,
'diff_percent' => 1.0,
'commission' => 10.00,
'is_blocked' => false
],
// ... weitere Ebenen bis max Tiefe oder Abbruchbedingung
],
'totals' => [ 'volume' => 1500, 'commission' => 12.50 ]
],
// ... weitere Beine
]
```
## 2. Implementierungsschritte
1. **`GrowthBonusCalculator.php`**: Methode `getMatrixDetails` hinzufügen.
* Muss rekursiv durch die `businessUserItems` laufen.
* Muss tracken, welcher "Schutz-Level" von oben kommt (rekursiv weitergegeben).
* Muss aber `protection_percent` lokal pro User neu bewerten (max(incoming, own)).
2. **`BusinessUserItemOptimized.php`**: Aufruf in `getGrowthBonusBreakdown` anpassen oder neue Methode `getGrowthBonusMatrix` hinzufügen.
3. **View `_user_detail_in.blade.php`**: Umbau der Tabelle zu einer Matrix.
### Herausforderung: Tiefe und Breite
Eine komplette Matrix kann sehr breit und lang werden.
* **Begrenzung:** Wir sollten die Tiefe standardmäßig begrenzen (z.B. 10-20 Ebenen) oder nur relevante Ebenen (wo Volumen > 0) anzeigen.
* **Breite:** In der Tabelle werden die Spalten "Ebene 1", "Ebene 2", ... sein.
## 3. Code-Anpassung `GrowthBonusCalculator.php`
```php
/**
* 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;
}
private function collectLegLevels(BusinessUserItemOptimized $item, int $level, float $incomingProtection, float $myPercent, array &$legData)
{
// 1. Eigenen Status ermitteln (Schutz für Downline)
$myProtection = 0.0;
if ($item->isQualLevel()) {
$qual = $item->qual_user_level;
$growth = is_array($qual) ? ($qual['growth_bonus'] ?? 0) : ($qual->growth_bonus ?? 0);
if ($growth > 0) {
$myProtection = (float) $growth;
}
}
// Der effektive Schutz, der AUF diesen User wirkt (von oben kommend + sein eigener Anspruch)
// WICHTIG: Für die Provision auf DIESEN User zählt der $incomingProtection (Schutz von oben).
// Für die Weitergabe nach unten zählt max($incoming, $myProtection).
// Berechnung für diesen User (Ebene)
$volume = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($volume > 0) {
// Differenz: Mein Anspruch - Schutz von oben
$diffPercent = max(0, $myPercent - $incomingProtection);
$commission = round($volume / 100 * $diffPercent, 2);
// Speichern in Matrix
// Wir summieren Volumen pro Ebene (falls durch parallele Zweige im Bein mehrere User auf gleicher Ebene sind - hier aber linearer Abstieg)
// Moment, businessUserItems ist ein Baum. Ein Bein kann breit werden.
// Wir müssen pro Ebene summieren.
if (!isset($legData['levels'][$level])) {
$legData['levels'][$level] = [
'volume' => 0.0,
'commission' => 0.0,
'details' => [] // Optional für Hover
];
}
$legData['levels'][$level]['volume'] += $volume;
$legData['levels'][$level]['commission'] += $commission;
// Metadaten für Anzeige (nur beim ersten Eintrag pro Ebene oder aggregiert?)
// Bei Matrix-View (Spalten=Ebenen) summieren wir alles auf Ebene X in diesem Bein.
// Das "Problem": In Ebene X können User mit unterschiedlichem Schutz-Status sein.
// Daher ist eine einfache Summe evtl. irreführend bei der %-Anzeige.
// Alternative: Wir zeigen pro Ebene den "dominanten" Status oder listen auf.
// Für die Tabelle ist eine Zelle pro Ebene vorgesehen.
// Wir speichern Detail-Infos für Tooltip.
$legData['levels'][$level]['details'][] = [
'u' => $item->user_id,
'v' => $volume,
'p' => $incomingProtection, // Protected by
'd' => $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, $myProtection);
// Rekursion
// Max Tiefe z.B. 20
if ($level < 20 && !empty($item->businessUserItems)) {
foreach ($item->businessUserItems as $child) {
$this->collectLegLevels($child, $level + 1, $nextProtection, $myPercent, $legData);
}
}
}
```
## 4. Design der Tabelle (Blade)
Spalten: Leg (Partner) | Ebene 1 | Ebene 2 | Ebene 3 | ... | Ebene 10 | Total
Zeilen: Partner A | ... | ... | ...
Zellen-Inhalt:
* Oben: Provision (€)
* Unten: Volumen (Pkt)
* Farbe: Grün (Volle %), Gelb (Teil %), Rot (0% / Block)
Da die Ebenen dynamisch sind, ermitteln wir `max_level` über alle Legs.

View file

@ -0,0 +1,399 @@
# Funktionsweise: Tiefenbonus (Growth Bonus)
## ⚠️ WICHTIG: Bug-Fix November 2025
### Das Problem (vor November 2025)
Die Payline-Prozentsätze (`pr_line_1` bis `pr_line_6`) in der Datenbank enthielten **bereits den Growth Bonus**.
**Beispiel Gold Member (falsche Berechnung):**
| Ebene | Wert in DB (`pr_line_X`) | Was ausgezahlt wurde | Was korrekt gewesen wäre |
| ------- | ------------------------ | -------------------- | ------------------------------ |
| Ebene 1 | 9% | 9% | 7% Payline + 2% Growth = 9% |
| Ebene 2 | 9% | 9% | 7% Payline + 2% Growth = 9% |
| Ebene 3 | 9% | 9% | 7% Payline + 2% Growth = 9% |
| Ebene 4 | 6% | 6% | 4% Payline + 2% Growth = 6% |
| Ebene 5 | 4% | 4% | 2% Payline + 2% Growth = 4% |
| Ebene 6 | 4% | 4% | 2% Payline + 2% Growth = 4% |
| Ebene 7 | - | 2% (Growth nochmal!) | 2% Growth (nur mit Differenz!) |
**Problem:** Der Growth Bonus wurde **doppelt gezählt**:
1. Einmal IN den Payline-Prozentsätzen (pr_line_1 = 9% statt 7%)
2. Nochmal SEPARAT auf Ebenen ab 7+ (Legacy-Berechnung)
### Die Lösung (ab November 2025)
1. **Payline-Prozentsätze korrigiert:** `pr_line_X` enthält NUR den Payline-Anteil
2. **Growth Bonus separat:** Wird mit Differenz-Logik berechnet
3. **Einmal pro Bein:** Growth Bonus wird nur EINMAL pro Firstline-Zweig ausgezahlt
**Beispiel Gold Member (korrekte Berechnung):**
| Ebene | Payline (`pr_line_X`) | Growth Bonus (separat) | Gesamt |
| -------- | --------------------- | ---------------------- | ------ |
| Ebene 1 | 7% | +2% (Differenz-Logik) | 9% |
| Ebene 2 | 7% | +2% | 9% |
| Ebene 3 | 7% | +2% | 9% |
| Ebene 4 | 4% | +2% | 6% |
| Ebene 5 | 2% | +2% | 4% |
| Ebene 6 | 2% | +2% | 4% |
| Ebene 7+ | - | +2% (Differenz-Logik) | 2% |
**Wichtig:** Der Growth Bonus wird NUR ausgezahlt, wenn kein gleichrangiger oder höherer Partner in der Downline ist (Differenz-Berechnung)!
---
## Differenz-Logik (ab November 2025)
Der Tiefenbonus ist ein **Differenz-Bonus**, der **sofort ab der 1. Ebene** beginnt.
Es gilt das Prinzip: **"Jeder Partner schützt sein eigenes Team-Volumen."**
### 1. Die Grundregel
- **Start:** Der Bonus berechnet sich auf Points ab der **1. Ebene** (direkte Downline).
- **Anspruch:** Ein Partner erhält seinen Status-Prozentsatz auf alle Points in seiner Linie, **bis** er auf einen Partner trifft, der selbst einen Status-Anspruch hat.
- **Blockade:** Sobald ein Partner in der Downline einen Anspruch hat, zieht er diesen von der Upline ab (Differenz-Rechnung).
- **⚠️ WICHTIG - Erreichtes Qualifikations-Level:** Die Blockade erfolgt NUR basierend auf dem **in dem Monat tatsächlich erreichten Level** (`qual_user_level`), NICHT auf dem aktuellen Karriere-Level des Partners!
### 1.1 Erreichte Qualifikation vs. Aktuelles Level
Ein Partner kann ein bestimmtes Karriere-Level (z.B. Gold) haben, aber in einem Monat die Qualifikationsvoraussetzungen nicht erfüllen. In diesem Fall:
| Situation | Aktuelles Level | Erreicht in Monat | Blockiert mit |
| --------- | --------------- | ----------------- | ------------- |
| Fall A | Gold (2%) | Gold qualifiziert | 2% ✅ |
| Fall B | Gold (2%) | Team Leader (0%) | 0% ❌ |
| Fall C | Team Leader | Silber (1.5%) | 1.5% ✅ |
**Technische Umsetzung:**
- Die Methode `getQualifiedGrowthBonus()` in `BusinessUserItemOptimized` gibt den Growth Bonus basierend auf dem **erreichten Qualifikations-Level** (`qual_user_level`) zurück.
- Die alte Methode `getActiveGrowthBonus()` gibt den Growth Bonus basierend auf dem **aktuellen Karriere-Level** zurück (NUR für Legacy-Berechnungen!).
- Der `GrowthBonusCalculator` verwendet ab November 2025 ausschließlich `getQualifiedGrowthBonus()`.
---
### 2. Die Differenz (Der Normalfall)
Points entstehen irgendwo im Team von **Partner B** (egal ob in B's Ebene 1 oder B's Ebene 50).
**Die Verteilung:**
1. **Sicht Partner B (Silber):**
- Er hat Anspruch auf **1,5 %** auf sein gesamtes Team.
- Da unter ihm (Partner C) niemand einen Status hat, der etwas wegnehmen könnte, erhält B die vollen **1,5 %**.
- Damit sind 1,5 % des "Kuchens" verteilt.
2. **Sicht Partner A (Diamant):**
- Du hast Anspruch auf **2,5 %**.
- Du schaust auf die Linie von Partner B.
- Partner B hat den Status Silber und beansprucht damit **1,5 %** für sich und sein ganzes Team.
- **Deine Rechnung:** 2,5 % (Dein Anspruch) - 1,5 % (Anspruch B) = **1,0 %**.
- **Ergebnis:** Du erhältst auf das gesamte Volumen unter Partner B exakt **1,0 %**.
---
### 2. Das "GAP" (Die direkte Ebene)
Da der Bonus ab Ebene 1 beginnt, entsteht das GAP (die Auszahlung trotz gleichem Rang) immer am **Eigenumsatz des Partners**:
- **Partner A** (Diamant, 2,5 %) ist Sponsor von **Partner B** (Diamant, 2,5 %).
- **Punkte von B (Eigenbestellung/Kunden):**
- Partner B erhält darauf _keinen_ Tiefenbonus (man kriegt keinen Tiefenbonus auf sich selbst).
- Partner B zieht also **0 %** vom Topf ab.
- **Partner A erhält die vollen 2,5 % auf die Punkte von B.**
- **Punkte UNTER B (Team von B):**
- Partner B greift hier zu (Start ab Ebene 1) und nimmt sich **2,5 %**.
- Partner A rechnet: 2,5 % - 2,5 % = **0 %**.
- **Partner A ist hier blockiert.**
> Fazit: Bei gleichem Rang verdient man nur an den direkten Points des Partners (GAP), aber nicht mehr an dessen Team.
---
### 3. Das Szenario (A -> B -> F)
Wir schauen uns deine Struktur mit 3 Diamanten in einer Linie an. Alle haben Anspruch auf **2,5 %**.
- **Partner A** (Ebene 1)
- **Partner B** (Ebene 2, direkt unter A)
- ... dazwischen Berater ohne Status ...
- **Partner F** (Ebene 6, unter B)
- ... Punkte entstehen unter F ...
### Bereich 1: Punkte von Partner B
- Das ist für **A** die Ebene 1.
- B blockiert nicht (da Eigenumsatz).
- **Ergebnis:** **A erhält 2,5 %**.
### Bereich 2: Punkte ZWISCHEN B und F (Ebene 3 bis 6)
- Hier entstehen Punkte im Team von B.
- **Sicht B:** Er ist qualifiziert (Start ab Ebene 1). Er nimmt sich **2,5 %**.
- **Sicht A:** Er hat Anspruch auf 2,5 %. B hat aber schon 2,5 % genommen. Differenz = 0 %.
- **Ergebnis:** **B erhält 2,5 %**. A geht leer aus.
### Bereich 3: Punkte von Partner F
- Das ist für **B** eine Ebene in seiner Downline.
- F blockiert hier noch nicht (Eigenumsatz).
- **Ergebnis:** **B erhält 2,5 %** auf die Punkte von F.
### Bereich 4: Punkte UNTER F (ab Ebene 7)
- Hier entstehen Punkte im Team von F.
- **Sicht F:** Er ist qualifiziert (Start ab Ebene 1). Er nimmt sich **2,5 %**.
- **Sicht B:** Anspruch 2,5 %. F hat schon 2,5 % genommen. Differenz = 0 %.
- **Sicht A:** Anspruch 2,5 %. B (und F) haben alles genommen. Differenz = 0 %.
- **Ergebnis:** **F erhält 2,5 %**. B und A gehen leer aus.
---
### 4. Zusammenfassung für die IT-Logik
1. **Trigger:** Ein Umsatz (Points) entsteht bei User X.
2. **Schleife:** Gehe die Upline hoch (Sponsor -> Sponsor...).
3. **Prüfung:**
- Hat der Upline-Partner einen Status? (z.B. Diamant).
- (Keine Prüfung auf Ebene mehr nötig, da Start immer ab Ebene 1).
4. **Rechnung:**
- Auszahlung = Mein %-Satz - Bereits verteilter %-Satz.
- Wenn Auszahlung > 0: Speichern.
- Setze `Bereits verteilter %-Satz` auf den neuen Wert (also `Mein %-Satz`).
---
## Code-Implementierung
Diese Implementierung nutzt eine **rekursive Aggregation von Volumen nach "Schutz-Level"**.
Anstatt für jede Transaktion die Upline hochzulaufen ("Push"), holt sich der User die aggregierten Volumina seiner Downline gruppiert nach dem bereits beanspruchten Prozentsatz ("Pull").
### A. Neue Methode `getVolumeByProtectionLevel()`
Diese Methode liefert ein Array zurück, das das Volumen nach "bereits verteiltem Prozentsatz" gruppiert.
Format: `['0.0' => 1000, '1.5' => 5000, ...]`
```php
/**
* Liefert das Volumen der Downline gruppiert nach dem "bereits verteilten Prozentsatz" (Protection Level).
* Rekursive Funktion, die die "Differenz-Logik" vorbereitet.
*
* @return array<string, float> Key = Protected Percent, Value = Volume Points
*/
public function getVolumeByProtectionLevel(): array
{
$volumes = [];
// 1. Eigenes Volumen (Unprotected / GAP)
// Man selbst schützt seinen eigenen Umsatz NICHT vor der Upline.
// Daher Start mit Protection Level 0.0 (oder dem was von unten kommt, aber hier ist es ja Eigenumsatz)
// WICHTIG: Wir nutzen das Feld, das auch TreeCalcBot für die Punkte nutzt
// sales_volume_points_TP_sum scheint in der DB/Model Logik für das relevante Volumen zu stehen
$ownVolume = (float) ($this->b_user->sales_volume_points_TP_sum ?? 0);
if ($ownVolume > 0) {
$key = '0.0';
if (!isset($volumes[$key])) $volumes[$key] = 0.0;
$volumes[$key] += $ownVolume;
}
// 2. Mein Schutz-Level ermitteln
// Das ist der Prozentsatz, den ICH auf mein Team beanspruche.
// Alles Volumen, das durch MICH hindurch zur Upline fließt, hat mindestens diesen Schutz-Level.
$myProtectionPercent = 0.0;
if ($this->isQualLevel()) {
$qual = $this->b_user->qual_user_level;
if (!empty($qual['growth_bonus'])) {
$myProtectionPercent = (float) $qual['growth_bonus'];
}
}
// 3. Kinder verarbeiten
if (!empty($this->businessUserItems)) {
foreach ($this->businessUserItems as $childItem) {
// Rekursion: Hol dir die Volumen-Töpfe aus der Downline
// Hinweis: Hier muss sichergestellt sein, dass die Kinder geladen sind.
// initBusinesslUserDetail lädt normalerweise die Struktur.
// Falls Kinder nicht geladen sind, müssten sie hier theoretisch geladen werden.
// Wir gehen davon aus, dass die Struktur bereits rekursiv via readParentsBusinessUsers geladen wurde.
$childVolumes = $childItem->getVolumeByProtectionLevel();
// 4. Schutz-Level anwenden (Aggregation)
foreach ($childVolumes as $protectedPercentStr => $vol) {
$incomingProtection = (float) $protectedPercentStr;
// Das Volumen ist bereits mit $incomingProtection geschützt.
// Da es nun durch MICH fließt, erhöht sich der Schutz auf MEINEN Level (falls meiner höher ist).
$effectiveProtection = max($incomingProtection, $myProtectionPercent);
$newKey = (string) $effectiveProtection;
if (!isset($volumes[$newKey])) $volumes[$newKey] = 0.0;
$volumes[$newKey] += $vol;
}
}
}
return $volumes;
}
```
### B. Neue Methode `calculateGrowthBonusRecursive()`
Diese Methode ersetzt die bisherige Berechnung und nutzt die oben definierte Aggregation.
```php
/**
* Berechnet den Growth Bonus (Tiefenbonus) basierend auf der Differenz-Logik.
*/
private function calculateGrowthBonusRecursive($qualUserLevel): float
{
if (empty($qualUserLevel->growth_bonus) || $qualUserLevel->growth_bonus <= 0) {
return 0.0;
}
$myGrowthPercent = (float) $qualUserLevel->growth_bonus;
$totalGrowthBonus = 0.0;
// Wir iterieren über alle direkten Beine (Firstlines)
foreach ($this->businessUserItems as $childItem) {
// Volumen-Verteilung aus diesem Bein abrufen
// Das Kind liefert uns: "Hier sind 1000 Punkte geschützt mit 0%, 5000 Punkte geschützt mit 1.5%"
$volumeDistribution = $childItem->getVolumeByProtectionLevel();
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;
// Optional Logging
// \Log::debug("Growth Bonus: User {$this->b_user->user_id} earns {$mySharePercent}% on {$volume} pts (Protected: {$alreadyDistributedPercent}%) from leg {$childItem->b_user->user_id}");
}
}
}
return $totalGrowthBonus;
}
```
### C. Integration in `calculateCommissions`
```php
private function calculateCommissions($qualUserLevel): void
{
$commission_pp_total = 0;
// 1. Normale Unilevel Provision (Payline) - NUR pr_line_X Werte
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];
$points = is_array($object) ? ((float)($object['points'] ?? 0)) : ((float)($object->points ?? 0));
$commission = round($points / 100 * $margin, 2);
$commission_pp_total += $commission;
// Rückschreiben
if (is_array($object)) {
$object['margin'] = $margin;
$object['commission'] = $commission;
$object['payline'] = true;
} else {
$object->margin = $margin;
$object->commission = $commission;
$object->payline = true;
}
$this->b_user->business_lines[$i] = $object;
}
}
// 2. Growth Bonus - Unterscheidung Legacy vs. Neu
$commission_growth_total = 0;
if (!empty($qualUserLevel->growth_bonus)) {
// Stichtag: 01.11.2025
$isLegacy = ($this->date->year < 2025) ||
($this->date->year == 2025 && $this->date->month < 11);
if ($isLegacy) {
// ALT: Pauschal ab Ebene paylines+1 (FALSCH - doppelte Auszahlung!)
$commission_growth_total = $this->calculateLegacyGrowthBonus($qualUserLevel);
} else {
// NEU: Differenz-Logik via GrowthBonusCalculator
$commission_growth_total = $this->calculateGrowthBonusRecursive($qualUserLevel);
}
}
$this->b_user->commission_pp_total = $commission_pp_total;
$this->b_user->commission_growth_total = $commission_growth_total;
}
```
---
## Legacy-Berechnung (vor November 2025) - DEPRECATED
**⚠️ Diese Logik war FALSCH und führte zu doppelter Auszahlung!**
```php
/**
* ALT: Pauschal Growth Bonus ab Ebene paylines+1
* PROBLEM: Growth Bonus war bereits in pr_line_X enthalten!
*/
private function calculateLegacyGrowthBonus($qualUserLevel): float
{
$commission_growth_total = 0;
// Start ab Ebene paylines+1 (z.B. 7 bei Gold)
$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);
// Auf JEDE Ebene ab payline wird der volle Growth Bonus gezahlt
// OHNE Differenz-Prüfung = FALSCH!
for ($i = $payline; $i <= $maxlines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$points = $this->b_user->business_lines[$i]['points'] ?? 0;
$commission = round($points / 100 * $growth_bonus, 2);
$commission_growth_total += $commission;
}
}
return $commission_growth_total;
}
```
**Warum war das falsch?**
1. `pr_line_1` bei Gold = 9% (enthielt bereits 2% Growth Bonus)
2. Growth Bonus wurde ab Ebene 7 NOCHMAL mit 2% berechnet
3. = **Doppelte Auszahlung** auf tieferen Ebenen
---
## Neue Berechnung (ab November 2025) - KORREKT
Der `GrowthBonusCalculator` verwendet die Differenz-Logik:
1. **Aggregation:** Sammelt Volumen gruppiert nach "Schutz-Level"
2. **Differenz:** Berechnet nur die Differenz (mein Anspruch - bereits verteilt)
3. **Einmal pro Bein:** Growth Bonus wird nur einmal pro Firstline-Zweig ausgezahlt
Siehe `GrowthBonusCalculator.php` für die Implementation.

View 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;
}
}
}

View file

@ -0,0 +1,448 @@
# BusinessPlan System - Gesamtübersicht
## 📋 Inhaltsverzeichnis
1. [System-Architektur](#system-architektur)
2. [Datei-Übersicht](#datei-übersicht)
3. [Datenfluss](#datenfluss)
4. [Punktetypen & Begriffe](#punktetypen--begriffe)
5. [Level-System](#level-system)
6. [Provisionsberechnung](#provisionsberechnung)
7. [Cron-Job (Monatsabschluss)](#cron-job-monatsabschluss)
8. [Dashboard-Integration](#dashboard-integration)
---
## System-Architektur
```
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND / VIEWS │
├─────────────────────────────────────────────────────────────────────┤
│ dashboard/_statistics.blade.php │ dashboard/_points.blade.php │
│ user/team/*.blade.php │ admin/business/*.blade.php │
└───────────────────┬─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ CONTROLLER │
├─────────────────────────────────────────────────────────────────────┤
│ HomeController │ TeamController │ AdminController │
└───────────────────────┼─────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ BUSINESS PLAN SERVICES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ TreeCalcBotOptimized.php │ │
│ │ - Hauptklasse für MLM-Strukturberechnungen │ │
│ │ - Initialisiert Business-User Strukturen │ │
│ │ - Berechnet Punkte über alle Ebenen │ │
│ │ - Delegiert an Repository, Renderer, Calculator │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ Business │ │ BusinessUserItem │ │ GrowthBonus │ │
│ │ User │ │ Optimized.php │ │ Calculator.php │ │
│ │ Repository │ │ │ │ │ │
│ │ .php │ │ - User-Datenbehälter │ │ - Tiefenbonus │ │
│ │ │ │ - Qualifikations- │ │ - Differenz- │ │
│ │ - DB Queries │ │ berechnung │ │ Logik │ │
│ │ - Caching │ │ - Provisions- │ │ │ │
│ │ - Relations │ │ berechnung │ │ │ │
│ └──────────────┘ └──────────────────────┘ └──────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SalesPointsVolume.php │ │
│ │ - Punkteerfassung bei Bestellungen │ │
│ │ - KP/TP Punkte-Unterscheidung │ │
│ │ - Neuberechnung bei Änderungen │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ MODELS │
├─────────────────────────────────────────────────────────────────────┤
│ User │ UserBusiness │ UserSalesVolume │
│ UserLevel │ UserBusinessStruct │ UserAbo │
│ ShoppingOrder │ ShoppingUser │ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Datei-Übersicht
| Datei | Zweck | Abhängigkeiten |
| ------------------------------- | ---------------------------------------- | ------------------------------ |
| `TreeCalcBotOptimized.php` | Hauptklasse für MLM-Strukturberechnungen | Repository, Renderer, Logger |
| `BusinessUserItemOptimized.php` | User-Datenbehälter mit Berechnungslogik | GrowthBonusCalculator |
| `BusinessUserRepository.php` | Optimierte DB-Abfragen mit Caching | User, UserBusiness Models |
| `GrowthBonusCalculator.php` | Tiefenbonus-Berechnung (Differenz-Logik) | BusinessUserItemOptimized |
| `SalesPointsVolume.php` | Punkteerfassung bei Bestellungen | UserSalesVolume, ShoppingOrder |
| `TreeHtmlRenderer.php` | HTML-Ausgabe für Struktur-Ansichten | - |
| `TreeHelperOptimized.php` | Hilfsfunktionen für Tree-Operationen | - |
### Dokumentation
| Datei | Inhalt |
| ------------------------- | ---------------------------------------- |
| `TreeCalcBotOptimized.md` | Technische Dokumentation der Hauptklasse |
| `Growth-Bonus.md` | Erklärung der Tiefenbonus-Logik |
| `Growth-Bonus-Matrix.md` | Matrix-Darstellung des Tiefenbonus |
---
## Datenfluss
### 1. Punkteerfassung (bei Bestellung)
```
┌──────────────────┐ ┌────────────────────┐ ┌──────────────────┐
│ ShoppingOrder │────▶│ SalesPointsVolume │────▶│ UserSalesVolume │
│ wird erstellt │ │ ::addSalesPoints │ │ wird erstellt │
└──────────────────┘ │ VolumeUser() │ └──────────────────┘
└────────────────────┘ │
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────┐
│ reCalculateSales │────▶│ month_KP_points │
│ PointsVolume() │ │ month_TP_points │
└────────────────────┘ │ month_shop_points│
└──────────────────┘
```
### 2. Strukturberechnung (Live oder Cron)
```
┌───────────────────────────────────────────────────────────────────────┐
│ TreeCalcBotOptimized │
│ │
│ 1. initStructureAdmin() oder initStructureUser() │
│ │ │
│ ├─▶ Prüfe gespeicherte Struktur (UserBusinessStructure) │
│ │ └─▶ Falls vorhanden und !forceLiveCalculation → laden │
│ │ │
│ └─▶ buildFreshStructure() │
│ │ │
│ ├─▶ loadRootUsers() - Top-Sponsoren ohne Parent │
│ │ │
│ ├─▶ loadParentsUsers() - Rekursive Downline │
│ │ └─▶ readParentsBusinessUsers() für jeden User │
│ │ │
│ ├─▶ calculateUserPointsOptimized() │
│ │ └─▶ Punkte pro Ebene (business_lines[1..n]) │
│ │ │
│ └─▶ calcQualPP() für jeden User │
│ ├─▶ calcuQualLevel() - Erreichte Stufe │
│ ├─▶ setNextUserLevel() - Nächste Stufe │
│ └─▶ calculateCommissions() - Provisionen │
│ ├─▶ Payline-Provisionen (Ebene 1-6) │
│ └─▶ Growth Bonus (ab Ebene 7+) │
│ └─▶ GrowthBonusCalculator │
└───────────────────────────────────────────────────────────────────────┘
```
### 3. Monatlicher Cron-Job
```
┌───────────────────────────────────────────────────────────────────────┐
│ BusinessUsersStoreOptimized (Cron) │
│ │
│ Ausführung: Einmal pro Monat (nach Monatsende) │
│ │
│ 1. storeUserBusinessStructure() │
│ └─▶ Erstellt UserBusinessStructure mit allen User-IDs │
│ │
│ 2. storeBusinessUsersDetail() │
│ └─▶ Für jeden User: initBusinesslUserDetail() mit forceLive=true │
│ └─▶ Speichert UserBusiness Datensatz │
│ │
│ 3. storeBusinessCompleted() │
│ └─▶ Markiert Struktur als "completed" │
└───────────────────────────────────────────────────────────────────────┘
```
---
## Punktetypen & Begriffe
### Punktearten
| Kürzel | Name | Beschreibung |
| ------ | -------------- | --------------------------------------------------- |
| **KP** | Kunden-Punkte | Eigene Bestellungen + Kundenbestellungen (Shop) |
| **TP** | Team-Punkte | KP + Punkte aus der Downline (Payline) |
| **PP** | Payline-Punkte | Summe der TP aus allen Ebenen bis zur Payline-Tiefe |
### Felder in UserSalesVolume
| Feld | Beschreibung |
| ------------------- | --------------------------------------------------------------------------- |
| `points` | Punkte dieser einzelnen Transaktion |
| `month_KP_points` | Kumulierte KP-Punkte des Monats |
| `month_TP_points` | Kumulierte TP-Punkte des Monats |
| `month_shop_points` | Kumulierte Shop-Punkte des Monats |
| `status` | 1=Berater-Bestellung, 2=Shop, 3=Shop-Pending, 4=Gutschrift, 5=Registrierung |
| `status_points` | 1=KP+TP, 2=nur KP |
### Felder in UserBusiness
| Feld | Beschreibung |
| ---------------------------- | ---------------------------------------------------------- |
| `sales_volume_KP_points` | KP-Punkte |
| `sales_volume_TP_points` | TP-Punkte |
| `sales_volume_points_shop` | Shop-Punkte |
| `sales_volume_points_KP_sum` | KP + Shop |
| `sales_volume_points_TP_sum` | TP + Shop |
| `payline_points` | Summe der Ebenen 1 bis Payline-Tiefe |
| `payline_points_qual_kp` | payline_points + Rest-KP |
| `business_lines` | JSON: Punkte pro Ebene {1: {points: X}, 2: {points: Y}...} |
| `qual_user_level` | Erreichtes Qualifikations-Level (Array) |
| `qual_user_level_next` | Nächste Provisions-Stufe |
| `next_qual_user_level` | Nächstes erreichbares Level |
| `commission_pp_total` | Payline-Provision |
| `commission_shop_sales` | Shop-Provision |
| `commission_growth_total` | Growth-Bonus (Tiefenbonus) |
---
## Level-System
### Qualifikationsbedingungen
| Level | Name | Min. KP (qual_kp) | Min. PP (qual_pp) | Paylines | Growth Bonus |
| ----- | -------------------- | ----------------- | ----------------- | -------- | ------------ |
| 1 | Junior Berater | 150 | 0 | 3 | - |
| 2 | Aktiv Junior Berater | 250 | 500 | 3 | - |
| 3 | Berater | 350 | 1.000 | 4 | - |
| 4 | Aktiv Berater | 450 | 2.500 | 5 | - |
| 5 | Vertriebspartner | 600 | 5.000 | 6 | - |
| 6 | Vertriebsleiter | 600 | 9.000 | 6 | - |
| 7 | Bronze Member | 600 (690\*) | 18.000 | 6 | 1,0% |
| 8 | Silber Member | 600 | 30.000 | 6 | 1,5% |
| 9 | Gold Member | 600 | 50.000 | 6 | 2,0% |
| 10 | Diamant Member | 600 | 100.000 | 6 | 2,5% |
| 11 | Platin Member\* | 600 | 250.000 | 7 | 3,0% |
| 12 | Platin Member\*\* | 600 | 500.000 | 7 | 3,5% |
| 13 | Platin Member\*\*\* | 600 | 1.000.000 | 8 | 4,0% |
\*690 Punkte zur Auszahlung (Schecksicherung)
### Payline-Prozentsätze (pr_line_X) - OHNE Growth Bonus
**⚠️ Diese Werte sind NUR der Payline-Anteil. Growth Bonus wird separat berechnet!**
| Level | E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8 | Growth |
| -------------------- | --- | --- | --- | --- | --- | --- | --- | --- | ------ |
| Junior Berater | 6% | 3% | 1% | - | - | - | - | - | - |
| Aktiv Junior Berater | 6% | 4% | 2% | - | - | - | - | - | - |
| Berater | 6% | 5% | 3% | 2% | - | - | - | - | - |
| Aktiv Berater | 6% | 5% | 4% | 2% | 1% | - | - | - | - |
| Vertriebspartner | 6% | 6% | 5% | 3% | 2% | 1% | - | - | - |
| Vertriebsleiter | 6% | 6% | 6% | 4% | 2% | 1% | - | - | - |
| Bronze Member | 6% | 6% | 6% | 4% | 2% | 2% | - | - | 1,0% |
| Silber Member | 7% | 7% | 7% | 4% | 2% | 2% | - | - | 1,5% |
| Gold Member | 7% | 7% | 7% | 4% | 2% | 2% | - | - | 2,0% |
| Diamant Member | 7% | 7% | 7% | 4% | 2% | 2% | - | - | 2,5% |
| Platin Member\* | 7% | 7% | 7% | 4% | 3% | 2% | 1% | - | 3,0% |
| Platin Member\*\* | 7% | 7% | 7% | 4% | 3% | 2% | 1% | - | 3,5% |
| Platin Member\*\*\* | 7% | 7% | 7% | 4% | 4% | 3% | 2% | 1% | 4,0% |
**Gesamt-Provision pro Ebene = pr_line_X + Growth Bonus (mit Differenz-Logik)**
### Qualifikations-Algorithmus
```php
// In BusinessUserItemOptimized::calcuQualLevel()
1. Hole alle Levels wo qual_kp <= User's KP-Summe
2. Sortiere nach Position (höchstes zuerst)
3. Für jedes Level:
a. Berechne payline_points für dieses Level (Ebenen 1 bis paylines)
b. Berechne rest_kp = max(0, KP_sum - Level.qual_kp)
c. payline_points_qual_kp = payline_points + rest_kp
d. Wenn payline_points_qual_kp >= Level.qual_pp → QUALIFIZIERT
4. Return erstes qualifiziertes Level (höchstes)
```
---
## Provisionsberechnung
### 1. Payline-Provisionen (Unilevel)
Feste Prozentsätze auf Teamumsatz, begrenzt auf Payline-Tiefe.
**⚠️ WICHTIG:** Die `pr_line_X` Werte in der DB sind **NUR Payline** (ohne Growth Bonus)!
**Beispiel Gold Member (6 Paylines) - korrekte Werte:**
| Ebene | Payline (`pr_line_X`) | Growth Bonus | Gesamt |
| ----- | --------------------- | ------------ | ------ |
| 1 | 7% | +2% | 9% |
| 2 | 7% | +2% | 9% |
| 3 | 7% | +2% | 9% |
| 4 | 4% | +2% | 6% |
| 5 | 2% | +2% | 4% |
| 6 | 2% | +2% | 4% |
| 7+ | - | +2% | 2% |
```php
// In BusinessUserItemOptimized::calculateCommissions()
for ($i = 1; $i <= $qualUserLevel->paylines; $i++) {
$points = $business_lines[$i]['points'];
$margin = $qual_user_level['pr_line_' . $i]; // NUR Payline-Prozentsatz
$commission = round($points / 100 * $margin, 2);
}
// Growth Bonus wird SEPARAT berechnet (siehe unten)
```
### 2. Growth Bonus (Tiefenbonus / Differenz-Bonus)
**Aktivierung:** Ab Bronze Member (Level 7+)
**⚠️ Bug-Fix November 2025:**
- **VOR Nov 2025:** Growth Bonus war IN den `pr_line_X` Werten enthalten UND wurde nochmal separat berechnet = **Doppelte Auszahlung!**
- **AB Nov 2025:** Growth Bonus wird NUR separat berechnet, `pr_line_X` enthält nur Payline-Anteil
**Logik:** Differenz zwischen eigenem Anspruch und bereits verteiltem Prozentsatz
```
Beispiel:
- User A: Diamant (2,5% Anspruch)
- User B (in A's Downline): Silber (1,5% Anspruch)
Punkte UNTER B:
- B erhält: 1,5% (sein voller Anspruch)
- A erhält: 2,5% - 1,5% = 1,0% (Differenz)
Punkte von B selbst (GAP):
- B erhält: 0% (kein Bonus auf sich selbst)
- A erhält: 2,5% (voller Anspruch, da B nicht "schützt")
```
**Implementation:** Siehe `GrowthBonusCalculator.php` und `Growth-Bonus.md`
### 3. Shop-Provision
```php
$commission_shop_sales = sales_volume_total_shop / 100 * margin_shop;
```
---
## Cron-Job (Monatsabschluss)
### Datei: `app/Cron/BusinessUsersStoreOptimized.php`
### Ausführung
```bash
# Typischerweise am 1. des Folgemonats
php artisan schedule:run
# Oder manuell:
php artisan business:store-monthly {month} {year}
```
### Ablauf
1. **storeUserBusinessStructure()**
- Prüft ob bereits Struktur für Monat/Jahr existiert
- Erstellt neue `UserBusinessStructure` mit allen User-IDs
- Speichert komplette Baumstruktur als JSON
2. **storeBusinessUsersDetail()**
- Iteriert über alle User (aus users Array)
- Für jeden nicht-abgeschlossenen User:
- `TreeCalcBotOptimized::initBusinesslUserDetail(user, forceLive=true)`
- Speichert `UserBusiness` Datensatz
- Markiert User als "completed" in Struktur
3. **storeBusinessCompleted()**
- Prüft ob alle User abgeschlossen
- Setzt `completed = true` auf Struktur
### Performance
- Typische Laufzeit: 10-60 Minuten (je nach User-Anzahl)
- Memory: 512MB-1GB empfohlen
- Kann in Batches/Chunks aufgeteilt werden
---
## Dashboard-Integration
### Dashboard-Statistiken (`dashboard/_statistics.blade.php`)
| Metrik | Datenquelle | Beschreibung |
| -------------------- | --------------------------------------------------------- | -------------------- |
| Kunden-Umsatz Punkte | `UserSalesVolume.getPointsKPSum()` | KP + Shop |
| Team-Umsatz Punkte | `UserBusiness.payline_points` | Payline-Summe |
| Direkte Neupartner | `User WHERE m_sponsor = $userId AND active_date IN month` | Neue Firstlines |
| Neupartner im Team | `UserSalesVolume WHERE status = 5 (registration)` | Registrierungspunkte |
| Kundenabos | `UserAbo WHERE member_id = $userId` | Kunden-Abos |
| Teamabos | `UserAbo WHERE user_id IN firstline_ids` | Team-Abos |
### Punkte-Tabelle (`dashboard/_points.blade.php`)
Zeigt detaillierte `UserSalesVolume` Einträge für gewählten Monat/Jahr:
- Datum, Punkte, Netto-Umsatz
- Status (Berater-Bestellung, Shop, Gutschrift, Registrierung)
- Bestellungs-Link, Kundeninformationen
---
## Erweiterungen & TODO
### Geplante Änderungen
1. **Dashboard-Statistiken erweitern**
- Monats/Jahr-Filter implementiert
- Statistik-Kacheln hinzugefügt
2. **Marketingplan-Anpassungen** (in Arbeit)
- Level-Struktur überprüfen
- Provisionsberechnung validieren
- Growth-Bonus Differenz-Logik testen
### Bekannte Einschränkungen
- Growth Bonus nur ab November 2025 mit neuer Differenz-Logik
- Vor November 2025: Legacy-Berechnung (pauschal ab Ebene 7+)
- Struktur-Tiefe begrenzt auf 20-30 Ebenen (Performance)
---
## Kontakt & Wartung
**Letzte Aktualisierung:** Dezember 2025
**Version:** BusinessPlan System v2.0
### Log-Dateien
- `storage/logs/laravel.log` - Allgemeine Logs
- BusinessUserItem/TreeCalcBot Logs mit Prefix "BusinessUserItem:" / "TreeCalcBot:"
### Cache löschen
```php
// In Repository
$repository->clearCache();
// Oder manuell
Cache::forget("stored_structure_{$month}_{$year}");
Cache::forget("root_users_{$month}_{$year}");
```

View file

@ -1,25 +1,28 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use App\Services\Util;
use App\Models\ShoppingOrder;
use App\Models\UserSalesVolume;
use App\Services\Util;
use App\User;
use stdClass;
class SalesPointsVolume
{
public static function changeSalesPointsVolumeUser(ShoppingOrder $shoppingOrder, $to_user_id){
public static function changeSalesPointsVolumeUser(ShoppingOrder $shoppingOrder, $to_user_id)
{
if($shoppingOrder->user_sales_volume){
if ($shoppingOrder->user_sales_volume) {
$to_user_id = intval($to_user_id);
if($shoppingOrder->user_sales_volume->user_id === $to_user_id){
if ($shoppingOrder->user_sales_volume->user_id === $to_user_id) {
\Session()->flash('alert-error', 'Keine Änderung: selber Berater');
return;
}
if(!$shoppingOrder->user_sales_volume->isCurrentMonthYear()){
if (! $shoppingOrder->user_sales_volume->isCurrentMonthYear()) {
\Session()->flash('alert-error', 'Änderung muss im selben Monat sein');
return;
}
@ -30,73 +33,74 @@ class SalesPointsVolume
$to_user = User::find($to_user_id);
$form_user = User::find($form_user_id);
$shoppingOrder->user_sales_volume->user_id = $to_user_id;
$shoppingOrder->user_sales_volume->message = 'zugewiesen: '.date('d.m.Y');
$shoppingOrder->user_sales_volume->user_id = $to_user_id;
$shoppingOrder->user_sales_volume->message = 'zugewiesen: ' . date('d.m.Y');
$syslog = $shoppingOrder->user_sales_volume->syslog;
$from_email = $form_user ? $form_user->email : '';
$to_email = $to_user ? $to_user->email : '';
$syslog[date('d.m.Y-h:i:s')] = 'change form: #'.$form_user_id.' '.$from_email.' to: #'.$to_user_id.' '.$to_email;
$syslog[date('d.m.Y-h:i:s')] = 'change form: #' . $form_user_id . ' ' . $from_email . ' to: #' . $to_user_id . ' ' . $to_email;
$shoppingOrder->user_sales_volume->syslog = $syslog;
$shoppingOrder->user_sales_volume->save();
//recalculate
// recalculate
self::reCalculateSalesPointsVolume($to_user_id, $month, $year);
self::reCalculateSalesPointsVolume($form_user_id, $month, $year);
\Session()->flash('alert-save', true);
}
}
private static function add_KP_TP_Points($userSalesVolume, $month_points){
if($userSalesVolume->status_points === 2) { //KP
private static function add_KP_TP_Points($userSalesVolume, $month_points)
{
if ($userSalesVolume->status_points === 2) { // KP
$month_points->KP += $userSalesVolume->points;
}else{
} else {
// === 1 //TP + KP
$month_points->KP += $userSalesVolume->points;
$month_points->TP += $userSalesVolume->points;
}
return $month_points;
}
public static function reCalculateSalesPointsVolume($user_id, $month, $year){
public static function reCalculateSalesPointsVolume($user_id, $month, $year)
{
$userSalesVolumes = UserSalesVolume::where('user_id', $user_id)->where('month', $month)->where('year', $year)->orderBy('id', 'ASC')->get();
$month_points = new stdClass();
$month_points = new stdClass;
$month_points->KP = 0;
$month_points->TP = 0;
$month_total_net = 0;
$month_shop_points = 0;
$month_shop_total_net = 0;
//TDOO Status === 3???
foreach($userSalesVolumes as $userSalesVolume){
// TDOO Status === 3???
foreach ($userSalesVolumes as $userSalesVolume) {
switch ($userSalesVolume->status) {
case 1: //Bestellung Berater
case 1: // Bestellung Berater
$month_points = self::add_KP_TP_Points($userSalesVolume, $month_points);
$month_total_net += $userSalesVolume->total_net;
$month_total_net += $userSalesVolume->total_net;
break;
case 2: //Shop
case 2: // Shop
$month_shop_points += $userSalesVolume->points;
$month_shop_total_net += $userSalesVolume->total_net;
$month_shop_total_net += $userSalesVolume->total_net;
break;
case 4: //Gutschrift
case 4: // Gutschrift
$month_points = self::add_KP_TP_Points($userSalesVolume, $month_points);
if($userSalesVolume->status_turnover === 2){
$month_shop_total_net += $userSalesVolume->total_net;
//ggf hier zu den Shop Points zählen wäre aber immer KP + TP kann nicht keine trennung bei month_shop_points
}else{
$month_total_net += $userSalesVolume->total_net;
if ($userSalesVolume->status_turnover === 2) {
$month_shop_total_net += $userSalesVolume->total_net;
// ggf hier zu den Shop Points zählen wäre aber immer KP + TP kann nicht keine trennung bei month_shop_points
} else {
$month_total_net += $userSalesVolume->total_net;
}
break;
case 5: //Registrierung
case 5: // Registrierung
$month_points = self::add_KP_TP_Points($userSalesVolume, $month_points);
$month_total_net += $userSalesVolume->total_net;
break;
$month_total_net += $userSalesVolume->total_net;
break;
}
$userSalesVolume->month_shop_points = $month_shop_points;
$userSalesVolume->month_shop_total_net = $month_shop_total_net;
@ -107,8 +111,8 @@ class SalesPointsVolume
}
}
public static function addSalesPointsVolumeUser(ShoppingOrder $shoppingOrder){
public static function addSalesPointsVolumeUser(ShoppingOrder $shoppingOrder)
{
/*
status
@ -118,15 +122,14 @@ class SalesPointsVolume
*/
$status = self::getStatusByOrderPaymentFor($shoppingOrder);
$user_id = $shoppingOrder->auth_user_id ? $shoppingOrder->auth_user_id : $shoppingOrder->member_id;
//akuteller tag / Monat.
$user_id = $shoppingOrder->auth_user_id ? $shoppingOrder->auth_user_id : $shoppingOrder->member_id;
// akuteller tag / Monat.
$month = date('m');
$year = date('Y');
$date = date('d.m.Y');
if($status === 3){ //shop bestellung User pending if is_like
$user_id = NULL;
if ($status === 3) { // shop bestellung User pending if is_like
$user_id = null;
}
$user_sales_volume = UserSalesVolume::create([
'user_id' => $user_id,
@ -135,24 +138,24 @@ class SalesPointsVolume
'year' => $year,
'date' => $date,
'points' => $shoppingOrder->points,
'total_net' => $shoppingOrder->subtotal,
'status_points' => 1, //KP + TP
'total_net' => $shoppingOrder->subtotal,
'status_points' => 1, // KP + TP
'message' => '',
'status' => $status,
]);
if($status !== 3){
if ($status !== 3) {
self::reCalculateSalesPointsVolume($user_sales_volume->user_id, $user_sales_volume->month, $user_sales_volume->year);
}
return $user_sales_volume;
}
public static function setToUserAndReCalculate(UserSalesVolume $user_sales_volume, $user_id){
public static function setToUserAndReCalculate(UserSalesVolume $user_sales_volume, $user_id)
{
//set month year date new, calculate it in the currently month!
//If the month has changed, it can no longer be added to the month before
// set month year date new, calculate it in the currently month!
// If the month has changed, it can no longer be added to the month before
$month = date('m');
$year = date('Y');
$date = date('d.m.Y');
@ -161,60 +164,64 @@ class SalesPointsVolume
$user_sales_volume->month = $month;
$user_sales_volume->year = $year;
$user_sales_volume->date = $date;
$user_sales_volume->status = 2; //hinzugefügt aus Shop can only Pending
$user_sales_volume->save();
$user_sales_volume->status = 2; // hinzugefügt aus Shop can only Pending
$user_sales_volume->save();
self::reCalculateSalesPointsVolume($user_id, $month, $year);
}
}
public static function getStatusByOrderPaymentFor(ShoppingOrder $shoppingOrder){
if($shoppingOrder->payment_for){
if($shoppingOrder->payment_for === 6){ //Kunde-Shop
if($shoppingOrder->shopping_user && $shoppingOrder->shopping_user->is_like){
return 3; //shop Kunden, berater zuordnen <- need?
public static function getStatusByOrderPaymentFor(ShoppingOrder $shoppingOrder)
{
if ($shoppingOrder->payment_for) {
if ($shoppingOrder->payment_for === 6) { // Kunde-Shop
if ($shoppingOrder->shopping_user && $shoppingOrder->shopping_user->is_like) {
return 3; // shop Kunden, berater zuordnen <- need?
}
return 2;
}
return 1;
}
return 0;
}
public static function editSalesPointsVolume($data){
public static function editSalesPointsVolume($data)
{
$user_sales_volume = UserSalesVolume::findOrFail($data['id']);
if(!$user_sales_volume->isCurrentMonthYear()){
if (! $user_sales_volume->isCurrentMonthYear()) {
\Session()->flash('alert-error', 'Änderung muss im selben Monat sein');
return;
}
$old_points = $user_sales_volume->points;
$old_total_net = $user_sales_volume->total_net;
$user_sales_volume->total_net = Util::reFormatNumber($data['total_net']);
$user_sales_volume->points = intval($data['points']);
$user_sales_volume->points = Util::reFormatNumber($data['points']);
$user_sales_volume->message = 'geändert: '.date('d.m.Y');
$user_sales_volume->message = 'geändert: ' . date('d.m.Y');
$user_sales_volume->info = $data['info'];
$user_sales_volume->status_points = $data['status_points'];
$user_sales_volume->status_turnover = isset($data['status_turnover']) ? intval($data['status_turnover']) : null;
$syslog = $user_sales_volume->syslog;
$syslog[date('d.m.Y-h:i:s')] = 'edit points: #'.$old_points.' '.$user_sales_volume->points .' total: #'.$old_total_net.' '.$user_sales_volume->total_net;
$syslog[date('d.m.Y-h:i:s')] = 'edit points: #' . $old_points . ' ' . $user_sales_volume->points . ' total: #' . $old_total_net . ' ' . $user_sales_volume->total_net;
$user_sales_volume->syslog = $syslog;
$user_sales_volume->save();
self::reCalculateSalesPointsVolume($user_sales_volume->user_id, $user_sales_volume->month, $user_sales_volume->year);
\Session()->flash('alert-success', "Points geändert");
return;
\Session()->flash('alert-success', 'Points geändert');
}
public static function addSalesPointsVolume($data){
if(!isset($data['user_id'])){
public static function addSalesPointsVolume($data)
{
if (! isset($data['user_id'])) {
\Session()->flash('alert-error', 'Kein Berater ausgewählt');
return;
}
$user = User::findOrFail($data['user_id']);
@ -223,8 +230,8 @@ class SalesPointsVolume
$date = date('d.m.Y');
$total_net = isset($data['total_net']) ? Util::reFormatNumber($data['total_net']) : 0;
$points = isset($data['points']) ? intval($data['points']) : 0;
$syslog[date('d.m.Y-h:i:s')] = 'add points: #'.$points.' total: #'.$total_net;
$points = isset($data['points']) ? Util::reFormatNumber($data['points']) : 0;
$syslog[date('d.m.Y-h:i:s')] = 'add points: #' . $points . ' total: #' . $total_net;
$status = isset($data['status']) ? intval($data['status']) : 4;
$status_turnover = isset($data['status_turnover']) ? intval($data['status_turnover']) : null;
@ -238,20 +245,14 @@ class SalesPointsVolume
'status_points' => $data['status_points'],
'status_turnover' => $status_turnover,
'total_net' => $total_net,
'message' => 'hinzugefügt: '.date('d.m.Y'),
'message' => 'hinzugefügt: ' . date('d.m.Y'),
'info' => $data['info'],
'syslog' => $syslog,
'status' => $status,
]);
self::reCalculateSalesPointsVolume($user_sales_volume->user_id, $user_sales_volume->month, $user_sales_volume->year);
\Session()->flash('alert-success', "Points hinzugefügt");
\Session()->flash('alert-success', 'Points hinzugefügt');
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
@ -17,31 +18,31 @@ class TreeCalcBot
private $sponsor;
private $init_from;
private static $userIDs = [];
public static function addUserID($id){
public static function addUserID($id)
{
self::$userIDs[$id] = $id;
}
public function __construct($month, $year, $init_from = 'member')
{
$this->date = new stdClass();
$date = Carbon::parse($year.'-'.$month.'-1');
$date = Carbon::parse($year . '-' . $month . '-1');
$this->date->month = $month;
$this->date->year = $year;
$this->date->start_date = $date->format('Y-m-d H:i:s');
$this->date->end_date = $date->endOfMonth()->format('Y-m-d H:i:s');
$this->init_from = $init_from;
}
public function initStructureAdmin($check = true, $forceLiveCalculation = false)
{
//check is month is saved.
if($check && $UserBusinessStructure = self::isFromStored($this->date->month, $this->date->year)){
if ($check && $UserBusinessStructure = self::isFromStored($this->date->month, $this->date->year)) {
$this->readStoredRootUsers($UserBusinessStructure);
$this->readStoredParentsUsers($UserBusinessStructure);
$this->readStoredParentlessUser($UserBusinessStructure);
}else{
} else {
$this->readRootUsers();
$this->readParentsUsers();
$this->readParentlessUser();
@ -50,20 +51,20 @@ class TreeCalcBot
public function initStructureUser($user_id)
{
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($user_id);
$BusinessUserItem->addUserID();
$this->business_users[] = $BusinessUserItem;
//check is month is saved.
if($UserBusinessStructure = self::isFromStored($this->date->month, $this->date->year)){
if ($UserBusinessStructure = self::isFromStored($this->date->month, $this->date->year)) {
$this->readStoredParentsUsers($UserBusinessStructure);
if(isset($this->business_users[0]) && $this->business_users[0]->sponsor){
if (isset($this->business_users[0]) && $this->business_users[0]->sponsor) {
$this->readStoredSponsorUser($this->business_users[0]->sponsor->user_id);
}
}else{
} else {
$this->readParentsUsers();
$this->readSponsorUser($user_id);
}
@ -74,11 +75,11 @@ class TreeCalcBot
$this->business_user = new BusinessUserItem($this->date);
$this->business_user->makeUser($user->id);
$this->business_user->checkSponsor($user);
if(!$this->business_user->isSave()){
if (!$this->business_user->isSave()) {
//Aufbau der Struktur für den User in die unendliche Tiefe.
$this->business_user->readParentsBusinessUsers();
//calculate Points in Lines
if(count($this->business_user->businessUserItems) > 0){
if (count($this->business_user->businessUserItems) > 0) {
$this->calcUserPoints($this->business_user->businessUserItems, 1);
}
//qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints)
@ -91,24 +92,26 @@ class TreeCalcBot
$this->business_user->storeUser();
}*/
public static function isFromStored($month, $year){
public static function isFromStored($month, $year)
{
//when is stored an completed
$UserBusinessStructure = UserBusinessStructure::where('year', $year)->where('month', $month)->first();
if($UserBusinessStructure && $UserBusinessStructure->completed){
return $UserBusinessStructure;
if ($UserBusinessStructure && $UserBusinessStructure->completed) {
return $UserBusinessStructure;
}
return false;
}
private function calcUserPoints($businessUserItems, $line){
if(!isset($this->business_user->business_lines[$line])){
$obj = new stdClass();
$obj->points = 0;
$this->business_user->addBusinessLineToUser($line, $obj);
private function calcUserPoints($businessUserItems, $line)
{
if (!isset($this->business_user->business_lines[$line])) {
$obj = new stdClass();
$obj->points = 0;
$this->business_user->addBusinessLineToUser($line, $obj);
}
foreach($businessUserItems as $business_user_item){
if(count($business_user_item->businessUserItems) > 0){
$this->calcUserPoints($business_user_item->businessUserItems, $line+1);
foreach ($businessUserItems as $business_user_item) {
if (count($business_user_item->businessUserItems) > 0) {
$this->calcUserPoints($business_user_item->businessUserItems, $line + 1);
}
//business_lines points nach line
$this->business_user->addBusinessLinePoints($line, $business_user_item->sales_volume_points_TP_sum); //TP + Shop Points
@ -117,36 +120,38 @@ class TreeCalcBot
}
}
public function getGrowthBonus(){
if(count($this->business_user->business_lines) > 6){
public function getGrowthBonus()
{
if (count($this->business_user->business_lines) > 6) {
$b_lines = $this->business_user->business_lines->toArray();
return array_slice($b_lines, 6);
return array_slice($b_lines, 6);
}
return [];
}
public function getKeybyLine($line, $key){
if($this->business_user->business_lines){
public function getKeybyLine($line, $key)
{
if ($this->business_user->business_lines) {
$b_lines = $this->business_user->business_lines;
if(isset($b_lines[$line])){
if($b_lines[$line] instanceof stdClass){
if(isset($b_lines[$line]->{$key})){
if (isset($b_lines[$line])) {
if ($b_lines[$line] instanceof stdClass) {
if (isset($b_lines[$line]->{$key})) {
return $b_lines[$line]->{$key};
}
}else{
if(isset($b_lines[$line][$key])){
} else {
if (isset($b_lines[$line][$key])) {
return $b_lines[$line][$key];
}
}
}
}
return 0;
}
//* reading from current*//
private function readRootUsers(){
private function readRootUsers()
{
$users = User::with('account')->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
@ -154,10 +159,10 @@ class TreeCalcBot
->where('users.m_level', "!=", null)
->where('users.m_sponsor', "=", null)
->where('users.payment_account', "!=", null)
->where('users.active_date', "<=", $this->date->end_date)
->where('users.active_date', "<=", $this->date->end_date)
->get();
if($users){
foreach($users as $user){
if ($users) {
foreach ($users as $user) {
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($user->id);
$BusinessUserItem->addUserID();
@ -166,23 +171,25 @@ class TreeCalcBot
}
}
private function readParentsUsers(){
foreach($this->business_users as $business_user){
private function readParentsUsers()
{
foreach ($this->business_users as $business_user) {
$business_user->readParentsBusinessUsers();
}
}
private function readParentlessUser(){
private function readParentlessUser()
{
$users = User::with('account')->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->date->end_date)
->get();
foreach($users as $user){
if(!isset(self::$userIDs[$user->id])){
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', "<", 4)
->where('users.payment_account', "!=", null)
->where('users.active_date', "<=", $this->date->end_date)
->get();
foreach ($users as $user) {
if (!isset(self::$userIDs[$user->id])) {
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($user->id);
$this->parentless[] = $BusinessUserItem;
@ -191,201 +198,207 @@ class TreeCalcBot
}
//* reading from stored*//
private function readStoredRootUsers(UserBusinessStructure $userBusinessStructure){
//* reading from stored*//
private function readStoredRootUsers(UserBusinessStructure $userBusinessStructure)
{
//first level is root
if($userBusinessStructure->structure){
foreach($userBusinessStructure->structure as $obj){
if ($userBusinessStructure->structure) {
foreach ($userBusinessStructure->structure as $obj) {
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($obj->user_id);
$BusinessUserItem->addUserID();
$this->business_users[] = $BusinessUserItem;
}
}
}
private function readStoredParentsUsers(UserBusinessStructure $userBusinessStructure){
foreach($this->business_users as $business_user){
private function readStoredParentsUsers(UserBusinessStructure $userBusinessStructure)
{
foreach ($this->business_users as $business_user) {
$business_user->readStoredParentsBusinessUsers($userBusinessStructure->structure);
}
}
private function readStoredParentlessUser(UserBusinessStructure $userBusinessStructure){
if($userBusinessStructure->parentless){
foreach($userBusinessStructure->parentless as $obj){
if(!isset(self::$userIDs[$obj->user_id])){
private function readStoredParentlessUser(UserBusinessStructure $userBusinessStructure)
{
if ($userBusinessStructure->parentless) {
foreach ($userBusinessStructure->parentless as $obj) {
if (!isset(self::$userIDs[$obj->user_id])) {
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($obj->user_id);
$this->parentless[] = $BusinessUserItem;
}
}
}
}
public function readSponsorUser($user_id){
public function readSponsorUser($user_id)
{
$user = User::find($user_id);
$userSponsor = User::find($user->m_sponsor);
if($userSponsor){
if ($userSponsor) {
$this->sponsor = new BusinessUserItem($this->date);
$this->sponsor->makeUser($userSponsor->id);
}
}
public function readStoredSponsorUser($user_id){
public function readStoredSponsorUser($user_id)
{
$this->sponsor = new BusinessUserItem($this->date);
$this->sponsor->makeUser($user_id);
}
public function getItems(){
public function getItems()
{
return $this->business_users;
}
public function makeHtmlTree(){
public function makeHtmlTree()
{
$deep = 0;
$ret = '<ol class="dd-list">';
foreach($this->business_users as $business_user){
$ret .= $this->addItem($business_user, $deep);
}
foreach ($this->business_users as $business_user) {
$ret .= $this->addItem($business_user, $deep);
}
$ret .= '</ol>';
return $ret;
}
private function addItem($item, $deep){
private function addItem($item, $deep)
{
$button = '';
if(($this->init_from === 'admin' && \Auth::user()->isAdmin()) || ($this->init_from === 'member')){ // && \Auth::user()->id === $item->user_id
if (($this->init_from === 'admin' && \Auth::user()->isAdmin()) || ($this->init_from === 'member')) { // && \Auth::user()->id === $item->user_id
$button = ' | <button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$item->user_id.'"
data-id="' . $item->user_id . '"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="'.$this->init_from .'"
data-route="'.route('modal_load').'"><span class="fa fa-calculator"></span></button>';
data-init_from="' . $this->init_from . '"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button>';
}
return '<li class="dd-item dd-nodrag" data-id="'.$item->user_id.'">'.
'<div class="dd-handle">
return '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
'<div class="dd-handle">
<div class="media align-items-center">
<div class="d-flex flex-column justify-content-center align-items-center">
'.(($deep > 0) ? '<div class="text-large font-weight-bolder line-height-1 my-2 text-secondary badge badge-outline-secondary">'.$deep.'</div>' : '').'
' . (($deep > 0) ? '<div class="text-large font-weight-bolder line-height-1 my-2 text-secondary badge badge-outline-secondary">' . $deep . '</div>' : '') . '
</div>
<div class="media-body ml-2">
<span class="'.($item->active_account ? '' : 'text-muted').'">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="'.$item->user_id.'" data-action="business-user-show" data-back="" data-modal="modal-md" data-init_from="'.$this->init_from .'" data-route="'.route('modal_load').'">
<span class="mr-1 ion ion-ios-contact '.($item->active_account ? 'text-primary' : 'text-danger').'"></span> <strong>'.$item->first_name.' '.$item->last_name.'</strong>
<span class="' . ($item->active_account ? '' : 'text-muted') . '">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="' . $item->user_id . '" data-action="business-user-show" data-back="" data-modal="modal-md" data-init_from="' . $this->init_from . '" data-route="' . route('modal_load') . '">
<span class="mr-1 ion ion-ios-contact ' . ($item->active_account ? 'text-primary' : 'text-danger') . '"></span> <strong>' . $item->first_name . ' ' . $item->last_name . '</strong>
</a>
<a href="mailto: '.$item->email.'">'.$item->email.'</a>
'.($item->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> '.$item->user_birthday : '').'
'.($item->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> '.$item->user_phone : '').'
<span class="badge badge-outline-default '.($item->active_account ? '' : 'text-muted').'">'.\App\Services\TranslationHelper::transUserLevelName($item->user_level_name).' | '.$item->m_account.'</span>
<br><span class="small">'.
($item->active_account ?
'<strong>'.__('team.total_points').': '.$item->sales_volume_points_KP_sum.'</strong> | '.__('team.e').': '.$item->sales_volume_KP_points.' | '.__('team.s').': '.$item->sales_volume_points_shop.' <strong>
| '.__('team.net_turnover').': '.formatNumber($item->sales_volume_total_sum).' &euro;</strong> | '.__('team.e').': '.formatNumber($item->sales_volume_total).' &euro; | '.__('team.s').': '.formatNumber($item->sales_volume_total_shop).' &euro;'.
$button
:
__('team.account_to').': '.$item->payment_account_date).
'</span>
<a href="mailto: ' . $item->email . '">' . $item->email . '</a>
' . ($item->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> ' . $item->user_birthday : '') . '
' . ($item->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> ' . $item->user_phone : '') . '
<span class="badge badge-outline-default ' . ($item->active_account ? '' : 'text-muted') . '">' . \App\Services\TranslationHelper::transUserLevelName($item->user_level_name) . ' | ' . $item->m_account . '</span>
<br><span class="small">' .
($item->active_account ?
'<strong>' . __('team.total_points') . ': ' . formatNumber($item->sales_volume_points_KP_sum) . '</strong> | ' . __('team.e') . ': ' . formatNumber($item->sales_volume_KP_points) . ' | ' . __('team.s') . ': ' . formatNumber($item->sales_volume_points_shop) . ' <strong>
| ' . __('team.net_turnover') . ': ' . formatNumber($item->sales_volume_total_sum) . ' &euro;</strong> | ' . __('team.e') . ': ' . formatNumber($item->sales_volume_total) . ' &euro; | ' . __('team.s') . ': ' . formatNumber($item->sales_volume_total_shop) . ' &euro;' .
$button
:
__('team.account_to') . ': ' . $item->payment_account_date) .
'</span>
</span>
</div>
</div>
</div>'.
$this->addParentItem($item, $deep).
</div>' .
$this->addParentItem($item, $deep) .
'</li>';
}
private function addParentItem($item, $deep){
if($item->businessUserItems){
private function addParentItem($item, $deep)
{
if ($item->businessUserItems) {
$ret = '<ol class="dd-list dd-nodrag">';
foreach($item->businessUserItems as $parent){
$ret .= $this->addItem($parent, $deep+1);
}
$ret .='</ol>';
foreach ($item->businessUserItems as $parent) {
$ret .= $this->addItem($parent, $deep + 1);
}
$ret .= '</ol>';
return $ret;
}
return;
}
return;
}
public function isParentless(){
public function isParentless()
{
return $this->parentless ? true : false;
}
}
public function makeParentlessHtml(){
$ret = "";
foreach($this->parentless as $item){
$ret .= '<li class="dd-item dd-nodrag" data-id="'.$item->user_id.'">'.
'<div class="dd-handle">
<span class="'.($item->active_account ? '' : 'text-muted').'">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="'.$item->user_id.'" data-action="business-user-show" data-back="" data-modal="modal-md" data-init_from="'.$this->init_from .'" data-route="'.route('modal_load').'">
<span class="mr-1 ion ion-ios-contact '.($item->active_account ? 'text-primary' : 'text-danger').'"></span> <strong>'.$item->first_name.' '.$item->last_name.'</strong>
public function makeParentlessHtml()
{
$ret = "";
foreach ($this->parentless as $item) {
$ret .= '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
'<div class="dd-handle">
<span class="' . ($item->active_account ? '' : 'text-muted') . '">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="' . $item->user_id . '" data-action="business-user-show" data-back="" data-modal="modal-md" data-init_from="' . $this->init_from . '" data-route="' . route('modal_load') . '">
<span class="mr-1 ion ion-ios-contact ' . ($item->active_account ? 'text-primary' : 'text-danger') . '"></span> <strong>' . $item->first_name . ' ' . $item->last_name . '</strong>
</a>
<a href="mailto: '.$item->email.'">'.$item->email.'</a>
'.($item->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> '.$item->user_birthday : '').'
'.($item->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> '.$item->user_phone : '').'
<span class="badge badge-outline-default '.($item->active_account ? '' : 'text-muted').'">'.\App\Services\TranslationHelper::transUserLevelName($item->user_level_name).' | '.$item->m_account.'</span>
<br><span class="small">'.
($item->active_account ?
'<strong>'.__('team.total_points').': '.$item->sales_volume_points_KP_sum.'</strong> | '.__('team.e').': '.$item->sales_volume_KP_points.' | '.__('team.s').': '.$item->sales_volume_points_shop.' <strong>
| '.__('team.net_turnover').': '.formatNumber($item->sales_volume_total_sum).' &euro;</strong> | '.__('team.e').': '.formatNumber($item->sales_volume_total).' &euro; | '.__('team.s').': '.formatNumber($item->sales_volume_total_shop).' &euro;'.
' | <button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$item->user_id.'"
<a href="mailto: ' . $item->email . '">' . $item->email . '</a>
' . ($item->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> ' . $item->user_birthday : '') . '
' . ($item->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> ' . $item->user_phone : '') . '
<span class="badge badge-outline-default ' . ($item->active_account ? '' : 'text-muted') . '">' . \App\Services\TranslationHelper::transUserLevelName($item->user_level_name) . ' | ' . $item->m_account . '</span>
<br><span class="small">' .
($item->active_account ?
'<strong>' . __('team.total_points') . ': ' . $item->sales_volume_points_KP_sum . '</strong> | ' . __('team.e') . ': ' . $item->sales_volume_KP_points . ' | ' . __('team.s') . ': ' . $item->sales_volume_points_shop . ' <strong>
| ' . __('team.net_turnover') . ': ' . formatNumber($item->sales_volume_total_sum) . ' &euro;</strong> | ' . __('team.e') . ': ' . formatNumber($item->sales_volume_total) . ' &euro; | ' . __('team.s') . ': ' . formatNumber($item->sales_volume_total_shop) . ' &euro;' .
' | <button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . $item->user_id . '"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-route="'.route('modal_load').'"><span class="fa fa-calculator"></span></button>'
:
__('team.account_to').' '.$item->payment_account_date).
'<br>'.$item->m_sponsor_name.
'</span>
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button>'
:
__('team.account_to') . ' ' . $item->payment_account_date) .
'<br>' . $item->m_sponsor_name .
'</span>
</span>
</div>'.
</div>' .
'</li>';
}
return $ret;
}
}
public function makeSponsorHtml(){
public function makeSponsorHtml()
{
if($this->sponsor){
if ($this->sponsor) {
//' | <a href="' . route('admin_business_user_detail', [$this->sponsor->id]) . '" class="btn icon-btn btn-xs btn-secondary"><span class="fa fa-calculator"></span></a>'
$ret = '<li class="dd-item dd-nodrag" data-id="">'.
'<div class="dd-handle">
<span class="'.($this->sponsor->active_account ? '' : 'text-muted').'">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="'.$this->sponsor->user_id.'" data-action="business-user-show" data-back="" data-init_from="'.$this->init_from .'" data-modal="modal-md" data-route="'.route('modal_load').'">
<span class="mr-1 ion ion-ios-contact '.($this->sponsor->active_account ? 'text-primary' : 'text-danger').'"></span> <strong>'.$this->sponsor->first_name.' '.$this->sponsor->last_name.'</strong>
$ret = '<li class="dd-item dd-nodrag" data-id="">' .
'<div class="dd-handle">
<span class="' . ($this->sponsor->active_account ? '' : 'text-muted') . '">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="' . $this->sponsor->user_id . '" data-action="business-user-show" data-back="" data-init_from="' . $this->init_from . '" data-modal="modal-md" data-route="' . route('modal_load') . '">
<span class="mr-1 ion ion-ios-contact ' . ($this->sponsor->active_account ? 'text-primary' : 'text-danger') . '"></span> <strong>' . $this->sponsor->first_name . ' ' . $this->sponsor->last_name . '</strong>
</a>
<a href="mailto: '.$this->sponsor->email.'">'.$this->sponsor->email.'</a>
'.($this->sponsor->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> '.$this->sponsor->user_birthday : '').'
'.($this->sponsor->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> '.$this->sponsor->user_phone : '').'
<span class="badge badge-outline-default '.($this->sponsor->active_account ? '' : 'text-muted').'">'.\App\Services\TranslationHelper::transUserLevelName($this->sponsor->user_level_name).' | '.$this->sponsor->m_account.'</span>';
<a href="mailto: ' . $this->sponsor->email . '">' . $this->sponsor->email . '</a>
' . ($this->sponsor->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> ' . $this->sponsor->user_birthday : '') . '
' . ($this->sponsor->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> ' . $this->sponsor->user_phone : '') . '
<span class="badge badge-outline-default ' . ($this->sponsor->active_account ? '' : 'text-muted') . '">' . \App\Services\TranslationHelper::transUserLevelName($this->sponsor->user_level_name) . ' | ' . $this->sponsor->m_account . '</span>';
if($this->init_from === 'admin'){
$ret .= '<br><span class="small">'.
($this->sponsor->active_account ?
'<strong>'.__('team.total_points').': '.$this->sponsor->sales_volume_points_KP_sum.'</strong> | '.__('team.e').': '.$this->sponsor->sales_volume_KP_points.' | '.__('team.s').': '.$this->sponsor->sales_volume_points_shop.' <strong>
| '.__('team.net_turnover').': '.formatNumber($this->sponsor->sales_volume_total_sum).' &euro;</strong> | '.__('team.e').': '.formatNumber($this->sponsor->sales_volume_total).' &euro; | '.__('team.s').': '.formatNumber($this->sponsor->sales_volume_total_shop).' &euro;'
:
__('team.account_to').' '.$this->sponsor->payment_account_date).
if ($this->init_from === 'admin') {
$ret .= '<br><span class="small">' .
($this->sponsor->active_account ?
'<strong>' . __('team.total_points') . ': ' . $this->sponsor->sales_volume_points_KP_sum . '</strong> | ' . __('team.e') . ': ' . $this->sponsor->sales_volume_KP_points . ' | ' . __('team.s') . ': ' . $this->sponsor->sales_volume_points_shop . ' <strong>
| ' . __('team.net_turnover') . ': ' . formatNumber($this->sponsor->sales_volume_total_sum) . ' &euro;</strong> | ' . __('team.e') . ': ' . formatNumber($this->sponsor->sales_volume_total) . ' &euro; | ' . __('team.s') . ': ' . formatNumber($this->sponsor->sales_volume_total_shop) . ' &euro;'
:
__('team.account_to') . ' ' . $this->sponsor->payment_account_date) .
'</span>';
}
$ret .= '</span>
}
$ret .= '</span>
</div>
</li>';
return $ret;
return $ret;
}
return __('team.no_sponsor_assigned');
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -79,9 +79,11 @@ class TreeCalcBotOptimized
if ($storedStructure) {
$this->logger->info("Loading stored business structure for {$this->date->month}/{$this->date->year}");
$this->loadStoredStructure($storedStructure);
return;
} else {
$this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year}");
$this->buildFreshStructure();
return;
}
} catch (\Exception $e) {
$this->logger->error("Error initializing admin structure: " . $e->getMessage());
@ -171,9 +173,8 @@ class TreeCalcBotOptimized
{
try {
$this->logger->info("Initializing business user details for: {$user->id}");
$this->businessUser = new BusinessUserItemOptimized($this->date, $this);
$this->businessUser->makeUserFromModel($user, $forceLiveCalculation); // ✅ Nutzt bereits User-Objekt
$this->businessUser->makeUserFromModel($user, $forceLiveCalculation);
$this->businessUser->checkSponsor($user);
// Führe vollständige Berechnung durch, wenn:
@ -184,14 +185,22 @@ class TreeCalcBotOptimized
$this->logger->info("Forcing live calculation for user {$user->id}");
}
// Aufbau der Struktur für den User in die unendliche Tiefe
// Phase 1: Aufbau der Struktur für den User in die unendliche Tiefe
$this->businessUser->readParentsBusinessUsers($forceLiveCalculation);
// Calculate Points in Lines (optimiert für Memory-Effizienz)
// Phase 2: 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)
// Phase 3: Qualifikation für ALLE User in der Struktur berechnen (Bottom-Up)
// WICHTIG: Muss VOR der Root-Qualifikation erfolgen, damit die Kinder
// ihr qual_user_level haben (für Growth Bonus Differenz-Berechnung)
if (count($this->businessUser->businessUserItems) > 0) {
$this->calculateQualificationsForStructure($this->businessUser->businessUserItems);
}
// Phase 4: Qualifikation für ROOT-User nach qual_kp und qual_pp
$this->businessUser->calcQualPP();
}
} catch (\Exception $e) {
@ -200,6 +209,96 @@ class TreeCalcBotOptimized
}
}
/**
* Berechnet Qualifikationen für alle User in der Struktur rekursiv (Bottom-Up)
*
* WICHTIG: Diese Methode muss NACH der Punkte-Aggregation aufgerufen werden!
* Sie stellt sicher, dass alle User in der Struktur ihr qual_user_level haben,
* was für die Growth Bonus Differenz-Berechnung benötigt wird.
*
* Der Ablauf ist:
* 1. Rekursiv zuerst die Kinder berechnen (Bottom-Up)
* 2. business_lines für diesen User berechnen (basierend auf seinen Kindern)
* 3. Qualifikation berechnen (verwendet business_lines für Payline-Punkte)
*
* @param array $businessUserItems Array von BusinessUserItemOptimized
*/
private function calculateQualificationsForStructure(array $businessUserItems): void
{
foreach ($businessUserItems as $item) {
// Rekursiv zuerst die Kinder berechnen (Bottom-Up)
// So haben tiefere Ebenen ihr qual_user_level bevor die höheren Ebenen berechnet werden
if (!empty($item->businessUserItems)) {
$this->calculateQualificationsForStructure($item->businessUserItems);
}
// Business Lines für diesen User berechnen (basierend auf seinen Kindern)
// WICHTIG: Dies ist nötig, damit getPointsforPayline() korrekt funktioniert
if (!empty($item->businessUserItems)) {
$this->calculateBusinessLinesForUser($item);
}
// Dann Qualifikation für diesen User berechnen
// Nur wenn noch nicht berechnet (Performance-Optimierung)
if (!$item->isQualificationCalculated()) {
$item->calcQualPP(false);
}
}
}
/**
* Berechnet die business_lines für einen einzelnen User basierend auf seinen Kindern
*
* Diese Methode aggregiert die Team-Punkte der Kinder in die business_lines,
* ähnlich wie calculateUserPointsOptimized, aber nur für einen einzelnen User.
*
* @param BusinessUserItemOptimized $user Der User, für den die business_lines berechnet werden
*/
private function calculateBusinessLinesForUser(BusinessUserItemOptimized $user): void
{
// Bereits berechnet? (business_lines existieren und haben Daten)
$existingLines = $user->business_lines;
if (!empty($existingLines) && count($existingLines) > 0) {
return;
}
// Initialisiere business_lines über die Methode
$user->initBusinessLines();
// Sammle alle Kinder rekursiv mit ihrer Tiefe
$this->collectChildrenPointsForUser($user->businessUserItems, 1, $user);
}
/**
* Rekursive Hilfsfunktion zum Sammeln der Punkte für business_lines
*
* @param array $children Die Kinder des Users
* @param int $line Die aktuelle Linie (Tiefe)
* @param BusinessUserItemOptimized $targetUser Der User, für den wir die business_lines bauen
*/
private function collectChildrenPointsForUser(array $children, int $line, BusinessUserItemOptimized $targetUser): void
{
foreach ($children as $child) {
// Initialisiere die Linie falls nötig
if (!$targetUser->hasBusinessLine($line)) {
$obj = new stdClass();
$obj->points = 0;
$targetUser->addBusinessLineToUser($line, $obj);
}
// Füge die Team-Punkte des Kindes hinzu
$points = (float) ($child->sales_volume_points_TP_sum ?? 0);
if ($points > 0) {
$targetUser->addBusinessLinePoints($line, $points);
}
// Rekursiv für die Kinder des Kindes (nächste Linie)
if (!empty($child->businessUserItems)) {
$this->collectChildrenPointsForUser($child->businessUserItems, $line + 1, $targetUser);
}
}
}
/**
* Gibt Growth Bonus zurück (ab Linie 6)
* Erweitert um Array/Object-Kompatibilität für business_lines
@ -276,6 +375,15 @@ class TreeCalcBotOptimized
return $this->businessUsers;
}
/**
* Getter-Methoden (Rückwärtskompatibilität)
*/
public function getItem(): object
{
return $this->businessUser;
}
/**
* Zählt die Gesamtanzahl aller User in der Struktur (rekursiv)
*/

View file

@ -22,8 +22,8 @@ class TreeHelperOptimized
}
$qualKP = (int) $userBusiness->qual_kp;
$pointsSum = (int) $userBusiness->sales_volume_points_KP_sum;
$qualKP = (float) $userBusiness->qual_kp;
$pointsSum = (float) $userBusiness->sales_volume_points_KP_sum;
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-info';
@ -39,8 +39,8 @@ class TreeHelperOptimized
return '-';
}
$qualKP = (int) $user->user_level->qual_kp;
$pointsSum = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$qualKP = (float) $user->user_level->qual_kp;
$pointsSum = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-warning-dark';
@ -54,9 +54,9 @@ class TreeHelperOptimized
public static function generateSalesVolumeDisplay(UserBusiness $userBusiness, string $type): string
{
if ($type === 'points') {
$total = (int) $userBusiness->sales_volume_points_KP_sum;
$individual = (int) $userBusiness->sales_volume_KP_points;
$shop = (int) $userBusiness->sales_volume_points_shop;
$total = (float) $userBusiness->sales_volume_points_KP_sum;
$individual = (float) $userBusiness->sales_volume_KP_points;
$shop = (float) $userBusiness->sales_volume_points_shop;
} else {
$total = (float) $userBusiness->sales_volume_total_sum;
$individual = (float) $userBusiness->sales_volume_total;
@ -64,9 +64,9 @@ class TreeHelperOptimized
$suffix = ' &euro;';
}
$totalFormatted = $type === 'points' ? $total : formatNumber($total);
$individualFormatted = $type === 'points' ? $individual : formatNumber($individual);
$shopFormatted = $type === 'points' ? $shop : formatNumber($shop);
$totalFormatted = formatNumber($total);
$individualFormatted = formatNumber($individual);
$shopFormatted = formatNumber($shop);
$suffix = $type === 'points' ? '' : ' &euro;';
return '<div class="no-line-break">' . $totalFormatted . $suffix . '</div>' .
@ -79,18 +79,18 @@ class TreeHelperOptimized
public static function generateSalesVolumeDisplayForUser(User $user, string $type, int $month, int $year): string
{
if ($type === 'points') {
$total = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$individual = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_KP_points');
$shop = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_shop');
$total = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$individual = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_KP_points');
$shop = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_shop');
} else {
$total = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total_sum');
$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);
$totalFormatted = formatNumber($total);
$individualFormatted = formatNumber($individual);
$shopFormatted = formatNumber($shop);
$suffix = $type === 'points' ? '' : ' &euro;';
return '<div class="no-line-break">' . $totalFormatted . $suffix . '</div>' .

View file

@ -65,9 +65,9 @@ class TreeHtmlRenderer
return '<li class="dd-item dd-nodrag" data-id="">' .
'<div class="dd-handle">' .
$this->renderUserInfo($sponsor, false, true) .
$this->renderUserInfo($sponsor, false, true) .
'</div>' .
'</li>';
'</li>';
}
/**
@ -100,30 +100,30 @@ class TreeHtmlRenderer
$html = '<div class="dd-item">';
$html .= '<div class="dd-handle">';
$html .= '<div class="row">';
// Sponsor Info
$html .= '<div class="col-md-3">';
$html .= '<strong>' . e($sponsor->account->first_name . ' ' . $sponsor->account->last_name) . '</strong><br>';
$html .= '<small>' . e($sponsor->email) . '</small>';
$html .= '</div>';
// Account Info
$html .= '<div class="col-md-2">';
$html .= '<span class="badge badge-secondary">' . e($sponsor->account->m_account ?? '') . '</span>';
$html .= '</div>';
// Level Info
$html .= '<div class="col-md-2">';
if ($sponsor->user_level) {
$html .= '<span class="badge badge-primary">' . e($sponsor->user_level->getLang('name')) . '</span>';
}
$html .= '</div>';
// Status
$html .= '<div class="col-md-2">';
$html .= get_active_badge($sponsor->isActiveAccount());
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
@ -139,52 +139,52 @@ class TreeHtmlRenderer
$html = '<li class="dd-item" data-id="' . ($member->user_id ?? 0) . '">';
$html .= '<div class="dd-handle">';
$html .= '<div class="row">';
// Einrückung basierend auf Tiefe
$indent = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;', $depth);
// Name und Email
$html .= '<div class="col-md-3">';
$html .= $indent;
$html .= '<strong>' . e(($member->first_name ?? '') . ' ' . ($member->last_name ?? '')) . '</strong><br>';
$html .= $indent . '<small>' . e($member->email ?? '') . '</small>';
$html .= '</div>';
// Account ID
$html .= '<div class="col-md-2">';
$html .= '<span class="badge badge-secondary">' . e($member->m_account ?? '') . '</span>';
$html .= '</div>';
// Level
$html .= '<div class="col-md-2">';
if (!empty($member->user_level_name)) {
$html .= '<span class="badge badge-primary">' . e($member->user_level_name) . '</span>';
if ($member->next_qual_user_level) {
$html .= '<span class="badge badge-outline-success ml-2"><i class="fa fa-arrow-up text-success" title="Karriere-Level erreicht!"></i></span>';
}
}
$html .= '</div>';
// Qualifikation
$html .= '<div class="col-md-2">';
if (!empty($member->qual_kp)) {
$pointsSum = (int) ($member->sales_volume_points_KP_sum ?? 0);
$qualKP = (int) $member->qual_kp;
$pointsSum = (float) ($member->sales_volume_points_KP_sum ?? 0);
$qualKP = (float) $member->qual_kp;
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-success' : 'badge-warning';
$html .= '<span class="badge ' . $badgeClass . '">KU ' . $qualKP . '</span>';
}
$html .= '</div>';
// Status
$html .= '<div class="col-md-2">';
$html .= get_active_badge($member->active_account ?? 0);
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
// Kinder rendern
if (!empty($member->businessUserItems) && is_array($member->businessUserItems)) {
$html .= '<ol class="dd-list">';
@ -193,9 +193,9 @@ class TreeHtmlRenderer
}
$html .= '</ol>';
}
$html .= '</li>';
return $html;
}
@ -215,10 +215,10 @@ class TreeHtmlRenderer
return '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
'<div class="dd-handle">' .
$this->renderUserCardWithDepth($item, $deep) .
$this->renderUserCardWithDepth($item, $deep) .
'</div>' .
$childrenHtml .
'</li>';
'</li>';
}
/**
@ -228,9 +228,9 @@ class TreeHtmlRenderer
{
return '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
'<div class="dd-handle">' .
$this->renderUserInfo($item, true, false) .
$this->renderUserInfo($item, true, false) .
'</div>' .
'</li>';
'</li>';
}
/**
@ -242,15 +242,15 @@ class TreeHtmlRenderer
if ($deep > 0) {
$depthBadge = '<div class="d-flex flex-column justify-content-center align-items-center">' .
'<div class="text-large font-weight-bolder line-height-1 my-2 text-secondary badge badge-outline-secondary">' . $deep . '</div>' .
'</div>';
'</div>';
}
return '<div class="media align-items-center">' .
$depthBadge .
'<div class="media-body ml-2">' .
$this->renderUserInfo($item, false, false) .
$this->renderUserInfo($item, false, false) .
'</div>' .
'</div>';
'</div>';
}
/**
@ -262,16 +262,16 @@ class TreeHtmlRenderer
$iconClass = $item->active_account ? 'text-primary' : 'text-danger';
\Log::debug("TreeHtmlRenderer: Rendering user info for user {$item->user_id}");
$html = '<span class="' . $statusClass . '">';
// User Link
$html .= '<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" ' .
'data-id="' . $item->user_id . '" data-action="business-user-show" data-back="" ' .
'data-modal="modal-md" data-init_from="' . $this->initFrom . '" data-route="' . route('modal_load') . '">' .
'<span class="mr-1 ion ion-ios-contact ' . $iconClass . '"></span> ' .
'<strong>' . e($item->first_name . ' ' . $item->last_name) . '</strong>' .
'</a>';
'</a>';
// Email
$html .= ' <a href="mailto:' . e($item->email) . '">' . e($item->email) . '</a>';
@ -292,7 +292,7 @@ class TreeHtmlRenderer
$levelName = $item->user_level_name ? TranslationHelper::transUserLevelName($item->user_level_name) : '';
$account = $item->m_account ?: '';
$html .= ' <span class="badge badge-outline-default ' . $statusClass . '">' . e($levelName . ' | ' . $account) . '</span>';
// Karriere-Aufstiegs-Icon für qualifizierte User (nur in Struktur-Ansicht)#
if ($item->next_qual_user_level) {
@ -302,15 +302,15 @@ class TreeHtmlRenderer
// Details für aktive Accounts
if ($item->active_account) {
$html .= '<br><span class="small">';
if(!$isSponsor){
if (!$isSponsor) {
$html .= $this->renderAccountDetails($item);
}
// Action Button (außer für Sponsor-Ansicht)
if (!$isSponsor && $this->shouldShowActionButton()) {
$html .= $this->renderActionButton($item->user_id);
}
$html .= '</span>';
} else {
// Inaktive Accounts
@ -336,14 +336,14 @@ class TreeHtmlRenderer
$totalPoints = $item->sales_volume_points_KP_sum ?: 0;
$ePoints = $item->sales_volume_KP_points ?: 0;
$sPoints = $item->sales_volume_points_shop ?: 0;
$totalSum = $item->sales_volume_total_sum ?: 0;
$eSum = $item->sales_volume_total ?: 0;
$sSum = $item->sales_volume_total_shop ?: 0;
return '<strong>' . __('team.total_points') . ': ' . $totalPoints . '</strong> | ' .
__('team.e') . ': ' . $ePoints . ' | ' .
__('team.s') . ': ' . $sPoints . ' <strong> | ' .
return '<strong>' . __('team.total_points') . ': ' . formatNumber($totalPoints) . '</strong> | ' .
__('team.e') . ': ' . formatNumber($ePoints) . ' | ' .
__('team.s') . ': ' . formatNumber($sPoints) . ' <strong> | ' .
__('team.net_turnover') . ': ' . formatNumber($totalSum) . ' &euro;</strong> | ' .
__('team.e') . ': ' . formatNumber($eSum) . ' &euro; | ' .
__('team.s') . ': ' . formatNumber($sSum) . ' &euro;';
@ -363,7 +363,7 @@ class TreeHtmlRenderer
'data-optimized="1" ' .
'data-route="' . route('modal_load') . '">' .
'<span class="fa fa-calculator"></span>' .
'</button>';
'</button>';
}
/**
@ -372,8 +372,8 @@ class TreeHtmlRenderer
private function shouldShowActionButton(): bool
{
try {
return ($this->initFrom === 'admin' && \Auth::check() && \Auth::user()->isAdmin()) ||
($this->initFrom === 'member');
return ($this->initFrom === 'admin' && \Auth::check() && \Auth::user()->isAdmin()) ||
($this->initFrom === 'member');
} catch (\Exception $e) {
// Fallback for tests or when no user is authenticated
return $this->initFrom === 'member';
@ -388,4 +388,4 @@ class TreeHtmlRenderer
$this->initFrom = $initFrom;
return $this;
}
}
}

View file

@ -2,12 +2,12 @@
namespace App\Services;
use App\Models\ShoppingOrder;
use App\Http\Controllers\SettingController;
use App\Models\ShoppingOrder;
/**
* DHL Data Helper
*
*
* Central class for preparing DHL API data structures
* Prevents code duplication between DhlShipmentService and CreateShipmentJob
*/
@ -15,31 +15,28 @@ class DhlDataHelper
{
/**
* Prepare order data for DHL API v2
*
*
* Structure matches official DHL API v2 createOrders endpoint:
* https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2
*
* @param ShoppingOrder $order
* @param float $weight
* @param array $options
* @param array|null $dhlConfig Optional pre-loaded config (for queue jobs)
* @return array
* @param array|null $dhlConfig Optional pre-loaded config (for queue jobs)
*/
public static function prepareOrderData(ShoppingOrder $order, float $weight, array $options = [], ?array $dhlConfig = null): array
{
\Log::info('prepareOrderData', $options);
//die daten für das versandlabel werden immer aus dem Formular genommen, damit anpassungen möglich sind
if (!isset($options['shipping_address'])) {
// die daten für das versandlabel werden immer aus dem Formular genommen, damit anpassungen möglich sind
if (! isset($options['shipping_address'])) {
throw new \Exception('shipping_address is required');
}
$shippingAddress = $options['shipping_address'];
// Get DHL configuration for shipper data
if ($dhlConfig === null) {
$settingController = new SettingController();
$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,
@ -63,7 +60,7 @@ class DhlDataHelper
// Consignee data (recipient) - from modal form (can be modified)
'consignee' => [
'name' => trim(($shippingAddress['firstname'] ?? '') . ' ' . ($shippingAddress['lastname'] ?? '')),
'name' => trim(($shippingAddress['firstname'] ?? '').' '.($shippingAddress['lastname'] ?? '')),
'name2' => $shippingAddress['company'] ?? '',
'street' => $shippingAddress['address'] ?? '',
'houseNumber' => $shippingAddress['houseNumber'] ?? '',
@ -72,6 +69,8 @@ class DhlDataHelper
'country' => $shippingAddress['country']?->code ?? 'DE',
'email' => $shippingAddress['email'] ?? '',
'phone' => $shippingAddress['phone'] ?? '',
// DHL Postnummer für Packstation/Paketbox
'postNumber' => $shippingAddress['postnumber'] ?? null,
// Store individual fields for easier access
'firstname' => $shippingAddress['firstname'] ?? '',
'lastname' => $shippingAddress['lastname'] ?? '',
@ -83,7 +82,7 @@ class DhlDataHelper
'services' => $options['services'] ?? [],
// Custom reference for tracking
'reference' => 'Order-' . $order->id,
'reference' => 'Order-'.$order->id,
];
}
}

View file

@ -2,14 +2,14 @@
namespace App\Services;
use App\Models\ShoppingOrder;
use App\Models\Country;
use Illuminate\Support\Facades\Log;
use App\Models\ShoppingOrder;
use Exception;
use Illuminate\Support\Facades\Log;
/**
* DHL Modal Service
*
*
* Service class that handles all business logic for the DHL shipment creation modal.
* Validates order data, processes addresses, and prepares data for the view.
*/
@ -30,10 +30,11 @@ class DhlModalService
/**
* Prepare modal data for DHL shipment creation
*
* @param mixed $id Order ID or 'new'
* @param array $data Additional data from the request
*
* @param mixed $id Order ID or 'new'
* @param array $data Additional data from the request
* @return array Prepared data for the view
*
* @throws Exception
*/
public function prepareModalData($id, array $data): array
@ -47,19 +48,20 @@ class DhlModalService
'errors' => [],
'warnings' => [],
'existingShipments' => [],
'modalMode' => 'search' // 'search', 'create', 'info'
'modalMode' => 'search', // 'search', 'create', 'info'
];
// If no order ID or 'new', return empty data for order selection
if (!$id || $id === 'new') {
if (! $id || $id === 'new') {
return $result;
}
try {
// Load and validate order
$order = $this->loadOrder($id);
if (!$order) {
if (! $order) {
$result['errors'][] = "Bestellung #{$id} wurde nicht gefunden.";
return $result;
}
@ -73,11 +75,11 @@ class DhlModalService
$forceCreate = isset($data['force_create']) && $data['force_create'];
// Determine modal mode based on existing shipments and force_create
if (!empty($existingShipments) && !$forceCreate) {
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)
'shipment_count' => count($existingShipments),
]);
} else {
$result['modalMode'] = 'create';
@ -90,23 +92,23 @@ class DhlModalService
// Validate address completeness
$addressValidation = $this->validateAddress($result['shippingAddress']);
if (!$addressValidation['valid']) {
if (! $addressValidation['valid']) {
$result['errors'] = array_merge($result['errors'], $addressValidation['errors']);
}
if (!empty($addressValidation['warnings'])) {
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'])
'address_valid' => empty($result['errors']),
]);
}
} catch (Exception $e) {
Log::error('[DHL Modal] Error preparing modal data', [
'order_id' => $id,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
$result['errors'][] = 'Fehler beim Laden der Bestelldaten: ' . $e->getMessage();
@ -117,24 +119,20 @@ class DhlModalService
/**
* Load order with required relationships
*
* @param mixed $id
* @return ShoppingOrder|null
*
* @param mixed $id
*/
private function loadOrder($id): ?ShoppingOrder
{
return ShoppingOrder::with([
'shopping_order_items',
'shopping_user',
'dhlShipments' // Include DHL shipments
'dhlShipments', // Include DHL shipments
])->find($id);
}
/**
* Get existing DHL shipments for the order
*
* @param ShoppingOrder $order
* @return array
*/
private function getExistingShipments(ShoppingOrder $order): array
{
@ -160,24 +158,23 @@ class DhlModalService
'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()
'is_delivered' => $shipment->isDelivered(),
'email' => $shipment->email, // E-Mail für Tracking-E-Mail Button
'can_send_email' => $shipment->canSendTrackingEmail(),
];
})->toArray();
}
/**
* Calculate order weight in kg
*
* @param ShoppingOrder $order
* @return float
*/
private function calculateOrderWeight(ShoppingOrder $order): float
{
return $order->weight / 1000; //from grams to kg
return $order->weight / 1000; // from grams to kg
/*
// Default fallback weight
$defaultWeight = 1.0;
if (!$order->shopping_order_items || $order->shopping_order_items->isEmpty()) {
return $defaultWeight;
}
@ -209,20 +206,17 @@ class DhlModalService
/**
* Process and parse shipping address from order
*
* @param ShoppingOrder $order
* @return array
*/
private function processShippingAddress(ShoppingOrder $order): array
{
$shoppingUser = $order->shopping_user;
if (!$shoppingUser) {
if (! $shoppingUser) {
return $this->getEmptyAddress();
}
// Determine if shipping address is different from billing
$useShipping = !($shoppingUser->same_as_billing ?? true);
$useShipping = ! ($shoppingUser->same_as_billing ?? true);
// Extract address data
$addressData = [
@ -237,6 +231,8 @@ class DhlModalService
'phone' => $useShipping ? ($shoppingUser->shipping_phone ?? '') : ($shoppingUser->billing_phone ?? ''),
'email' => $shoppingUser->billing_email ?? '',
'houseNumber' => '',
// DHL Postnummer für Packstation/Paketbox (nur bei Versandadresse)
'postnumber' => $useShipping ? ($shoppingUser->shipping_postnumber ?? '') : '',
];
// Parse and separate street name and number
@ -247,14 +243,12 @@ class DhlModalService
/**
* Parse street address and separate street name from house number
*
* @param array &$addressData
*/
private function parseStreetAddress(array &$addressData): void
{
$address = trim($addressData['address']);
// If address_2 is empty and address contains both street and number
if (!empty($address)) {
if (! empty($address)) {
// Try to separate street name and house number
$patterns = [
// Pattern 1: "Musterstraße 123" or "Musterstraße 123a"
@ -262,7 +256,7 @@ class DhlModalService
// Pattern 2: "Musterstraße 123-125" or "Musterstraße 123/125"
'/^(.+?)\s+(\d+[-\/]\d+[a-zA-Z]?)$/u',
// Pattern 3: "123 Musterstraße" (number first)
'/^(\d+[a-zA-Z]?)\s+(.+)$/u'
'/^(\d+[a-zA-Z]?)\s+(.+)$/u',
];
foreach ($patterns as $index => $pattern) {
@ -288,8 +282,7 @@ class DhlModalService
/**
* Validate address completeness and format
*
* @param array $address
*
* @return array Validation result with 'valid', 'errors', and 'warnings' keys
*/
private function validateAddress(array $address): array
@ -303,7 +296,7 @@ class DhlModalService
'lastname' => 'Nachname',
'address' => 'Straße',
'zipcode' => 'Postleitzahl',
'city' => 'Stadt'
'city' => 'Stadt',
];
foreach ($requiredFields as $field => $label) {
@ -318,33 +311,31 @@ class DhlModalService
}
// Street number validation
if (!empty($address['address']) && empty($address['houseNumber'])) {
if (! empty($address['address']) && empty($address['houseNumber'])) {
$warnings[] = 'Hausnummer konnte nicht automatisch erkannt werden. Bitte prüfen Sie die Adressangaben.';
}
// Postal code format validation for Germany
if (!empty($address['zipcode']) && $address['country'] && $address['country']->code === 'DE') {
if (!preg_match('/^\d{5}$/', $address['zipcode'])) {
if (! empty($address['zipcode']) && $address['country'] && $address['country']->code === 'DE') {
if (! preg_match('/^\d{5}$/', $address['zipcode'])) {
$warnings[] = 'Deutsche Postleitzahl sollte 5 Ziffern haben.';
}
}
// Country validation
if (!$address['country']) {
if (! $address['country']) {
$errors[] = 'Land konnte nicht ermittelt werden.';
}
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings
'warnings' => $warnings,
];
}
/**
* Get empty address template
*
* @return array
*/
private function getEmptyAddress(): array
{
@ -359,12 +350,13 @@ class DhlModalService
'country' => null,
'phone' => '',
'email' => '',
'postnumber' => '',
];
}
/**
* Get available countries for shipping
*
*
* @return \Illuminate\Database\Eloquent\Collection
*/
private function getAvailableCountries()
@ -374,13 +366,11 @@ class DhlModalService
/**
* Get available DHL product codes from settings
*
* @return array
*/
private function getAvailableProductCodes(): array
{
// Get DHL configuration with merged settings
$settingController = new \App\Http\Controllers\SettingController();
$settingController = new \App\Http\Controllers\SettingController;
$dhlConfig = $settingController->getDhlConfig();
$productCodes = [];
@ -388,19 +378,19 @@ class DhlModalService
// Add products based on configured account numbers
$accountNumbers = $dhlConfig['account_numbers'] ?? [];
if (!empty($accountNumbers['V01PAK'])) {
if (! empty($accountNumbers['V01PAK'])) {
$productCodes['V01PAK'] = 'DHL Paket National';
}
if (!empty($accountNumbers['V53PAK'])) {
if (! empty($accountNumbers['V53PAK'])) {
$productCodes['V53PAK'] = 'DHL Paket International';
}
if (!empty($accountNumbers['V62WP'])) {
if (! empty($accountNumbers['V62WP'])) {
$productCodes['V62WP'] = 'DHL Warenpost National';
}
if (!empty($accountNumbers['V07PAK'])) {
if (! empty($accountNumbers['V07PAK'])) {
$productCodes['V07PAK'] = 'DHL Retoure Online';
}
@ -409,7 +399,7 @@ class DhlModalService
$productCodes = [
'V01PAK' => 'DHL Paket National',
'V53PAK' => 'DHL Paket International',
'V62WP' => 'DHL Warenpost National'
'V62WP' => 'DHL Warenpost National',
];
}
@ -418,8 +408,7 @@ class DhlModalService
/**
* Validate shipment parameters before API call
*
* @param array $shipmentData
*
* @return array Validation result
*/
public function validateShipmentData(array $shipmentData): array
@ -438,7 +427,7 @@ class DhlModalService
// Product code validation
$productCode = $shipmentData['product_code'] ?? '';
$availableProducts = array_keys($this->getAvailableProductCodes());
if (!in_array($productCode, $availableProducts)) {
if (! in_array($productCode, $availableProducts)) {
$errors[] = 'Ungültiger Produktcode ausgewählt.';
}
@ -450,7 +439,7 @@ class DhlModalService
'shipping_houseNumber' => 'Hausnummer',
'shipping_zipcode' => 'Postleitzahl',
'shipping_city' => 'Stadt',
'shipping_country_id' => 'Land'
'shipping_country_id' => 'Land',
];
foreach ($requiredAddressFields as $field => $label) {
@ -462,20 +451,17 @@ class DhlModalService
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings
'warnings' => $warnings,
];
}
/**
* Prepare address data for DHL API
*
* @param array $formData
* @return array
*/
public function prepareAddressForApi(array $formData): array
{
$country = null;
if (!empty($formData['shipping_country_id'])) {
if (! empty($formData['shipping_country_id'])) {
$country = Country::find($formData['shipping_country_id']);
}
@ -491,7 +477,8 @@ class DhlModalService
'country_id' => $country?->id,
'country' => $country, // Store country object for DhlDataHelper
'phone' => trim($formData['shipping_phone'] ?? ''),
'email' => trim($formData['shipping_email'] ?? '') // Add email if available
'email' => trim($formData['shipping_email'] ?? ''), // Add email if available
'postnumber' => trim($formData['shipping_postnumber'] ?? ''), // DHL Postnummer für Packstation/Paketbox
];
}
}

View file

@ -2,9 +2,11 @@
namespace App\Services;
use Acme\Dhl\Models\DhlShipment;
use App\Models\ShoppingOrder;
use App\Http\Controllers\SettingController;
use App\Jobs\CreateShipmentJob;
use App\Jobs\CancelShipmentJob;
use App\Services\DhlDataHelper;
use Illuminate\Support\Facades\Log;
use Exception;
@ -144,4 +146,174 @@ class DhlShipmentService
];
}
}
/**
* Cancel a DHL shipment (sync or async based on config)
*
* @param DhlShipment $shipment
* @param array $options
* @return array
*/
public function cancelShipment(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->cancelShipmentAsync($shipment, $options, $dhlConfig);
} else {
return $this->cancelShipmentSync($shipment, $options, $dhlConfig);
}
}
/**
* Cancel shipment asynchronously using queue
*
* @param DhlShipment $shipment
* @param array $options
* @param array $dhlConfig
* @return array
*/
private function cancelShipmentAsync(DhlShipment $shipment, array $options, array $dhlConfig): array
{
try {
// Dispatch job
CancelShipmentJob::dispatch($shipment, $options);
Log::info('[DHL Service] Shipment cancellation dispatched to queue', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no
]);
return [
'success' => true,
'message' => 'Sendung wird storniert...',
'queued' => true,
'shipment_id' => $shipment->id
];
} catch (Exception $e) {
Log::error('[DHL Service] Failed to dispatch shipment cancellation', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return [
'success' => false,
'message' => 'Fehler beim Einreihen der Stornierung: ' . $e->getMessage(),
'queued' => false
];
}
}
/**
* Cancel shipment synchronously
*
* @param DhlShipment $shipment
* @param array $options
* @param array $dhlConfig
* @return array
*/
private function cancelShipmentSync(DhlShipment $shipment, array $options, array $dhlConfig): array
{
try {
// Validate shipment has DHL number
if (empty($shipment->dhl_shipment_no)) {
return [
'success' => false,
'message' => 'Sendung hat keine DHL-Sendungsnummer und kann nicht storniert werden.',
'queued' => false,
'shipment_id' => $shipment->id
];
}
// Validate shipment can be cancelled
if (! $shipment->canCancel()) {
return [
'success' => false,
'message' => 'Sendung kann im aktuellen Status "' . $shipment->status . '" nicht storniert werden. Nur Status "created" oder "pending" sind stornierbar.',
'queued' => false,
'shipment_id' => $shipment->id
];
}
Log::info('[DHL Service] Cancelling shipment synchronously', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'status' => $shipment->status,
'base_url' => $dhlConfig['base_url']
]);
// Create DHL client
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
$shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient);
// Cancel the shipment directly
$success = $shippingService->cancelLabel($shipment->dhl_shipment_no);
if ($success) {
Log::info('[DHL Service] Shipment cancelled successfully (sync)', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no
]);
return [
'success' => true,
'message' => 'Sendung wurde erfolgreich storniert!',
'queued' => false,
'shipment_id' => $shipment->id
];
} else {
throw new Exception('Cancellation returned false');
}
} catch (\InvalidArgumentException $e) {
Log::warning('[DHL Service] Shipment cancellation validation failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage()
]);
return [
'success' => false,
'message' => $e->getMessage(),
'queued' => false,
'shipment_id' => $shipment->id
];
} catch (Exception $e) {
Log::error('[DHL Service] Shipment cancellation failed (sync)', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'status' => $shipment->status,
'error' => $e->getMessage(),
'error_trace' => $e->getTraceAsString()
]);
// Check if it's an API authentication/resource error
$errorMessage = $e->getMessage();
if (strpos($errorMessage, 'RF-UndefinedResource') !== false) {
return [
'success' => false,
'message' => 'Die Sendung konnte bei DHL nicht gefunden werden. Mögliche Ursachen: Sendung wurde bereits storniert, ist zu alt, oder wurde in einem anderen Modus (Sandbox/Production) erstellt.',
'queued' => false,
'shipment_id' => $shipment->id,
'technical_error' => $errorMessage
];
}
return [
'success' => false,
'message' => 'Fehler beim Stornieren der Sendung: ' . $errorMessage,
'queued' => false,
'shipment_id' => $shipment->id
];
}
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\Attribute;
@ -13,24 +14,21 @@ use App\User;
class HTMLHelper
{
public static $months = [
1 => 'January',
2 => 'February',
3 => 'March',
4 => 'April',
5 => 'May',
6 => 'June',
7 => 'July',
8 => 'August',
9 => 'September',
10 => 'October',
11 => 'November',
12 => 'December'
2 => 'February',
3 => 'March',
4 => 'April',
5 => 'May',
6 => 'June',
7 => 'July',
8 => 'August',
9 => 'September',
10 => 'October',
11 => 'November',
12 => 'December',
];
private static $roles = [
0 => 'Berater',
1 => 'VIP',
@ -39,30 +37,35 @@ class HTMLHelper
4 => 'SySAdmin',
];
public static function getMonth($i){
public static function getMonth($i)
{
return trans('cal.months.'.self::$months[intval($i)]);
}
public static function getTransMonths(){
public static function getTransMonths()
{
$ret = [];
foreach(self::$months as $key=>$val){
$ret[$key] = trans('cal.months.'.$val);
}
return $ret;
foreach (self::$months as $key => $val) {
$ret[$key] = trans('cal.months.'.$val);
}
return $ret;
}
public static function getYearRange($start = 2021)
{
$end = date("Y");
$end = date('Y');
return array_reverse(range($start, $end));
}
public static function getRoleLabel($role_id = 0){
public static function getRoleLabel($role_id = 0)
{
return '<span class="badge badge-pill '.self::getLabel($role_id).'">'.self::$roles[$role_id].'</span>';
}
public static function getLabel($id){
public static function getLabel($id)
{
switch ($id) {
case 0:
return 'badge-default';
@ -80,308 +83,375 @@ class HTMLHelper
return 'badge-primary';
break;
}
}
public static function getRolesOptions(){
$ret = "";
foreach (self::$roles as $role_id => $value){
public static function getRolesOptions()
{
$ret = '';
foreach (self::$roles as $role_id => $value) {
$ret .= '<option value="'.$role_id.'">'.$value.'</option>\n';
}
return $ret;
}
public static function getYearSelectOptions(){
$start = date("Y", strtotime("-5 years", time()));
$end = date("Y", strtotime("+1 years", time()));
public static function getYearSelectOptions()
{
$start = date('Y', strtotime('-5 years', time()));
$end = date('Y', strtotime('+1 years', time()));
$values = range($start, $end);
$now = date("Y", time());
$ret = "";
foreach ($values as $value){
$now = date('Y', time());
$ret = '';
foreach ($values as $value) {
$attr = ($value == $now) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
}
public static function getAboDeliveryOptions($default = 5){
$values = \App\Models\UserAbo::$aboDeliveryDays;
$ret = "";
foreach ($values as $value){
public static function getAboDeliveryOptions($default = 5)
{
$values = \App\Models\UserAbo::$aboDeliveryDays;
$ret = '';
foreach ($values as $value) {
$attr = ($value == $default) ? 'selected="selected"' : '';
$str = self::getAboStrLang($value);
$ret .= '<option value="'.$value.'" '.$attr.'>'.$str.'</option>\n';
}
return $ret;
}
public static function getAboStrLang($num){
public static function getAboStrLang($num)
{
return $num.'. '.__('abo.of_month');
}
public static function getAboFirstExecutionDate($date, $interval)
{
return AboHelper::getFirstAboDate($date, $interval)->format('d.m.Y');
}
public static function getAttributesWithoutParents($id = false, $sameId = false, $all = true){
public static function getAttributesWithoutParents($id = false, $sameId = false, $all = true)
{
$values = Attribute::where('parent_id', null)->get();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
foreach ($values as $value){
if($sameId == $value->id){
foreach ($values as $value) {
if ($sameId == $value->id) {
continue;
}
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getCategoriesWithoutParents($id = false, $sameId = false, $all = true){
public static function getCategoriesWithoutParents($id = false, $sameId = false, $all = true)
{
$values = Category::where('parent_id', null)->get();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
foreach ($values as $value){
if($sameId == $value->id){
foreach ($values as $value) {
if ($sameId == $value->id) {
continue;
}
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getProductsOptions($ids = array(), $all = true){
if($ids == null){
$ids = array();
public static function getProductsOptions($ids = [], $all = true)
{
if ($ids == null) {
$ids = [];
}
$values = Product::all();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
foreach ($values as $value){
$attr = in_array($value->id, $ids) ? 'selected="selected"' : '';
foreach ($values as $value) {
$attr = in_array($value->id, $ids) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.' ('.($value->active ? 'on' : 'off').')</option>\n';
}
return $ret;
}
public static function getCategoriesOptions($ids = array(), $all = true){
public static function getCategoriesOptions($ids = [], $all = true)
{
$values = Category::where('active', 1)->orderBy('pos', 'DESC')->get();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
foreach ($values as $value){
foreach ($values as $value) {
$attr = in_array($value->id, $ids) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getProductIngredientsOptions($has_ids = array(), $all = true){
public static function getProductIngredientsOptions($has_ids = [], $all = true)
{
$values = Ingredient::where('active', 1)->get();
$ret = "";
$attr = "";
foreach ($values as $value){
if(!in_array($value->id, $has_ids)){
$ret = '';
$attr = '';
foreach ($values as $value) {
if (! in_array($value->id, $has_ids)) {
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
}
return $ret;
}
public static function getAttributesOptions($ids = array(), $all = true){
/**
* Erzeugt Options für Bundle-Produkt-Auswahl
* Filtert bereits enthaltene Produkte und das aktuelle Produkt selbst aus
*
* @param array $has_ids IDs der bereits enthaltenen Produkte
* @param int|null $exclude_product_id ID des aktuellen Produkts (um Selbst-Referenz zu vermeiden)
* @return string HTML Options
*/
public static function getProductBundleOptions($has_ids = [], $exclude_product_id = null)
{
$values = Product::where('active', 1)->orderBy('name')->get();
$ret = '';
foreach ($values as $value) {
// Überspringe bereits enthaltene Produkte und das Produkt selbst
if (! in_array($value->id, $has_ids) && $value->id != $exclude_product_id) {
$label = $value->name;
if ($value->number) {
$label = $value->number.' - '.$value->name;
}
$ret .= '<option value="'.$value->id.'">'.htmlspecialchars($label).'</option>\n';
}
}
return $ret;
}
public static function getAttributesOptions($ids = [], $all = true)
{
$values = Attribute::where('active', 1)->get();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
foreach ($values as $value){
$attr = in_array($value->id, $ids) ? 'selected="selected"' : '';
foreach ($values as $value) {
$attr = in_array($value->id, $ids) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getUserLevelOptions($id = false, $all = true){
public static function getUserLevelOptions($id = false, $all = true)
{
$values = UserLevel::where('active', 1)->get();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('none').'</option>\n';
}
foreach ($values as $value){
foreach ($values as $value) {
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->name.'</option>\n';
}
return $ret;
}
public static function getCompanyOptions($company){
$options = array(1 => __('business'), 0 => __('private'), );
$ret = "";
foreach ($options as $id => $value){
public static function getCompanyOptions($company)
{
$options = [1 => __('business'), 0 => __('private')];
$ret = '';
foreach ($options as $id => $value) {
$attr = ($id == $company) ? 'selected="selected"' : '';
$ret .= '<option value="'.$id.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
}
public static function getContriesWithMore($id, $all=true){#
public static function getContriesWithMore($id, $all = true)
{ //
$values = Country::all();
$counter = 1;
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value){
if( $counter == 7){
foreach ($values as $value) {
if ($counter == 7) {
$ret .= '<optgroup label="'.__('further countrie').'">';
}
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->getLocated().'</option>\n';
$counter ++;
$counter++;
}
$ret .= '</optgroup>';
return $ret;
}
public static function getContriesCodes($id, $all=true){#
public static function getContriesCodes($id, $all = true)
{ //
$values = Country::all();
$counter = 1;
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value){
foreach ($values as $value) {
if(!$value->phone) continue;
if( $counter == 7){
if (! $value->phone) {
continue;
}
if ($counter == 7) {
$ret .= '<optgroup label="'.__('further countrie').'">';
}
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->phone.'('.$value->getLocated().')</option>\n';
$counter ++;
$counter++;
}
$ret .= '</optgroup>';
return $ret;
}
public static function getCountriesWithoutUsedShippings($all=true){#
public static function getCountriesWithoutUsedShippings($all = true)
{ //
$values = Country::all();
$country_ids = ShippingCountry::all()->pluck('country_id')->toArray();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value){
if(!in_array($value->id, $country_ids)){
foreach ($values as $value) {
if (! in_array($value->id, $country_ids)) {
$ret .= '<option value="'.$value->id.'">'.$value->getLocated().'</option>\n';
}
}
return $ret;
}
public static function getCountryNameFormShipping($id){
public static function getCountryNameFormShipping($id)
{
$value = ShippingCountry::find($id);
if($value){
if ($value) {
return $value->country->getLocated();
}
return "not defined";
return 'not defined';
}
public static function getCountriesForShipping($id, $all=false){#
public static function getCountriesForShipping($id, $all = false)
{ //
$values = ShippingCountry::all();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value){
foreach ($values as $value) {
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$value->country->getLocated().'</option>\n';
}
return $ret;
}
public static function getSalutation($id){
$values = array('mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV'));
$ret = "";
public static function getSalutation($id)
{
$values = ['mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV')];
$ret = '';
$ret .= '<option value="">'.__('please select').'</option>\n';
foreach ($values as $key => $value){
foreach ($values as $key => $value) {
$attr = ($key == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$key.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
}
public static function getSalutationLang($id){
$values = array('mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV'));
return (!empty($values[$id]) ? $values[$id] : '');
public static function getSalutationLang($id)
{
$values = ['mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV')];
return ! empty($values[$id]) ? $values[$id] : '';
}
public static function getTaxSaleOptions($id){
$values = array('1' => __('account.taxable_sales_1'), '2' => __('account.taxable_sales_2'));
$ret = "";
public static function getTaxSaleOptions($id)
{
$values = ['1' => __('account.taxable_sales_1'), '2' => __('account.taxable_sales_2')];
$ret = '';
$ret .= '<option value="">'.__('please select').'</option>\n';
foreach ($values as $key => $value){
foreach ($values as $key => $value) {
$attr = ($key == $id) ? 'selected="selected"' : '';
$ret .= '<option value="'.$key.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
}
public static function getMembersOptions($id, $all=false){
public static function getMembersOptions($id, $all = false)
{
$values = User::where('active', '=', true)->where('blocked', '=', false)->where('payment_account', '>=', now())->get();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value){
foreach ($values as $value) {
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$to="";
if($value->account){
$to = $value->account->first_name." ".$value->account->last_name." | ";
$to = '';
if ($value->account) {
$to = $value->account->first_name.' '.$value->account->last_name.' | ';
}
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$to.$value->email.' #'.$value->account->m_account.'</option>\n';
}
return $ret;
}
public static function getUserCustomerOptions($id, $all=false){
public static function getUserCustomerOptions($id, $all = false)
{
$values = ShoppingUser::select(['id', 'billing_firstname', 'billing_lastname', 'billing_email', 'number'])
->where('shopping_users.member_id', '=', \Auth::user()->id)->get();
$ret = "";
if($all){
$ret = '';
if ($all) {
$ret .= '<option value="">'.__('please select').'</option>\n';
}
foreach ($values as $value){
foreach ($values as $value) {
$attr = ($value->id == $id) ? 'selected="selected"' : '';
$to = $value->billing_firstname." ".$value->billing_lastname." | ".$value->billing_email;
$to = $value->billing_firstname.' '.$value->billing_lastname.' | '.$value->billing_email;
$ret .= '<option value="'.$value->id.'" '.$attr.'>'.$to.' #'.$value->number.'</option>\n';
}
return $ret;
}
public static function getOptionRange($select, $from=1, $to=50){
public static function getOptionRange($select, $from = 1, $to = 50)
{
$values = range($from, $to);
$ret = "";
foreach ($values as $value){
$ret = '';
foreach ($values as $value) {
$attr = ($value == $select) ? 'selected="selected"' : '';
$ret .= '<option value="'.$value.'" '.$attr.'>'.$value.'</option>\n';
}
return $ret;
return $ret;
}
}
}

View file

@ -6,6 +6,7 @@ use App\Models\UserBusiness;
use App\Models\UserLevel;
use App\User;
use Illuminate\Support\Collection;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
class LevelReportService
{
@ -67,16 +68,29 @@ class LevelReportService
// Lade aktuellen User Level
$currentUser = $currentUserLevels->get($userBusiness->user_id);
$currentUserLevelName = 'Unbekannt';
$currentUserLevel = null;
if ($currentUser && $currentUser->m_level) {
$currentUserLevel = $userLevels->get($currentUser->m_level);
$currentUserLevelName = $currentUserLevel ? $currentUserLevel->name : 'Level ID: ' . $currentUser->m_level;
}
// Lade neues Level für POS-Vergleich (wird für Filter und level_updated benötigt)
$newLevel = $userLevels->get($newLevelId);
$newLevelPos = $newLevelData['pos'] ?? ($newLevel ? $newLevel->pos : 0);
// 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
// Skip wenn:
// 1. User existiert nicht oder hat kein Level
// 2. User ist bereits auf dem neuen Level (gleiche ID)
// 3. User hat bereits ein höheres oder gleichwertiges Level (POS >= neue Level POS)
if (
!$currentUser ||
$currentUser->m_level == $newLevelId ||
($currentUserLevel && $currentUserLevel->pos >= $newLevelPos)
) {
continue; // Skip - User ist bereits auf das neue Level umgestellt oder hat bereits ein höheres Level
}
}
@ -100,7 +114,7 @@ class LevelReportService
'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'),
'level_updated' => $onlyNotUpdated ? 'Nein' : ($currentUser && ($currentUser->m_level == $newLevelId || ($currentUserLevel && $currentUserLevel->pos >= $newLevelPos)) ? '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,
@ -155,7 +169,7 @@ class LevelReportService
return $stats;
}
public function exportToCsv(Collection $promotions, string $filename = null): string
public function exportToCsv(Collection $promotions, ?string $filename = null): string
{
if (!$filename) {
$filename = 'level_promotions_' . date('Y-m-d_H-i-s') . '.csv';
@ -229,4 +243,100 @@ class LevelReportService
return $filepath;
}
/**
* Holt Level-Aufstiege für ein Team basierend auf TreeCalcBotOptimized
* Nur für einen spezifischen Monat/Jahr und nur für Team-Mitglieder
*/
public function getTeamLevelPromotions(TreeCalcBotOptimized $treeCalcBot, int $month, int $year, array $filters = []): Collection
{
$onlyNotUpdated = $filters['only_not_updated'] ?? false;
// Lade UserLevels für Referenz
$userLevels = UserLevel::where('active', 1)->orderBy('pos')->get()->keyBy('id');
// Extrahiere alle User-IDs aus dem Team
$teamUserIds = $this->extractTeamUserIds($treeCalcBot);
if (empty($teamUserIds)) {
return collect([]);
}
// Lade UserBusiness Einträge für Team-Mitglieder mit Level-Aufstiegen
$userBusinesses = UserBusiness::whereIn('user_id', $teamUserIds)
->where('month', $month)
->where('year', $year)
->whereNotNull('next_qual_user_level')
->whereRaw("JSON_LENGTH(next_qual_user_level) > 0")
->orderBy('user_id')
->get();
return $this->processLevelPromotions($userBusinesses, $userLevels, $onlyNotUpdated);
}
/**
* Extrahiert alle User-IDs aus TreeCalcBotOptimized-Struktur
* Ähnlich wie getTeamUsersFromStructure im TeamController, aber nur IDs
*/
private function extractTeamUserIds(TreeCalcBotOptimized $treeCalcBot): array
{
$userIds = [];
$processedIds = [];
// Sammle User-IDs aus Root-Items
$businessUsers = $treeCalcBot->getItems();
foreach ($businessUsers as $businessUser) {
$userId = $businessUser->user_id ?? null;
if ($userId && !isset($processedIds[$userId])) {
$processedIds[$userId] = true;
$userIds[] = $userId;
$this->collectUserIdsRecursive($businessUser->businessUserItems ?? [], $userIds, $processedIds);
}
}
// Sammle parentless User-IDs
if ($treeCalcBot->isParentless()) {
$parentless = $treeCalcBot->__get('parentless');
if (is_array($parentless)) {
foreach ($parentless as $businessUser) {
if ($businessUser) {
$userId = $businessUser->user_id ?? null;
if ($userId && !isset($processedIds[$userId])) {
$processedIds[$userId] = true;
$userIds[] = $userId;
$this->collectUserIdsRecursive($businessUser->businessUserItems ?? [], $userIds, $processedIds);
}
}
}
}
}
return $userIds;
}
/**
* Sammelt rekursiv User-IDs aus businessUserItems
*/
private function collectUserIdsRecursive(array $businessUserItems, array &$userIds, array &$processedIds, int $depth = 0): void
{
$maxDepth = 20;
if ($depth > $maxDepth) {
return;
}
foreach ($businessUserItems as $businessUserItem) {
if ($businessUserItem) {
$userId = $businessUserItem->user_id ?? null;
if ($userId && !isset($processedIds[$userId])) {
$processedIds[$userId] = true;
$userIds[] = $userId;
// Rekursiv für verschachtelte Items
if (isset($businessUserItem->businessUserItems) && is_array($businessUserItem->businessUserItems)) {
$this->collectUserIdsRecursive($businessUserItem->businessUserItems, $userIds, $processedIds, $depth + 1);
}
}
}
}
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Mail\MailLog;
@ -9,26 +10,26 @@ class MyLog
{
public static function writeLog($channel = 'payment', $context = 'error', $message = '', $data = []){
public static function writeLog($channel = 'payment', $context = 'error', $message = '', $data = [], $mail = true)
{
switch ($context) {
case 'notice':
\Log::channel($channel)->notice($message.' : '.json_encode($data));
\Log::channel($channel)->notice($message . ' : ' . json_encode($data));
break;
case 'warning':
\Log::channel($channel)->warning($message.' : '.json_encode($data));
break;
\Log::channel($channel)->warning($message . ' : ' . json_encode($data));
break;
case 'info':
\Log::channel($channel)->info($message.' : '.json_encode($data));
\Log::channel($channel)->info($message . ' : ' . json_encode($data));
break;
default:
\Log::channel($channel)->error($message.' : '.json_encode($data));
\Log::channel($channel)->error($message . ' : ' . json_encode($data));
break;
}
Mail::to(config('app.exception_mail'))->send(new MailLog($channel, $context, $message, $data));
if ($mail) {
Mail::to(config('app.exception_mail'))->send(new MailLog($channel, $context, $message, $data));
}
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\ShoppingUser;
@ -8,7 +9,8 @@ use Yard;
class OrderPaymentService
{
public static function deleteInstance($identifier){
public static function deleteInstance($identifier)
{
Yard::instance('shopping')->deleteStoredCart($identifier);
\App\Models\ShoppingInstance::where('identifier', $identifier)->delete();
@ -20,53 +22,58 @@ class OrderPaymentService
}*/
}
public static function updateInstanceStatus($identifier, $status, $lower = true){
if(!ShoppingInstance::where('identifier', $identifier)->exists()){
public static function updateInstanceStatus($identifier, $status, $lower = true)
{
if (!ShoppingInstance::where('identifier', $identifier)->exists()) {
return false;
}
if($lower){
if ($lower) {
ShoppingInstance::where('identifier', $identifier)->where('status', '<', $status)
->update(['status' => $status]);
}else{
->update(['status' => $status]);
} else {
ShoppingInstance::where('identifier', $identifier)
->update(['status' => $status]);
->update(['status' => $status]);
}
}
public static function getInstanceStatus($identifier){
public static function getInstanceStatus($identifier)
{
$shopping_instance = ShoppingInstance::where('identifier', $identifier)->first();
if(!$shopping_instance){
if (!$shopping_instance) {
return false;
}
return $shopping_instance->getStatus();
}
public static function getTypeBadge(ShoppingInstance $shoppingInstance){
public static function getTypeBadge(ShoppingInstance $shoppingInstance)
{
$isFor = $shoppingInstance->shopping_data['is_for'] ?? '-';
if ($isFor === 'abo-ot-customer' ) {
return ' <span class="badge badge-pill badge-warning">'.__('abo.abo').'</span>';
if ($isFor === 'abo-ot-customer') {
return ' <span class="badge badge-pill badge-warning">' . __('abo.abo') . '</span>';
}
if ($isFor === 'ot-customer' ) {
return ' <span class="badge badge-pill badge-secondary">'.__('order.order').'</span>';
if ($isFor === 'ot-customer') {
return ' <span class="badge badge-pill badge-secondary">' . __('order.order') . '</span>';
}
return "";
return "";
}
public static function getStatusBadge(ShoppingInstance $shoppingInstance){
public static function getStatusBadge(ShoppingInstance $shoppingInstance)
{
$status = $shoppingInstance->getStatus();
$badgeClasses = [
'link_sent' => 'success',
'link_openly' => 'warning',
'link_sent' => 'info',
'link_openly' => 'info',
'link_paid' => 'secondary',
'link_check' => 'warning',
'link_pending' => 'warning',
'link_appointed' => 'secondary',
'link_appointed' => 'warning',
'link_failed' => 'danger',
'link_canceled' => 'danger'
];
if (isset($badgeClasses[$status])) {
return sprintf(' <span class="badge badge-pill badge-%s">%s</span>',
return sprintf(
' <span class="badge badge-pill badge-%s">%s</span>',
$badgeClasses[$status],
__('payment.' . $status)
);
@ -75,21 +82,23 @@ class OrderPaymentService
return '';
}
public static function getStatusAlert($status){
public static function getStatusAlert($status)
{
$badgeClasses = [
'link_sent' => 'success',
'link_openly' => 'success',
'link_sent' => 'info',
'link_openly' => 'info',
'link_check' => 'warning',
'link_pending' => 'warning',
'link_failed' => 'danger',
'link_canceled' => 'danger',
'link_appointed' => 'success',
'link_appointed' => 'warning',
'link_paid' => 'success',
];
if (isset($badgeClasses[$status])) {
return sprintf(' <div class="alert alert-%s">%s</div>',
return sprintf(
' <div class="alert alert-%s">%s</div>',
$badgeClasses[$status],
__('payment.alert_' . $status)
);
@ -98,19 +107,20 @@ class OrderPaymentService
return '';
}
public static function getCustomPayment($identifier){
public static function getCustomPayment($identifier)
{
$shopping_instance = ShoppingInstance::where('identifier', $identifier)->first();
if(!$shopping_instance){
if (!$shopping_instance) {
abort(403, __('msg.shopping_instance_not_found'));
}
$shopping_data = $shopping_instance->shopping_data;
$shopping_user = $shopping_data['shopping_user_id'] ? ShoppingUser::find($shopping_data['shopping_user_id']) : null;
if(!$shopping_user){
if (!$shopping_user) {
abort(403, __('msg.shopping_user_not_found'));
}
$yard_shopping_items = self::getRestoredYardShoppingItems($shopping_instance);
$data = [
'shopping_instance' => $shopping_instance,
'shopping_user' => $shopping_user,
@ -123,7 +133,8 @@ class OrderPaymentService
return $data;
}
public static function getRestoredYardShoppingItems($shopping_instance){
public static function getRestoredYardShoppingItems($shopping_instance)
{
Yard::instance('shopping')->destroy();
Yard::instance('shopping')->restore($shopping_instance->identifier, [], false);
@ -141,14 +152,14 @@ class OrderPaymentService
$is_currency = Yard::instance('shopping')->isPriceCurrency();
$tax_free = Yard::instance('shopping')->getUserTaxFree();
foreach($rows as $row){
foreach ($rows as $row) {
$product = \App\Models\Product::find($row->id);
$item = new \stdClass();
$item->image = $row->options->has('image') ? $row->options->image : null;
$item->price_net = (float) Yard::instance('shopping')->rowPriceNet($row, 3, '.', '');
$item->price_net_total = (float) Yard::instance('shopping')->rowSubtotalNet($row, 2, '.', '');
$item->price_currency = $is_currency ? "~".Yard::instance('shopping')->getCurrencyByKey('rowPriceNetCurrency', $row, 3)." ".Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$item->price_currency_total = $is_currency ? "~".Yard::instance('shopping')->getCurrencyByKey('rowSubtotalCurrency', $row, 3)." ".Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$item->price_currency = $is_currency ? "~" . Yard::instance('shopping')->getCurrencyByKey('rowPriceNetCurrency', $row, 3) . " " . Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$item->price_currency_total = $is_currency ? "~" . Yard::instance('shopping')->getCurrencyByKey('rowSubtotalCurrency', $row, 3) . " " . Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$item->price = $row->price;
$item->price_total = ($row->qty * $row->price);
$item->qty = $row->qty;
@ -158,23 +169,22 @@ class OrderPaymentService
$item->abo_type = AboHelper::getAboShowOn($product);
$item->number = $product->number;
$item->contents = $product->contents;
$ret['items'][] = $item;
$ret['items'][] = $item;
}
$ret['tax_free'] = $tax_free;
$ret['total']['subtotal'] = Yard::instance('shopping')->subtotal();
$ret['total']['subtotal_currency'] = $is_currency ? "~".Yard::instance('shopping')->getCurrencyByKey('subtotal')." ".Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['subtotal_currency'] = $is_currency ? "~" . Yard::instance('shopping')->getCurrencyByKey('subtotal') . " " . Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['shippingCountryName'] = Yard::instance('shopping')->getShippingCountryName();
$ret['total']['shippingNet'] = Yard::instance('shopping')->shippingNet();
$ret['total']['shippingNet currency'] = $is_currency ? "~".Yard::instance('shopping')->getCurrencyByKey('shippingNet')." ".Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['shippingNet currency'] = $is_currency ? "~" . Yard::instance('shopping')->getCurrencyByKey('shippingNet') . " " . Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['subtotalWithShipping'] = Yard::instance('shopping')->subtotalWithShipping();
$ret['total']['subtotalWithShipping_currency'] = $is_currency ? "~".Yard::instance('shopping')->getCurrencyByKey('subtotalWithShipping')." ".Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['subtotalWithShipping_currency'] = $is_currency ? "~" . Yard::instance('shopping')->getCurrencyByKey('subtotalWithShipping') . " " . Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['taxWithShipping'] = Yard::instance('shopping')->taxWithShipping();
$ret['total']['taxWithShipping_currency'] = $is_currency ? "~".Yard::instance('shopping')->getCurrencyByKey('taxWithShipping')." ".Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['taxWithShipping_currency'] = $is_currency ? "~" . Yard::instance('shopping')->getCurrencyByKey('taxWithShipping') . " " . Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['totalWithShipping'] = Yard::instance('shopping')->totalWithShipping();
$ret['total']['totalWithShipping_currency'] = $is_currency ? "~".Yard::instance('shopping')->getCurrencyByKey('totalWithShipping')." ".Yard::instance('shopping')->getPriceCurrencyUnit() : null;
$ret['total']['totalWithShipping_currency'] = $is_currency ? "~" . Yard::instance('shopping')->getCurrencyByKey('totalWithShipping') . " " . Yard::instance('shopping')->getPriceCurrencyUnit() : null;
return $ret;
}
}
}

View file

@ -1,20 +1,22 @@
<?php
namespace App\Services;
use App\User;
use App\Models\UserLevel;
use App\Mail\MailCheckout;
use App\Services\UserUtil;
use App\Services\AboHelper;
use App\Models\ProductBuying;
use App\Models\ShoppingOrder;
use App\Models\UserCreditItem;
use App\Models\ShoppingPayment;
use App\Services\ShopApiOrderCart;
use Illuminate\Support\Facades\Mail;
use App\Models\UserCreditItem;
use App\Models\UserLevel;
use App\Repositories\InvoiceRepository;
use App\Services\AboHelper;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\Services\ShopApiOrderCart;
use App\Services\UserUtil;
use App\Services\Util;
use App\User;
use Illuminate\Support\Facades\Mail;
class Payment
{
@ -61,63 +63,71 @@ class Payment
];
public static function getFormattedTxaction($txaction){
if($txaction && isset(self::$txaction_text[$txaction])){
return __('payment.'.self::$txaction_text[$txaction]);
public static function getFormattedTxaction($txaction)
{
if ($txaction && isset(self::$txaction_text[$txaction])) {
return __('payment.' . self::$txaction_text[$txaction]);
}
return __('payment.'.self::$txaction_text['NULL']);
return __('payment.' . self::$txaction_text['NULL']);
}
public static function getFormattedTxactionColor($txaction){
if($txaction && isset(self::$txaction_color[$txaction])){
public static function getFormattedTxactionColor($txaction)
{
if ($txaction && isset(self::$txaction_color[$txaction])) {
return self::$txaction_color[$txaction];
}
return "warning";
}
public static function getTransTxactionFilterText(){
$ret = [];
foreach(self::$txaction_filter_text as $key=>$val){
$ret[$key] = trans('payment.'.$val);
}
return $ret;
}
public static function getTransTxactionInvoice(){
$ret = [];
foreach(self::$txaction_invoice as $key=>$val){
$ret[$key] = trans('payment.'.$val);
}
return $ret;
}
public static function getShoppingOrderBadge(ShoppingOrder $shopping_order){
if($shopping_order->mode === 'test'){
return '<span class="badge badge-pill badge-default">'.strtoupper($shopping_order->mode).' - '.self::getFormattedTxaction($shopping_order->txaction).'</span>';
public static function getTransTxactionFilterText()
{
$ret = [];
foreach (self::$txaction_filter_text as $key => $val) {
$ret[$key] = trans('payment.' . $val);
}
if($shopping_order->mode === 'dev'){
return '<span class="badge badge-pill badge-info">'.strtoupper($shopping_order->mode).' - '.self::getFormattedTxaction($shopping_order->txaction).'</span>';
}
return '<span class="badge badge-pill badge-'.self::getFormattedTxactionColor($shopping_order->txaction).'">'.self::getFormattedTxaction($shopping_order->txaction).'</span>';
return $ret;
}
public static function getPaymentForBadge(ShoppingOrder $shopping_order){
public static function getTransTxactionInvoice()
{
$ret = [];
foreach (self::$txaction_invoice as $key => $val) {
$ret[$key] = trans('payment.' . $val);
}
return $ret;
}
public static function getShoppingOrderBadge(ShoppingOrder $shopping_order)
{
if ($shopping_order->mode === 'test') {
return '<span class="badge badge-pill badge-default">' . strtoupper($shopping_order->mode) . ' - ' . self::getFormattedTxaction($shopping_order->txaction) . '</span>';
}
if ($shopping_order->mode === 'dev') {
return '<span class="badge badge-pill badge-info">' . strtoupper($shopping_order->mode) . ' - ' . self::getFormattedTxaction($shopping_order->txaction) . '</span>';
}
return '<span class="badge badge-pill badge-' . self::getFormattedTxactionColor($shopping_order->txaction) . '">' . self::getFormattedTxaction($shopping_order->txaction) . '</span>';
}
public static function getPaymentForBadge(ShoppingOrder $shopping_order)
{
$abo = '';
if($shopping_order->is_abo){
$abo = ' <span class="badge badge-pill badge-success">'.__('abo.abo').'</span>';
if ($shopping_order->is_abo) {
$abo = ' <span class="badge badge-pill badge-success">' . __('abo.abo') . '</span>';
}
return '<span class="badge badge-pill badge-'.$shopping_order->getPaymentForColor().'">'.$shopping_order->getPaymentForType().'</span>'.$abo;
return '<span class="badge badge-pill badge-' . $shopping_order->getPaymentForColor() . '">' . $shopping_order->getPaymentForType() . '</span>' . $abo;
}
public static function getShoppingPaymentBadge(ShoppingPayment $shopping_payment){
if($shopping_payment->mode === 'test'){
return '<span class="badge badge-pill badge-default">'.strtoupper($shopping_payment->mode).' - '.self::getFormattedTxaction($shopping_payment->txaction).'</span>';
public static function getShoppingPaymentBadge(ShoppingPayment $shopping_payment)
{
if ($shopping_payment->mode === 'test') {
return '<span class="badge badge-pill badge-default">' . strtoupper($shopping_payment->mode) . ' - ' . self::getFormattedTxaction($shopping_payment->txaction) . '</span>';
}
return '<span class="badge badge-pill badge-'.self::getFormattedTxactionColor($shopping_payment->txaction).'">'.self::getFormattedTxaction($shopping_payment->txaction).'</span>';
return '<span class="badge badge-pill badge-' . self::getFormattedTxactionColor($shopping_payment->txaction) . '">' . self::getFormattedTxaction($shopping_payment->txaction) . '</span>';
}
public static function addUserCreditMargin(User $user, $credit, $status, $message){
public static function addUserCreditMargin(User $user, $credit, $status, $message)
{
UserCreditItem::create([
'user_id' => $user->id,
'credit' => $credit,
@ -128,22 +138,24 @@ class Payment
]);
}
public static function addBuyingRestriction(User $user, $product_id){
public static function addBuyingRestriction(User $user, $product_id)
{
ProductBuying::create([
'user_id' => $user->id,
'product_id' => $product_id,
'amount' => 1
'product_id' => $product_id,
'amount' => 1
]);
}
public static function addSponsorBuyingPoints(User $user, $product){
public static function addSponsorBuyingPoints(User $user, $product)
{
if($user->user_sponsor){
if ($user->user_sponsor) {
$data = [
'user_id' => $user->user_sponsor->id,
'total_net' => 0,
'points' => $product->sponsor_buying_points_amount,
'info' => 'VP: '.$user->getFullName(false).' | '.$product->name,
'info' => 'VP: ' . $user->getFullName(false) . ' | ' . $product->name,
'status_points' => 2,
'status' => 5
];
@ -151,14 +163,15 @@ class Payment
}
}
public static function updateUserLevel(User $user, $to_level_id){
public static function updateUserLevel(User $user, $to_level_id)
{
//nur updaten, wenn der user->m_level kleiner ist als $to_level_id
if($user->user_level){
if ($user->user_level) {
$ToUserLevel = UserLevel::find($to_level_id);
if($user->user_level->pos < $ToUserLevel->pos){
if ($user->user_level->pos < $ToUserLevel->pos) {
$user->m_level = $to_level_id;
}
}else{
} else {
$user->m_level = $to_level_id;
}
$user->save();
@ -169,7 +182,8 @@ class Payment
$paid = Status der Zahlung, Payone = true, MIVITA Rechnung = false damit kann später die rechnung auf bezahlt gesetzt werden.
*/
public static function paymentStatusPaidAction(ShoppingOrder $shopping_order, $paid, $shopping_payment = null){
public static function paymentStatusPaidAction(ShoppingOrder $shopping_order, $paid, $shopping_payment = null)
{
$send_link = false;
$shopping_order->setUserHistoryValue(['status' => 8]);
ShoppingUserService::snycOrdersByShoppingOrder($shopping_order);
@ -177,32 +191,32 @@ class Payment
$shopping_order->save();
//if product has actions
if($shopping_order->shopping_order_items && $shopping_order->auth_user_id){
foreach($shopping_order->shopping_order_items as $shopping_order_item){
if($shopping_order_item->product){
if ($shopping_order->shopping_order_items && $shopping_order->auth_user_id) {
foreach ($shopping_order->shopping_order_items as $shopping_order_item) {
if ($shopping_order_item->product) {
$user = User::findOrFail($shopping_order->auth_user_id);
$user->save();
if($shopping_order_item->product->buying_restriction){
if ($shopping_order_item->product->buying_restriction) {
self::addBuyingRestriction($user, $shopping_order_item->product->id);
}
if($shopping_order_item->product->sponsor_buying_points){
if ($shopping_order_item->product->sponsor_buying_points) {
self::addSponsorBuyingPoints($user, $shopping_order_item->product);
}
if($shopping_order_item->product->action){
if ($shopping_order_item->product->action) {
$send_link = true;
//new date
$date = \Carbon::now()->modify('1 year');
if($user->payment_account && $user->daysActiveAccount()>0){
if ($user->payment_account && $user->daysActiveAccount() > 0) {
$date = \Carbon::parse($user->payment_account)->modify('1 year');
}
foreach ($shopping_order_item->product->action as $do){
if($shopping_order_item->product->getActionName($do) === 'payment_for_account'){
foreach ($shopping_order_item->product->action as $do) {
if ($shopping_order_item->product->getActionName($do) === 'payment_for_account') {
$user->payment_order_id = $shopping_order_item->product->id; //34
$user->payment_account = $date;
$user->wizard = 100;
//only date is > now and acount is deactive.
if($date > \Carbon::now()){
if($user->active === 0){
if ($date > \Carbon::now()) {
if ($user->active === 0) {
$user->active = true;
UserUtil::reactiveUserResetChilds($user->id, 'on payment_for_account Payment');
}
@ -210,21 +224,21 @@ class Payment
$shopping_order->setUserHistoryValue(['status' => 9]);
}
if($shopping_order_item->product->getActionName($do) === 'payment_for_shop'){
if ($shopping_order_item->product->getActionName($do) === 'payment_for_shop') {
$user->payment_order_id = $shopping_order_item->product->id; //35
$user->payment_shop = $date;
$user->wizard = 100;
$shopping_order->setUserHistoryValue(['status' => 9]);
}
if($shopping_order_item->product->getActionName($do) === 'payment_for_shop_upgrade'){
if($shopping_order_item->product->upgrade_to_id){
if ($shopping_order_item->product->getActionName($do) === 'payment_for_shop_upgrade') {
if ($shopping_order_item->product->upgrade_to_id) {
$user->payment_order_id = $shopping_order_item->product->upgrade_to_id;
}
$user->payment_shop = $user->payment_account; //same Date, is upgrade
$shopping_order->setUserHistoryValue(['status' => 9]);
}
if($shopping_order_item->product->getActionName($do) === 'payment_for_lead_upgrade'){
if($shopping_order_item->product->upgrade_to_id){
if ($shopping_order_item->product->getActionName($do) === 'payment_for_lead_upgrade') {
if ($shopping_order_item->product->upgrade_to_id) {
self::updateUserLevel($user, $shopping_order_item->product->upgrade_to_id);
}
}
@ -232,66 +246,66 @@ class Payment
}
}
}
}
}
if($shopping_order->homeparty){
if ($shopping_order->homeparty) {
$shopping_order->setUserHistoryValue(['status' => 9]);
$shopping_order->homeparty->completed = 1;
$shopping_order->homeparty->save();
}
if($shopping_order->shopping_collect_order){
if ($shopping_order->shopping_collect_order) {
$shopping_order->setUserHistoryValue(['status' => 9]);
ShopApiOrderCart::finishOrder($shopping_order->shopping_collect_order);
}
//the Order is Pay, so we can set the Status in the Abo
if($shopping_order->is_abo){
if($shopping_payment){
if ($shopping_order->is_abo) {
if ($shopping_payment) {
Util::setInstanceStatusByPayment($shopping_payment, 10); //link_paid
$shopping_payment->identifier = null;
$shopping_payment->save();
}
AboHelper::setAboActive($shopping_order, 2);
AboHelper::setAboActive($shopping_order, 2, true);
}
//make Invoice is not exist and is live
if($shopping_order->mode === 'live'){
if ($shopping_order->mode === 'live' || Util::isTestSystem(true)) {
$invoice_repo = new InvoiceRepository($shopping_order);
if(!$shopping_order->isInvoice()){
if (!$shopping_order->isInvoice()) {
$invoice_repo->createAndSalesVolume();
}
}
return $send_link;
}
public static function paymentStatusSendMail(ShoppingOrder $shopping_order, $shopping_payment, $data){
public static function paymentStatusSendMail(ShoppingOrder $shopping_order, $shopping_payment, $data)
{
$bcc = [];
$billing_email = $shopping_order->shopping_user->billing_email;
// Überprüfung der Billing-E-Mail-Adresse
if(!$billing_email){
if($data['mode'] === 'test'){
if (!$billing_email) {
if ($data['mode'] === 'test') {
$billing_email = config('app.checkout_test_mail');
}else{
} else {
$billing_email = config('app.checkout_mail');
}
}
if(!filter_var($billing_email, FILTER_VALIDATE_EMAIL)){
\Log::channel('payment')->error("Invalid billing email at shopping_order ".$shopping_order->id, ['billing_email' => $billing_email]);
if (!filter_var($billing_email, FILTER_VALIDATE_EMAIL)) {
\Log::channel('payment')->error("Invalid billing email at shopping_order " . $shopping_order->id, ['billing_email' => $billing_email]);
$billing_email = config('app.checkout_mail');
}
if($data['mode'] === 'test'){
if ($data['mode'] === 'test') {
$bcc[] = config('app.checkout_test_mail');
}else{
} else {
$bcc[] = config('app.checkout_mail');
}
if(!$shopping_order->shopping_user->is_like && $shopping_order->shopping_user->member){
if (!$shopping_order->shopping_user->is_like && $shopping_order->shopping_user->member) {
$bcc[] = $shopping_order->shopping_user->member->email;
}
$data['payment_error'] = isset($data['payment_error']) ? $data['payment_error'] : false;

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use Yard;
@ -13,14 +14,16 @@ use App\Http\Controllers\Pay\PayoneController;
class PaymentHelper
{
public function setProduct($product){
public function setProduct($product)
{
Yard::instance('shopping')->destroy();
Yard::instance('shopping')->add($product->id, $product->getLang('name'), 1, $product->price, false, false, ['image' => "", 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
}
public function initELVPayment($user){
public function initELVPayment($user)
{
$shopping_user = $this->makeShoppingUser($user);
$shopping_order = $this->makeShoppingOrder($user, $shopping_user);
@ -41,7 +44,7 @@ class PaymentHelper
$pay->setPersonalData();
$response = $pay->onlyPaymentResponse();
$shopping_payment = $pay->getShoppingPayment();
if($response['status'] === 'ERROR'){
if ($response['status'] === 'ERROR') {
$payT = PaymentTransaction::create([
'shopping_payment_id' => $shopping_payment->id,
'request' => 'authorization',
@ -51,9 +54,9 @@ class PaymentHelper
'status' => $response['status'],
'mode' => $shopping_payment->mode,
]);
UserHistory::create(['user_id'=>$user->id, 'shopping_order_id'=>$shopping_order->id, 'action'=>'abo_open_payment', 'referenz'=>$payT->id, 'identifier'=>$user->payment_account, 'status'=>3]);
UserHistory::create(['user_id' => $user->id, 'shopping_order_id' => $shopping_order->id, 'action' => 'abo_open_payment', 'referenz' => $payT->id, 'identifier' => $user->payment_account, 'status' => 3]);
}
if($response['status'] === 'REDIRECT'){
if ($response['status'] === 'REDIRECT') {
$payT = PaymentTransaction::create([
'shopping_payment_id' => $shopping_payment->id,
'request' => 'authorization',
@ -63,9 +66,9 @@ class PaymentHelper
'mode' => $shopping_payment->mode,
]);
UserHistory::create(['user_id'=>$user->id, 'shopping_order_id'=>$shopping_order->id, 'action'=>'abo_open_payment', 'referenz'=>$payT->id, 'identifier'=>$user->payment_account, 'status'=>4]);
UserHistory::create(['user_id' => $user->id, 'shopping_order_id' => $shopping_order->id, 'action' => 'abo_open_payment', 'referenz' => $payT->id, 'identifier' => $user->payment_account, 'status' => 4]);
}
if($response['status'] === 'APPROVED'){
if ($response['status'] === 'APPROVED') {
$payT = PaymentTransaction::create([
'shopping_payment_id' => $shopping_payment->id,
'request' => 'authorization',
@ -73,13 +76,14 @@ class PaymentHelper
'userid' => $response['userid'],
'status' => $response['status'],
'transmitted_data' => $response,
'mode' => $shopping_payment->mode
]);
UserHistory::create(['user_id'=>$user->id, 'shopping_order_id'=>$shopping_order->id, 'action'=>'abo_open_payment', 'referenz'=>$payT->id, 'identifier'=>$user->payment_account, 'status'=>5]);
'mode' => $shopping_payment->mode
]);
UserHistory::create(['user_id' => $user->id, 'shopping_order_id' => $shopping_order->id, 'action' => 'abo_open_payment', 'referenz' => $payT->id, 'identifier' => $user->payment_account, 'status' => 5]);
}
}
public function makeShoppingUser($user, $is_from = 'membership', $is_for = 'me'){
public function makeShoppingUser($user, $is_from = 'membership', $is_for = 'me')
{
$shopping_user = new ShoppingUser();
$shopping_user->auth_user_id = $user->id;
$shopping_user->mode = 'prev';
@ -112,11 +116,13 @@ class PaymentHelper
$shopping_user->shipping_city = $user->account->shipping_city;
$shopping_user->shipping_country_id = $user->account->shipping_country_id;
$shopping_user->shipping_phone = $user->account->shipping_phone;
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
$shopping_user->save();
return $shopping_user;
}
public function makeShoppingOrder($user, $shopping_user){
public function makeShoppingOrder($user, $shopping_user)
{
$data = [
'shopping_user_id' => $shopping_user->id,
@ -137,11 +143,11 @@ class PaymentHelper
'txaction' => 'prev',
'mode' => $user->test_mode ? 'test' : 'live',
];
$shopping_order = ShoppingOrder::create($data);
$items = Yard::instance('shopping')->getContentByOrder();
foreach ($items as $item) {
if (!ShoppingOrderItem::where('shopping_order_id', $shopping_order->id)->where('row_id', $item->rowId)->count()){
if (!ShoppingOrderItem::where('shopping_order_id', $shopping_order->id)->where('row_id', $item->rowId)->count()) {
$price_net = Yard::instance('shopping')->rowPriceNet($item, 2, '.', '');
$tax = $item->price - $price_net;
$data = [
@ -165,6 +171,4 @@ class PaymentHelper
$shopping_order->makeTaxSplit();
return $shopping_order;
}
}
}

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\ShoppingCollectOrder;
@ -13,23 +14,23 @@ class ShopApiOrderCart
public function __construct()
{
$this->shoppingCollectOrder = new ShoppingCollectOrder();
$this->shoppingCollectOrder->shipping = 0;
$this->shoppingCollectOrder->shipping_net = 0;
$this->shoppingCollectOrder->shipping_tax = 0;
$this->shoppingCollectOrder->points = 0;
$this->shoppingCollectOrder->price_total_net = 0;
$this->shoppingCollectOrder->price_total = 0;
$this->shoppingCollectOrder->tax_total = 0;
$this->shoppingCollectOrder->qty_total = 0;
$this->shoppingCollectOrder->tax_split = [];
$this->shoppingCollectOrder->orders = [];
$this->shoppingCollectOrder->shop_items = [];
$this->shoppingCollectOrder = new ShoppingCollectOrder();
$this->shoppingCollectOrder->shipping = 0;
$this->shoppingCollectOrder->shipping_net = 0;
$this->shoppingCollectOrder->shipping_tax = 0;
$this->shoppingCollectOrder->points = 0;
$this->shoppingCollectOrder->price_total_net = 0;
$this->shoppingCollectOrder->price_total = 0;
$this->shoppingCollectOrder->tax_total = 0;
$this->shoppingCollectOrder->qty_total = 0;
$this->shoppingCollectOrder->tax_split = [];
$this->shoppingCollectOrder->orders = [];
$this->shoppingCollectOrder->shop_items = [];
}
public function add(ShoppingOrder $shopping_order){
public function add(ShoppingOrder $shopping_order)
{
$order = new stdClass();
$order->order_id = $shopping_order->id;
@ -41,25 +42,25 @@ class ShopApiOrderCart
$this->shoppingCollectOrder->shipping += $shopping_order->shipping;
$this->shoppingCollectOrder->shipping_net += $shopping_order->shipping_net;
foreach ($shopping_order->shopping_order_items as $item){
foreach ($shopping_order->shopping_order_items as $item) {
$tax_rate = intval($item->product->tax);
$user_price_net = $this->calcuPriceWith($item->price, $tax_rate, $item->discount);
$user_price_net_qty = round($user_price_net * $item->qty, 2);
$user_tax = $this->calcuTaxWith($user_price_net, $tax_rate);
$user_tax_qty = round($user_tax * $item->qty, 2);
$shop_item_id = $item->product->id.'-'.$item->price;
$shop_item_id = $item->product->id . '-' . $item->price;
//set to item
if(isset($this->shoppingCollectOrder->shop_items[$shop_item_id])){
if (isset($this->shoppingCollectOrder->shop_items[$shop_item_id])) {
$shop_item = $this->shoppingCollectOrder->shop_items[$shop_item_id];
if ($shop_item instanceof stdClass) {
$shop_item->user_price_total_net += $user_price_net_qty;
$shop_item->user_tax_total += $user_tax_qty;
$shop_item->points_total += ($item->points * $item->qty);
$shop_item->points_total += round($item->points * $item->qty, 2);
$shop_item->qty += $item->qty;
}
}else{
} else {
$shop_item = new stdClass();
$shop_item->pid = $item->product->id;
@ -69,30 +70,31 @@ class ShopApiOrderCart
$shop_item->qty = $item->qty;
$shop_item->tax_rate = $tax_rate;
$shop_item->points = $item->points;
$shop_item->points = round($item->points, 2);
$shop_item->user_price_net = round($user_price_net, 2);
$shop_item->user_price_total_net = round($user_price_net_qty, 2);
$shop_item->user_tax = round($user_tax, 2);
$shop_item->user_tax_total = round($user_tax_qty, 2);
$shop_item->points_total = ($item->points * $item->qty);
$shop_item->points_total = round($item->points * $item->qty, 2);
}
//only for tax split / tax and price on calculate function
$this->shoppingCollectOrder->addTaxToSplit((int)$tax_rate, (float)$user_tax_qty);
$this->shoppingCollectOrder->addNetToSplit((int)$tax_rate, (float)$user_price_net_qty);
$this->shoppingCollectOrder->tax_total += $user_tax_qty;
$this->shoppingCollectOrder->price_total_net += $user_price_net_qty;
$this->shoppingCollectOrder->points += ($item->points * $item->qty);
$this->shoppingCollectOrder->qty_total += $item->qty;
//only for tax split / tax and price on calculate function
$this->shoppingCollectOrder->addShopItem($shop_item_id, $shop_item);
$this->shoppingCollectOrder->addTaxToSplit((int)$tax_rate, (float)$user_tax_qty);
$this->shoppingCollectOrder->addNetToSplit((int)$tax_rate, (float)$user_price_net_qty);
$this->shoppingCollectOrder->tax_total += $user_tax_qty;
$this->shoppingCollectOrder->price_total_net += $user_price_net_qty;
$this->shoppingCollectOrder->points += round($item->points * $item->qty, 2);
$this->shoppingCollectOrder->qty_total += $item->qty;
$this->shoppingCollectOrder->addShopItem($shop_item_id, $shop_item);
}
$this->shoppingCollectOrder->addOrder($order);
}
public function calculate(){
public function calculate()
{
$this->shoppingCollectOrder->shipping_tax = round($this->shoppingCollectOrder->shipping - $this->shoppingCollectOrder->shipping_net, 2);
$this->shoppingCollectOrder->tax_total += $this->shoppingCollectOrder->shipping_tax;
//add shipping tax to split
@ -100,15 +102,16 @@ class ShopApiOrderCart
$this->shoppingCollectOrder->addNetToSplit(config('app.main_tax_rate'), $this->shoppingCollectOrder->shipping_net);
$this->shoppingCollectOrder->price_total_net += $this->shoppingCollectOrder->shipping_net;
$this->shoppingCollectOrder->price_total = round($this->shoppingCollectOrder->tax_total + $this->shoppingCollectOrder->price_total_net, 2);
$this->shoppingCollectOrder->price_total = round($this->shoppingCollectOrder->tax_total + $this->shoppingCollectOrder->price_total_net, 2);
}
public function store(){
public function store()
{
$this->shoppingCollectOrder->user_id = \Auth::user()->id;
$this->shoppingCollectOrder->status = 1;
//remove shopping_order
$temp = [];
foreach($this->orders as $order){
foreach ($this->orders as $order) {
$order->shopping_order = null;
$temp[] = $order;
}
@ -117,53 +120,55 @@ class ShopApiOrderCart
}
//price brutto calu with
private function calcuPriceWith($price, $tax_rate = null, $discount = null){
private function calcuPriceWith($price, $tax_rate = null, $discount = null)
{
$tax_dec = ($tax_rate + 100) / 100;
$price = $price / $tax_dec;
$margin = (($discount -100)*-1) / 100;
$margin = (($discount - 100) * -1) / 100;
$price = $price * $margin;
return round($price, 2);
}
private function calcuTaxWith($price, $tax_rate = null){
private function calcuTaxWith($price, $tax_rate = null)
{
$tax_dec = ($tax_rate + 100) / 100;
$tax = ($price * $tax_dec) - $price;
return round($tax, 2);
}
private function setOrderAdress($from, $shopping_user){
$ret = "";
if($from === 'billing'){
$ret .= $shopping_user->billing_company ? 'Firma: '.$shopping_user->billing_company.' | ' : '';
$ret .= \App\Services\HTMLHelper::getSalutationLang($shopping_user->billing_salutation).' ';
$ret .= $shopping_user->billing_firstname.' ';
$ret .= $shopping_user->billing_lastname.' | ';
$ret .= $shopping_user->billing_address.' | ';
$ret .= $shopping_user->billing_zipcode.' ';
$ret .= $shopping_user->billing_city.' | ';
$ret .= $shopping_user->billing_country->getLocated().' | ';
$ret .= $shopping_user->billing_email;
private function setOrderAdress($from, $shopping_user)
{
$ret = "";
if ($from === 'billing') {
$ret .= $shopping_user->billing_company ? 'Firma: ' . $shopping_user->billing_company . ' | ' : '';
$ret .= \App\Services\HTMLHelper::getSalutationLang($shopping_user->billing_salutation) . ' ';
$ret .= $shopping_user->billing_firstname . ' ';
$ret .= $shopping_user->billing_lastname . ' | ';
$ret .= $shopping_user->billing_address . ' | ';
$ret .= $shopping_user->billing_zipcode . ' ';
$ret .= $shopping_user->billing_city . ' | ';
$ret .= $shopping_user->billing_country->getLocated() . ' | ';
$ret .= $shopping_user->billing_email;
}
if($from === 'shipping'){
if($shopping_user->same_as_billing == 1){
if ($from === 'shipping') {
if ($shopping_user->same_as_billing == 1) {
return 'Lieferadresse ist gleich Rechnungsadresse';
}
$ret .= $shopping_user->shipping_company ? 'Firma: '.$shopping_user->shipping_company.' | ' : '';
$ret .= \App\Services\HTMLHelper::getSalutationLang($shopping_user->shipping_salutation).' ';
$ret .= $shopping_user->shipping_firstname.' ';
$ret .= $shopping_user->shipping_lastname.' | ';
$ret .= $shopping_user->shipping_address.' | ';
$ret .= $shopping_user->shipping_zipcode.' ';
$ret .= $shopping_user->shipping_city.' | ';
$ret .= $shopping_user->shipping_country->getLocated().' | ';
$ret .= $shopping_user->shipping_company ? 'Firma: ' . $shopping_user->shipping_company . ' | ' : '';
$ret .= \App\Services\HTMLHelper::getSalutationLang($shopping_user->shipping_salutation) . ' ';
$ret .= $shopping_user->shipping_firstname . ' ';
$ret .= $shopping_user->shipping_lastname . ' | ';
$ret .= $shopping_user->shipping_address . ' | ';
$ret .= $shopping_user->shipping_zipcode . ' ';
$ret .= $shopping_user->shipping_city . ' | ';
$ret .= $shopping_user->shipping_country->getLocated() . ' | ';
}
return $ret;
}
public function __get($property) {
public function __get($property)
{
if (property_exists($this->shoppingCollectOrder, $property)) {
return $this->shoppingCollectOrder->$property;
}
@ -171,20 +176,20 @@ class ShopApiOrderCart
return $this->shoppingCollectOrder->{$property};
}
}
public function getTotalTax()
{
return $this->shoppingCollectOrder->tax_total;
return $this->shoppingCollectOrder->tax_total;
}
public function getTotalPriceNetto()
{
return $this->shoppingCollectOrder->price_total_net;
return $this->shoppingCollectOrder->price_total_net;
}
public function getTotalPrice()
{
return $this->shoppingCollectOrder->price_total;
return $this->shoppingCollectOrder->price_total;
}
public function getTaxSplit()
@ -192,10 +197,11 @@ class ShopApiOrderCart
return 0;
}
public static function finishOrder(ShoppingCollectOrder $shoppingCollectOrder){
public static function finishOrder(ShoppingCollectOrder $shoppingCollectOrder)
{
//get orders an set
foreach($shoppingCollectOrder->orders as $order){
foreach ($shoppingCollectOrder->orders as $order) {
$ShoppingOrder = ShoppingOrder::findOrFail($order['order_id']);
$ShoppingOrder->api_status = 2; //bestellt
$api_notice = $ShoppingOrder->api_notice;
@ -207,5 +213,4 @@ class ShopApiOrderCart
$shoppingCollectOrder->status = 2; //order
$shoppingCollectOrder->save();
}
}
}

View file

@ -1,21 +1,23 @@
<?php
namespace App\Services;
use App\User;
use App\Models\ShoppingUser;
use App\Models\UserCleanUpLog;
use App\Models\ShoppingUserMemberLog;
use App\Http\Controllers\Api\KasController;
use App\Models\ShoppingUser;
use App\Models\ShoppingUserMemberLog;
use App\Models\UserCleanUpLog;
use App\Models\UserShop;
use App\User;
class UserUtil
{
public static function setShoppingUserToNewMember($pre_member_id, $new_member_id)
{
{
$ShoppingUsers = ShoppingUser::where('member_id', $pre_member_id)->get();
foreach($ShoppingUsers as $shopping_user){
foreach ($ShoppingUsers as $shopping_user) {
ShoppingUserMemberLog::create([
'pre_member_id' => $shopping_user->member_id,
'shopping_user_id' => $shopping_user->id,
@ -25,11 +27,12 @@ class UserUtil
$shopping_user->save();
}
}
public static function setNewSponsorToChilds($inactive_sponsor_id, $new_sponsor_id){
public static function setNewSponsorToChilds($inactive_sponsor_id, $new_sponsor_id)
{
//alle User die diesen inaktivien Sponsor haben
$child_users = User::where('m_sponsor', $inactive_sponsor_id)->get(); //auch deaktiverte
foreach($child_users as $child_user){
foreach ($child_users as $child_user) {
UserCleanUpLog::create([
'inactive_sponsor_id' => $inactive_sponsor_id,
'child_user_id' => $child_user->id,
@ -40,35 +43,36 @@ class UserUtil
}
}
public static function resetChildsToSponsor($re_sponsor_id){
public static function resetChildsToSponsor($re_sponsor_id)
{
//alle alten Childs vom re_sponsor_id / User wieder herstellen
$UserCleanUpUsers = UserCleanUpLog::where('inactive_sponsor_id', $re_sponsor_id)->get();
foreach($UserCleanUpUsers as $UserCleanUpUser){
foreach ($UserCleanUpUsers as $UserCleanUpUser) {
$child_user = User::find($UserCleanUpUser->child_user_id);
if($child_user){
if ($child_user) {
//delete Logs from user child where is newer then this
$deleteUserCleanUpLogs = UserCleanUpLog::where('child_user_id', $UserCleanUpUser->child_user_id)->where('created_at', '>', $UserCleanUpUser->created_at)->get();
foreach($deleteUserCleanUpLogs as $deleteUserCleanUpLog){
foreach ($deleteUserCleanUpLogs as $deleteUserCleanUpLog) {
$deleteUserCleanUpLog->delete();
}
if($child_user->m_sponsor){ // child is active
if ($child_user->m_sponsor) { // child is active
$child_user->m_sponsor = $re_sponsor_id;
}
if($child_user->pre_sponsor){ //child is inactive
if ($child_user->pre_sponsor) { //child is inactive
$child_user->pre_sponsor = $re_sponsor_id;
}
}
$child_user->save();
//delete this log
$UserCleanUpUser->delete();
}
}
}
public static function setUserToClient($user_id, $sponsor_id){
public static function setUserToClient($user_id, $sponsor_id)
{
$user = User::find($user_id);
if($user){
if ($user) {
$data = [
'member_id' => $sponsor_id,
'language' => $user->lang ? $user->lang : 'de',
@ -94,84 +98,87 @@ class UserUtil
'shipping_city' => $user->account->shipping_city,
'shipping_country_id' => $user->account->shipping_country_id,
'shipping_phone' => $user->account->getShippingPhoneFull(),
'shipping_postnumber' => $user->account->shipping_postnumber,
];
ShoppingUser::create($data);
}
}
/*
find next activ sponsor on user id
first $sponsor_id can user_id, looks has m_sponsor or pre_sponsor.
*/
public static function findNextActiveSponsor($sponsor_id){
public static function findNextActiveSponsor($sponsor_id)
{
$user = User::withTrashed()->find($sponsor_id);
if(!$user){ //kein User unter der ID - to root
if (!$user) { //kein User unter der ID - to root
return User::find(6);
}
//user ist aktiv
if($user->isActiveAccount()){
if ($user->isActiveAccount()) {
return $user;
}
if($user->m_sponsor){ //hat der User einen m_sponsor
if ($user->m_sponsor) { //hat der User einen m_sponsor
return self::findNextActiveSponsor($user->m_sponsor);
}
if($user->pre_sponsor){ //hat der User einen pre_sponsor - schon inaktiv
if ($user->pre_sponsor) { //hat der User einen pre_sponsor - schon inaktiv
return self::findNextActiveSponsor($user->pre_sponsor);
}
//dump('not sponsor');
return $user;
return $user;
}
public static function deactiveUser($user){
public static function deactiveUser($user)
{
$user->pre_sponsor = $user->m_sponsor; //den sponsor speichern für wiederherstellung
$user->m_sponsor = null;
$user->active = false;
$user->save();
$user->save();
}
public static function reactiveUser($user){
if($user->pre_sponsor){
public static function reactiveUser($user)
{
if ($user->pre_sponsor) {
$pre_sponsor = self::findNextActiveSponsor($user->pre_sponsor);
$user->m_sponsor = $pre_sponsor->id; //den sponsor wiederherstellen
$user->pre_sponsor = null;
}
$user->active = true;
$user->save();
$user->save();
}
public static function deleteUser(User $user, $complete = false)
{
//shop wird gelöscht
if($user->shop){
$subdomain_name = $user->shop->slug.'.mivita.care';
$user->shop->name = "delete".$user->shop->id;
$user->shop->slug = "delete".$user->shop->id;
if ($user->shop) {
// $subdomain_name = $user->shop->slug . '.mivita.care';
$user->shop->name = "delete" . $user->shop->id;
$user->shop->slug = "delete" . $user->shop->id;
$user->shop->save();
$user->shop->delete();
//isset KAS - delete Subdomain
if(!Util::isTestSystem()){
/*if (!Util::isTestSystem()) {
$kas = new KasController();
$pra = array(
'subdomain_name' => $subdomain_name,
);
$kas->action('delete_subdomain', $pra);
}
}*/
}
//user soll nicht komplett gelöscht werden
$user->email = "delete-".$user->email;
$user->email = "delete-" . $user->email;
//password wird gelöscht
$user->password = "delete".time();
$user->password = "delete" . time();
$user->confirmed = 0;
$user->confirmation_code = "delete".time();
$user->confirmation_code = "delete" . time();
$user->confirmation_date = null;
$user->confirmation_code_to = null;
$user->confirmation_code_remider = 2;
// $user->agreement = null;
// $user->agreement = null;
$user->active = 0;
$user->remember_token = '';
$user->active_date = null;
@ -179,9 +186,9 @@ class UserUtil
$user->deleted_at = now();
$user->pre_deleted_at = now();
//user soll komplett gelöscht werden
if($complete){
$user->email = "delete-".time()."-".rand(1000, 9999);
if($user->account){
if ($complete) {
$user->email = "delete-" . time() . "-" . rand(1000, 9999);
if ($user->account) {
$user->account->delete();
}
$user->pre_deleted_at = null;
@ -191,36 +198,82 @@ class UserUtil
return true;
}
public static function checkEmailExists($user)
{
$email = str_replace("delete-", "", $user->email);
public static function reactiveUserResetChilds($user_id, $info = ''){
$user = User::where('email', $email)->first();
if ($user) {
return 'Der Account kann nicht wieder hergestellt werden, da die E-Mail-Adresse <b>' . $email . '</b> bereits in Verwendung ist.';
}
return null;
}
public static function restoreUser($user, $payment_account)
{
if ($user->pre_sponsor) {
$pre_sponsor = self::findNextActiveSponsor($user->pre_sponsor);
$user->m_sponsor = $pre_sponsor->id; //den sponsor wiederherstellen
$user->pre_sponsor = null;
}
$user->email = str_replace("delete-", "", $user->email);
$user->confirmed = 1;
$user->confirmation_date = now();
$user->confirmation_code = null;
$user->confirmation_code_to = null;
$user->confirmation_code_remider = 0;
$user->active = 1;
$user->active_date = now();
$user->deleted_at = null;
$user->pre_deleted_at = null;
$user->payment_account = $payment_account;
$user->payment_shop = $payment_account;
$user->wizard = 100;
$user->save();
$userShop = UserShop::withTrashed()->where('user_id', $user->id)->first();
if ($userShop) {
$userShop->name = null;
$userShop->slug = null;
$userShop->active = 0;
$userShop->deleted_at = null;
$userShop->save();
}
}
public static function reactiveUserResetChilds($user_id, $info = '')
{
$user = User::find($user_id);
if(!$user){
\Log::channel('cleanup')->error('reactiveUserResetChilds find no user by user_id:'.$user_id);
if (!$user) {
\Log::channel('cleanup')->error('reactiveUserResetChilds find no user by user_id:' . $user_id);
return 0;
}
$data = [
'user_id' => $user->id,
'user_id' => $user->id,
'email' => $user->email,
'm_account' => $user->account ? $user->account->m_account : '',
'm_first_name' => $user->account ? $user->account->m_first_name : '',
'm_last_name' => $user->account ? $user->account->m_last_name : '',
];
\Log::channel('cleanup')->info('reactiveUserResetChilds '.$info.' : '.json_encode($data));
\Log::channel('cleanup')->info('reactiveUserResetChilds ' . $info . ' : ' . json_encode($data));
self::reactiveUser($user);
self::resetChildsToSponsor($user->id);
}
public static function deactiveUserNewSponsorChilds($user_id, $info = ''){
public static function deactiveUserNewSponsorChilds($user_id, $info = '')
{
$user = User::find($user_id);
if(!$user){
\Log::channel('cleanup')->error('deactiveUserNewSponsorChilds find no user by user_id:'.$user_id);
if (!$user) {
\Log::channel('cleanup')->error('deactiveUserNewSponsorChilds find no user by user_id:' . $user_id);
return 0;
}
$data = [
'user_id' => $user->id,
'user_id' => $user->id,
'email' => $user->email,
'm_account' => $user->account ? $user->account->m_account : '',
'm_first_name' => $user->account ? $user->account->m_first_name : '',
@ -228,14 +281,12 @@ class UserUtil
];
$active_sponsor = self::findNextActiveSponsor($user->m_sponsor);
if($active_sponsor){
if ($active_sponsor) {
self::setNewSponsorToChilds($user->id, $active_sponsor->id);
}else{
\Log::channel('cleanup')->error('cleanUpInActiveUser find no active_sponsor by inactive_user:'.$user->id);
} else {
\Log::channel('cleanup')->error('cleanUpInActiveUser find no active_sponsor by inactive_user:' . $user->id);
}
\Log::channel('cleanup')->info('deactiveUserNewSponsorChilds '.$info.' : '.json_encode($data));
\Log::channel('cleanup')->info('deactiveUserNewSponsorChilds ' . $info . ' : ' . json_encode($data));
self::deactiveUser($user);
}
}
}

View file

@ -2,17 +2,13 @@
namespace App\Services;
use App\Models\Country;
use App\Models\ShippingCountry;
use App\Models\UserHistory;
use App\Models\UserShop;
use Illuminate\Support\Str;
use Request;
use Yard;
class Util
{
private static $postRoute = 'base.';
public static function getToken()
@ -23,111 +19,153 @@ class Util
public static function uuidToken()
{
$uuid = (string) Str::uuid();
$e_uuid = explode("-", $uuid);
$e_uuid = explode('-', $uuid);
if (isset($e_uuid[0]) && $e_uuid[1]) {
return $e_uuid[0] . "-" . $e_uuid[1];
return $e_uuid[0] . '-' . $e_uuid[1];
}
return $uuid;
}
public static function formatDate()
{
if (\App::getLocale() === "en") {
if (\App::getLocale() === 'en') {
return 'yyyy-mm-dd';
}
return 'dd.mm.yyyy';
}
public static function formatDateDB()
{
if (\App::getLocale() === "en") {
if (\App::getLocale() === 'en') {
return 'Y-m-d';
}
return 'd.m.Y';
}
public static function formatDateTimeDB()
{
if (\App::getLocale() === "en") {
if (\App::getLocale() === 'en') {
return 'Y-m-d - H:i';
}
return 'd.m.Y - H:i';
}
public static function _format_number($value)
{
return preg_replace("/[^0-9,-]/", "", $value);
// Erlaubt Zahlen, Komma, Punkt und Minus (für Dezimalzahlen in DE und EN Format)
return preg_replace('/[^0-9,.-]/', '', $value);
}
public static function _thousands_separator()
{
return \App::getLocale() === "en" ? ',' : '.';
return \App::getLocale() === 'en' ? ',' : '.';
}
public static function _decimal_separator()
{
return \App::getLocale() === "en" ? '.' : ',';
return \App::getLocale() === 'en' ? '.' : ',';
}
public static function maxStrLength($str, $length = 40)
{
if (strlen($str) > $length) {
$str = substr($str, 0, $length);
//$str = substr($str, 0, strrpos($str, " "));
$str = $str . " ...";
// $str = substr($str, 0, strrpos($str, " "));
$str = $str . ' ...';
}
return $str;
}
public static function reFormatNumber($value)
{
return (float) str_replace(',', '.', self::_format_number($value));
// Wenn bereits ein Float/Int, direkt zurückgeben
if (is_numeric($value) && ! is_string($value)) {
return (float) $value;
}
$value = (string) $value;
// Entferne alle nicht-numerischen Zeichen außer Punkt, Komma und Minus
$value = preg_replace('/[^0-9,.\-]/', '', $value);
if ($value === '') {
return 0.0;
}
// Erkenne das Zahlenformat anhand der Position des letzten Trennzeichens
$lastComma = strrpos($value, ',');
$lastDot = strrpos($value, '.');
if ($lastComma !== false && ($lastDot === false || $lastComma > $lastDot)) {
// Deutsches Format: "1.000,50" -> Punkt entfernen (Tausender), Komma zu Punkt
$value = str_replace('.', '', $value);
$value = str_replace(',', '.', $value);
} else {
// Englisches Format: "1,000.50" -> Komma entfernen (Tausender)
$value = str_replace(',', '', $value);
}
return (float) $value;
}
public static function formatNumber($value, $dec = 2)
{
$value = floatval(str_replace(',', '', $value));
// Wenn der Wert bereits numerisch ist (int/float), direkt formatieren
if (is_numeric($value) && ! is_string($value)) {
return number_format((float) $value, $dec, self::_decimal_separator(), self::_thousands_separator());
}
// Bei String-Eingaben: deutsches Format (mit Komma) zu Float konvertieren
$value = self::reFormatNumber($value);
return number_format($value, $dec, self::_decimal_separator(), self::_thousands_separator());
}
public static function cleanIntegerFromString($value)
{
// Entferne alle nicht-numerischen Zeichen außer Minus
$cleanStr = preg_replace("/[^0-9-]/", "", $value);
$cleanStr = preg_replace('/[^0-9-]/', '', $value);
// Konvertiere zu Integer und entferne führende Nullen
$number = (int)$cleanStr;
$number = (int) $cleanStr;
return $number;
}
public static function cleanNumberFormat($num = 0, $dec = 2, $fullzero = false)
{
if ($fullzero && $num == 0) {
return number_format($num, $dec, self::_decimal_separator(), self::_thousands_separator());
}
return rtrim(rtrim(number_format($num, $dec, self::_decimal_separator(), self::_thousands_separator()), '0'), self::_decimal_separator());
}
public static function utf8ize($mixed)
public static function utf8ize($mixed)
{
if (is_array($mixed)) {
foreach ($mixed as $key => $value) {
$mixed[$key] = self::utf8ize($value);
}
} elseif (is_string($mixed)) {
return mb_convert_encoding($mixed, "UTF-8", "UTF-8");
return mb_convert_encoding($mixed, 'UTF-8', 'UTF-8');
}
return $mixed;
}
public static function getPostRoute()
{
return self::$postRoute;
}
public static function setPostRoute($postRoute)
{
self::$postRoute = $postRoute;
@ -136,9 +174,10 @@ class Util
public static function getUserShop()
{
$shop = session('user_shop');
if (empty($shop) || !is_object($shop)) {
if (empty($shop) || ! is_object($shop)) {
return null;
}
return $shop;
}
@ -148,6 +187,7 @@ class Util
if ($user && $user->shop) {
return $user->shop;
}
return false;
}
@ -158,10 +198,10 @@ class Util
return $auth_user;
}
}
return false;
}
public static function getUserShopIdentifier()
{
if (\Session::has('user_shop_identifier')) {
@ -169,6 +209,7 @@ class Util
return $user_shop_identifier;
}
}
return false;
}
@ -178,6 +219,7 @@ class Util
if ($identifier && \Session::has('user_shop_payment') && \Session::get('user_shop_payment') === 6) {
return OrderPaymentService::getInstanceStatus($identifier);
}
return false;
}
@ -201,6 +243,7 @@ class Util
if (\Session::has('shopping_instance')) {
return \Session::get('shopping_instance');
}
return false;
}
@ -211,6 +254,7 @@ class Util
if ($user_shop_identifier && $auth_user) {
return UserHistory::whereUserId($auth_user->id)->whereIdentifier($user_shop_identifier)->get()->last();
}
return false;
}
@ -229,6 +273,7 @@ class Util
if ($user_history = self::getUserHistory()) {
return $user_history->{$key};
}
return null;
}
@ -239,8 +284,10 @@ class Util
return 'test';
}
}
return config('app.mode');
return config('app.mode');
}
public static function addRoute($p = [])
{
$b = [];
@ -249,6 +296,7 @@ class Util
$b = ['subdomain' => $user_shop->slug];
}
}
return array_merge($p, $b);
}
@ -256,12 +304,14 @@ class Util
{
if (isset($user->account->country_id)) {
//ch schweiz is out
// ch schweiz is out
if ($user->account->country_id === 6) {
return false;
}
return true;
}
return false;
}
@ -272,34 +322,38 @@ class Util
return \Auth::guard('customers')->user()->user_shop_domain;
}
}
return self::getMyMivitaShopUrl();
}
public static function getMyMivitaShopUrl($add_url = "")
public static function getMyMivitaShopUrl($add_url = '')
{
if (\Session::has('user_shop_domain')) {
$url = \Session::get('user_shop_domain') . $add_url;
if (!str_starts_with($url, 'http')) {
if (! str_starts_with($url, 'http')) {
$url = 'https://' . ltrim($url, '/');
}
return $url;
}
//alois sein shop
// alois sein shop
$user = \App\User::find(6);
if ($user && $user->shop) {
return config('app.protocol') . $user->shop->slug . "." . config('app.domain') . config('app.tld_care') . $add_url;
return config('app.protocol') . $user->shop->slug . '.' . config('app.domain') . config('app.tld_care') . $add_url;
}
}
public static function getMyMivitaPortalUrl($protocol = true)
{
$pro = $protocol ? config('app.protocol') : "";
$pro = $protocol ? config('app.protocol') : '';
return $pro . config('app.pre_url_portal') . config('app.domain') . config('app.tld_care');
}
public static function getMyMivitaUrl($protocol = true)
{
$pro = $protocol ? config('app.protocol') : "";
$pro = $protocol ? config('app.protocol') : '';
return $pro . config('app.pre_url_crm') . config('app.domain') . config('app.tld_care');
}
@ -311,10 +365,11 @@ class Util
if (\Session::has('user_shop_payment')) {
return \Session::get('user_shop_payment');
}
return null;
}
public static function getUserShopBackUrl($reference = "")
public static function getUserShopBackUrl($reference = '')
{
if (\Session::has('user_shop')) {
@ -322,9 +377,10 @@ class Util
return \Session::get('user_shop_domain');
}
if ($user_shop = \Session::get('user_shop')) {
return config('app.protocol') . $user_shop->slug . "." . config('app.domain') . config('app.tld_care') . "/back/to/shop/" . $reference;
return config('app.protocol') . $user_shop->slug . '.' . config('app.domain') . config('app.tld_care') . '/back/to/shop/' . $reference;
}
}
return config('app.protocol') . config('app.domain') . config('app.tld_care');
}
@ -337,17 +393,19 @@ class Util
return \Session::get('back_link');
}
if (self::getUserPaymentFor($instance) === 3) {
return \Session::get('user_shop_domain') . "/user/membership";
return \Session::get('user_shop_domain') . '/user/membership';
}
if (self::getUserPaymentFor($instance) === 2) {
return \Session::get('user_shop_domain') . "/user/orders";
return \Session::get('user_shop_domain') . '/user/orders';
}
return \Session::get('user_shop_domain');
}
if ($user_shop = \Session::get('user_shop')) {
return config('app.protocol') . $user_shop->slug . "." . config('app.domain') . config('app.tld_care') . $uri;
return config('app.protocol') . $user_shop->slug . '.' . config('app.domain') . config('app.tld_care') . $uri;
}
}
return config('app.protocol') . config('app.domain') . config('app.tld_care');
}
@ -363,6 +421,7 @@ class Util
if (Request::getHost() === 'naturcosmetic.' . config('app.domain') . config('app.tld_care')) {
return true;
}
return \Config::get('app.url') === config('app.domain') . config('app.tld_shop');
}
@ -372,8 +431,10 @@ class Util
if ($dev && config('app.debug') !== true) {
return false;
}
return true;
}
return false;
}
@ -382,7 +443,7 @@ class Util
if ($size > 0) {
$size = (int) $size;
$base = log($size) / log(1024);
$suffixes = array(' bytes', ' KB', ' MB', ' GB', ' TB');
$suffixes = [' bytes', ' KB', ' MB', ' GB', ' TB'];
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
} else {
@ -390,56 +451,70 @@ class Util
}
}
public static function formatTextWithLineBreaks($text, $translate = false)
{
if ($translate) {
$translated = ['payment.commission_shop', 'payment.commission_payline', 'payment.commission_growth_bonus'];
foreach ($translated as $value) {
// $text = str_replace($key, trans($value), $text);
$text = str_replace($value, __($value), $text);
}
}
return nl2br($text);
}
public static function sanitize($string, $force_lowercase = true, $anal = false, $substr = false)
{
$strip = array(
"~",
"`",
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"_",
"=",
"+",
"[",
"{",
"]",
"}",
"\\",
"|",
";",
":",
"\"",
$strip = [
'~',
'`',
'!',
'@',
'#',
'$',
'%',
'^',
'&',
'*',
'(',
')',
'_',
'=',
'+',
'[',
'{',
']',
'}',
'\\',
'|',
';',
':',
'"',
"'",
"&#8216;",
"&#8217;",
"&#8220;",
"&#8221;",
"&#8211;",
"&#8212;",
"—",
"–",
",",
"<",
".",
">",
"/",
"?"
);
$clean = trim(str_replace($strip, "", strip_tags($string)));
$clean = preg_replace('/\s+/', "_", $clean);
$clean = ($anal) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean;
'&#8216;',
'&#8217;',
'&#8220;',
'&#8221;',
'&#8211;',
'&#8212;',
'—',
'–',
',',
'<',
'.',
'>',
'/',
'?',
];
$clean = trim(str_replace($strip, '', strip_tags($string)));
$clean = preg_replace('/\s+/', '_', $clean);
$clean = ($anal) ? preg_replace('/[^a-zA-Z0-9]/', '', $clean) : $clean;
if ($substr) {
$clean = (strlen($clean) > 20) ? substr($clean, -20) : $clean;
}
return ($force_lowercase) ?
(function_exists('mb_strtolower')) ?
mb_strtolower($clean, 'UTF-8') :

View file

@ -1,98 +1,110 @@
<?php
namespace App\Services;
use App\Services\Shop;
use App\Models\Country;
use App\Models\Product;
use App\Models\ShippingCountry;
use \Gloudemans\Shoppingcart\Cart;
use Illuminate\Support\Collection;
use Gloudemans\Shoppingcart\Cart;
use Gloudemans\Shoppingcart\CartItem;
use Illuminate\Session\SessionManager;
use Illuminate\Contracts\Events\Dispatcher;
use Gloudemans\Shoppingcart\Contracts\Buyable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Session\SessionManager;
use Illuminate\Support\Collection;
class Yard extends Cart
{
private $shipping_price = 0;
private $shipping_price_net = 0;
private $shipping_tax_rate = 0;
private $shipping_tax = 0;
private $shipping_country_id = 0; //default de
private $shipping_is_for;
private $num_comp;
private $user_tax_free;
private $shipping_free;
private $user_reverse_charge;
private $user_country_id;
private $user_country;
private $shopping_data = [];
private $initShippingExtras = false;
private $shipping_price_net = 0;
private $shipping_tax_rate = 0;
private $shipping_tax = 0;
private $shipping_country_id = 0; // default de
private $shipping_is_for;
private $num_comp;
private $user_tax_free;
private $shipping_free;
private $user_reverse_charge;
private $user_country_id;
private $user_country;
private $shopping_data = [];
private $initShippingExtras = false;
public function __construct(SessionManager $session, Dispatcher $events)
{
parent::__construct($session, $events);
}
public function instance($instance = null)
{
parent::instance($instance);
if(!$this->initShippingExtras){
$this->initShippingExtras = true; //erst true, sonst wird es immer wieder aufgerufen
if (! $this->initShippingExtras) {
$this->initShippingExtras = true; // erst true, sonst wird es immer wieder aufgerufen
$this->makeShippingExtras($instance);
}
return $this;
return $this;
}
private function makeShippingExtras($instance){
private function makeShippingExtras($instance)
{
if($this->getYardExtra('shipping_price')){
if ($this->getYardExtra('shipping_price')) {
$this->shipping_price = (float) ($this->getYardExtra('shipping_price'));
}
if($this->getYardExtra('shipping_price_net')){
if ($this->getYardExtra('shipping_price_net')) {
$this->shipping_price_net = (float) ($this->getYardExtra('shipping_price_net'));
}
if($this->getYardExtra('shipping_tax_rate')){
if ($this->getYardExtra('shipping_tax_rate')) {
$this->shipping_tax_rate = (float) ($this->getYardExtra('shipping_tax_rate'));
}
if($this->getYardExtra('shipping_tax')){
if ($this->getYardExtra('shipping_tax')) {
$this->shipping_tax = (float) ($this->getYardExtra('shipping_tax'));
}
if($this->getYardExtra('shipping_country_id')){
$this->shipping_country_id = $this->getYardExtra('shipping_country_id');
if ($this->getYardExtra('shipping_country_id')) {
$this->shipping_country_id = $this->getYardExtra('shipping_country_id');
}
if($this->getYardExtra('shipping_is_for')){
$this->shipping_is_for = $this->getYardExtra('shipping_is_for');
if ($this->getYardExtra('shipping_is_for')) {
$this->shipping_is_for = $this->getYardExtra('shipping_is_for');
}
if($this->getYardExtra('num_comp')){
if ($this->getYardExtra('num_comp')) {
$this->num_comp = $this->getYardExtra('num_comp');
}
if($this->getYardExtra('user_tax_free')){
$this->user_tax_free = $this->getYardExtra('user_tax_free');
if ($this->getYardExtra('user_tax_free')) {
$this->user_tax_free = $this->getYardExtra('user_tax_free');
}
if($this->getYardExtra('shipping_free')){
$this->shipping_free = $this->getYardExtra('shipping_free');
if ($this->getYardExtra('shipping_free')) {
$this->shipping_free = $this->getYardExtra('shipping_free');
}
if($this->getYardExtra('user_reverse_charge')){
$this->user_reverse_charge = $this->getYardExtra('user_reverse_charge');
if ($this->getYardExtra('user_reverse_charge')) {
$this->user_reverse_charge = $this->getYardExtra('user_reverse_charge');
}
if($this->getYardExtra('user_country_id')){
$this->user_country_id = $this->getYardExtra('user_country_id');
if ($this->getYardExtra('user_country_id')) {
$this->user_country_id = $this->getYardExtra('user_country_id');
}
if($this->getYardExtra('user_country')){
$this->user_country = $this->getYardExtra('user_country');
if ($this->getYardExtra('user_country')) {
$this->user_country = $this->getYardExtra('user_country');
}
if(gettype($this->shipping_country_id) !== 'object' && $this->shipping_country_id == 0){
if (gettype($this->shipping_country_id) !== 'object' && $this->shipping_country_id == 0) {
$shippingCountry = ShippingCountry::first();
if($shippingCountry){
if ($shippingCountry) {
$this->shipping_country_id = $shippingCountry->id;
}
}
if($this->shipping_price == 0){
if ($this->shipping_price == 0) {
self::instance($instance)->setShippingCountryWithPrice($this->shipping_country_id, $this->shipping_is_for);
}
}
@ -102,45 +114,52 @@ class Yard extends Cart
return config('cart.tax');
}
public function putYardExtra($key, $value){
public function putYardExtra($key, $value)
{
$content = $this->getYContent();
$content->put($key, $value);
$this->putShippingExtras($content);
//$this->ysession->put($this->yinstance, $content);
// $this->ysession->put($this->yinstance, $content);
}
public function getYardExtra($key){
public function getYardExtra($key)
{
$content = $this->getYContent();
if ($content->has($key)){
if ($content->has($key)) {
return $content->get($key);
}
return false;
}
public function getYContent()
{
return $this->getShippingExtras();
/* if (is_null($this->ysession->get($this->yinstance))) {
return new Collection([]);
}
return $this->ysession->get($this->yinstance);*/
/* if (is_null($this->ysession->get($this->yinstance))) {
return new Collection([]);
}
return $this->ysession->get($this->yinstance);*/
}
public function getShippingCountryName(){
public function getShippingCountryName()
{
$shippingCountry = ShippingCountry::find($this->shipping_country_id);
if($shippingCountry && $shippingCountry->country){
if ($shippingCountry && $shippingCountry->country) {
return $shippingCountry->country->getLocated();
}
return "";
return '';
}
public function getShippingCountryCountryId()
{
$shippingCountry = ShippingCountry::find($this->shipping_country_id);
if($shippingCountry && $shippingCountry->country){
if ($shippingCountry && $shippingCountry->country) {
return $shippingCountry->country->id;
}
return 1; //default DE
return 1; // default DE
}
public function getShippingCountryId()
@ -148,17 +167,13 @@ class Yard extends Cart
return $this->shipping_country_id;
}
public function getShippingPrice()
{
return $this->shipping_price;
}
public function reCalculateShippingPrice(){
public function reCalculateShippingPrice()
{
$this->calculateShippingPrice();
}
@ -171,7 +186,6 @@ class Yard extends Cart
$this->putYardExtra('shipping_is_for', $shipping_is_for);
$this->calculateShippingPrice();
}
public function setUserPriceInfos($user_price_infos = [])
@ -190,14 +204,14 @@ class Yard extends Cart
$this->user_country = Country::findOrFail($user_price_infos['user_country_id']);
$this->putYardExtra('user_country', $this->user_country);
}
public function getUserPriceInfos(){
public function getUserPriceInfos()
{
return [
'user_tax_free' =>$this->user_tax_free,
'user_reverse_charge' =>$this->user_reverse_charge,
'user_country_id' =>$this->user_country_id,
'user_tax_free' => $this->user_tax_free,
'user_reverse_charge' => $this->user_reverse_charge,
'user_country_id' => $this->user_country_id,
'shipping_free' => $this->shipping_free,
];
}
@ -224,61 +238,60 @@ class Yard extends Cart
public function getShippingFreeMissingValue()
{
if($this->shipping_free && $this->total(2, '.', '') < $this->shipping_free){
if ($this->shipping_free && $this->total(2, '.', '') < $this->shipping_free) {
return $this->shipping_free - $this->total(2, '.', '');
}
}
return 0;
}
private function calculateShippingPrice(){
private function calculateShippingPrice()
{
$shippingCountry = ShippingCountry::find($this->shipping_country_id);
if(!$shippingCountry){
if (! $shippingCountry) {
return;
}
$shipping = $shippingCountry->shipping;
$shipping_price = $shipping->shipping_prices->first();
if(!$shipping_price){
if (! $shipping_price) {
return;
}
if($this->weight() == 0){
if ($this->weight() == 0) {
$shipping_price->price = 0;
$shipping_price->price_comp = 0;
}else{
if($this->shipping_free && $this->total(2, '.', '') >= $this->shipping_free){
if($this->weightByFreeShipping() == 0){
} else {
if ($this->shipping_free && $this->total(2, '.', '') >= $this->shipping_free) {
if ($this->weightByFreeShipping() == 0) {
$shipping_price->price = 0;
$shipping_price->price_comp = 0;
}else{
} else {
$shipping_price = $this->shippingPriceByWeight($shipping->shipping_prices, $this->weightByFreeShipping());
}
}else{
} else {
$shipping_price = $this->shippingPriceByWeight($shipping->shipping_prices, $this->weight());
//first by price
//$shipping_price = $this->shippingPriceByTotal($shipping->shipping_prices, $this->total(2, '.', ''));
//sec by weight
//if(!$shipping_price){
//}
// first by price
// $shipping_price = $this->shippingPriceByTotal($shipping->shipping_prices, $this->total(2, '.', ''));
// sec by weight
// if(!$shipping_price){
// }
}
//default
if(!$shipping_price){
// default
if (! $shipping_price) {
$shipping_price = $shipping->shipping_prices->first();
}
}
if($shipping_price){
if ($shipping_price) {
$price = $shipping_price->price;
$this->num_comp = 0; //compensation is checked in Settings
if(Shop::isCompProducts($this->shipping_is_for)){
$this->num_comp = 0; // compensation is checked in Settings
if (Shop::isCompProducts($this->shipping_is_for)) {
$price = $shipping_price->price_comp;
$this->num_comp = $shipping_price->num_comp;
}
$this->shipping_price = $price;
$this->shipping_tax_rate = $shipping_price->tax_rate;
$this->shipping_price_net = round($price / ((100+$shipping_price->tax_rate) / 100), 2);
$this->shipping_tax = round($price / (100+$shipping_price->tax_rate) * 100, 2);
$this->shipping_price_net = round($price / ((100 + $shipping_price->tax_rate) / 100), 2);
$this->shipping_tax = round($price / (100 + $shipping_price->tax_rate) * 100, 2);
$this->putYardExtra('num_comp', $this->num_comp);
$this->putYardExtra('shipping_price', $this->shipping_price);
@ -288,31 +301,36 @@ class Yard extends Cart
}
}
private function shippingPriceByTotal($prices, $total){
foreach ($prices as $price){
if($price->total_from > 0 && $price->total_to > 0){
if($total >= $price->total_from && $total <= $price->total_to){
private function shippingPriceByTotal($prices, $total)
{
foreach ($prices as $price) {
if ($price->total_from > 0 && $price->total_to > 0) {
if ($total >= $price->total_from && $total <= $price->total_to) {
return $price;
}
}
}
return false;
}
private function shippingPriceByWeight($prices, $weight){
foreach ($prices as $price){
if($price->weight_from > 0 && $price->weight_to > 0){
if($weight >= $price->weight_from && $weight <= $price->weight_to){
private function shippingPriceByWeight($prices, $weight)
{
foreach ($prices as $price) {
if ($price->weight_from > 0 && $price->weight_to > 0) {
if ($weight >= $price->weight_from && $weight <= $price->weight_to) {
return $price;
}
}
}
return false;
}
/**
* @param null $decimals
* @param null $decimalPoint
* @param null $thousandSeperator
* @param null $decimals
* @param null $decimalPoint
* @param null $thousandSeperator
* @return string
*/
public function shipping($decimals = null, $decimalPoint = null, $thousandSeperator = null)
@ -324,61 +342,61 @@ class Yard extends Cart
{
return $this->numberFormat($this->shipping_price_net, $decimals, $decimalPoint, $thousandSeperator);
}
//
private function shippingTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
return $this->numberFormat($this->shipping_tax, $decimals, $decimalPoint, $thousandSeperator);
}
/* private function subShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null){
$subShipping = $this->shipping_price_net
return $this->numberFormat($subShipping, $decimals, $decimalPoint, $thousandSeperator);
}*/
//netto
/* private function subShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null){
$subShipping = $this->shipping_price_net
return $this->numberFormat($subShipping, $decimals, $decimalPoint, $thousandSeperator);
}*/
// netto
public function subtotalWithShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$subtotal = (float) $this->shipping_price_net + $this->subtotal(2, '.', '');
return $this->numberFormat($subtotal, $decimals, $decimalPoint, $thousandSeperator);
}
public function taxWithShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if($this->user_tax_free){
if ($this->user_tax_free) {
return $this->numberFormat(0, $decimals, $decimalPoint, $thousandSeperator);
}
$total = $this->totalWithShipping(2, '.', '');
// $totalTax = (float) $this->tax(2, '.', '') + $this->shipping_tax;
$totalTax = $this->subtotalWithShipping(2, '.', '');
$totalTax = $this->subtotalWithShipping(2, '.', '');
return $this->numberFormat(($total - $totalTax), $decimals, $decimalPoint, $thousandSeperator);
}
public function totalWithShipping($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if($this->user_tax_free){
if ($this->user_tax_free) {
$total = (float) ($this->subtotal(2, '.', '')) + $this->shipping_price_net;
}else{
} else {
$total = (float) ($this->total(2, '.', '')) + $this->shipping_price;
}
return $this->numberFormat($total, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the total price of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function weight($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
return $total + ($cartItem->options->weight ? ($cartItem->options->weight*$cartItem->qty) : 0);
return $total + ($cartItem->options->weight ? ($cartItem->options->weight * $cartItem->qty) : 0);
}, 0);
return $total;
@ -388,11 +406,13 @@ class Yard extends Cart
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
if($cartItem->options->no_free_shipping){
return $total + ($cartItem->options->weight ? ($cartItem->options->weight*$cartItem->qty) : 0);
if ($cartItem->options->no_free_shipping) {
return $total + ($cartItem->options->weight ? ($cartItem->options->weight * $cartItem->qty) : 0);
}
return $total;
}, 0);
return $total;
}
@ -400,7 +420,10 @@ class Yard extends Cart
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
return $total + ($cartItem->options->points ? ($cartItem->options->points * $cartItem->qty) : 0);
// Punkte als Float konvertieren (falls als String mit Komma gespeichert)
$points = $cartItem->options->points ? Util::reFormatNumber($cartItem->options->points) : 0;
return $total + ($points * $cartItem->qty);
}, 0);
return $total;
@ -414,17 +437,19 @@ class Yard extends Cart
$comp_count = $content->reduce(function ($comp_count, CartItem $cartItem) {
return $cartItem->options->comp ? $comp_count + 1 : $comp_count;
}, 0);
return $count-$comp_count;
return $count - $comp_count;
}
/**
* Get the total price of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function total($decimals = NULL, $decimalPoint = NULL, $thousandSeperator = NULL, $withFees = true)
public function total($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withFees = true)
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
@ -437,17 +462,18 @@ class Yard extends Cart
/**
* Get the total tax of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function tax($decimals = NULL, $decimalPoint = NULL, $thousandSeperator = NULL, $withFees = true)
public function tax($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withFees = true)
{
$content = $this->getContent();
$tax = $content->reduce(function ($tax, CartItem $cartItem) {
$priceTax = $cartItem->price / (100 + $cartItem->taxRate) * $cartItem->taxRate;
return $tax + ($cartItem->qty * $priceTax);
}, 0);
@ -457,9 +483,9 @@ class Yard extends Cart
/**
* Get the subtotal (total - tax) of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function subtotal($decimals = null, $decimalPoint = null, $thousandSeperator = null)
@ -468,41 +494,43 @@ class Yard extends Cart
$subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) {
$price_net = $cartItem->price / ((100 + $cartItem->taxRate) / 100);
return $subTotal + ($cartItem->qty * $price_net);
}, 0);
return $this->numberFormat($subTotal, $decimals, $decimalPoint, $thousandSeperator);
}
public function getCartItemByProduct($product_id, $set_price='with'){
if($product = Product::find($product_id)) {
$image = "";
public function getCartItemByProduct($product_id, $set_price = 'with')
{
if ($product = Product::find($product_id)) {
$image = '';
if ($product->images->count()) {
$image = $product->images->first()->slug;
}
$price = $product->price;
if($set_price === 'with'){
if ($set_price === 'with') {
$cartItem = $this->getCartItem($product->id, $product->getLang('name'), 1, $price, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
$price = $product->getPriceWith(false, true, $this->getUserCountry());
}
if($set_price === 'withTaxFree'){
if ($set_price === 'withTaxFree') {
$cartItem = $this->getCartItem($product->id, $product->getLang('name'), 1, $price, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]);
$price = $product->getPriceWith($this->getUserTaxFree(), false, $this->getUserCountry());
}
$content = $this->getContent();
if ($content->has($cartItem->rowId)){
if ($content->has($cartItem->rowId)) {
return $content->get($cartItem->rowId);
}
return $cartItem;
}
return null;
return null;
}
public function getCartItem($id, $name = null, $qty = null, $price = null, array $options = []){
public function getCartItem($id, $name = null, $qty = null, $price = null, array $options = [])
{
if ($id instanceof Buyable) {
$cartItem = CartItem::fromBuyable($id, $qty ?: []);
} elseif (is_array($id)) {
@ -510,23 +538,27 @@ class Yard extends Cart
} else {
$cartItem = CartItem::fromAttributes($id, $name, $price, $options);
}
return $cartItem;
}
public function destroy()
{
// $this->ysession->remove($this->yinstance);
// $this->ysession->remove($this->yinstance);
parent::destroy();
}
public function rowPriceNet(CartItem $row, $decimals = null, $decimalPoint = null, $thousandSeperator = null){
$price = round($row->price / ((100 + $row->taxRate) /100), 4);
public function rowPriceNet(CartItem $row, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$price = round($row->price / ((100 + $row->taxRate) / 100), 4);
return $this->numberFormat($price, $decimals, $decimalPoint, $thousandSeperator);
}
public function rowSubtotalNet(CartItem $row, $decimals = null, $decimalPoint = null, $thousandSeperator = null){
$price = round($row->price / ((100 + $row->taxRate) /100), 4);
public function rowSubtotalNet(CartItem $row, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$price = round($row->price / ((100 + $row->taxRate) / 100), 4);
return $this->numberFormat(($price * $row->qty), $decimals, $decimalPoint, $thousandSeperator);
}
@ -540,117 +572,122 @@ class Yard extends Cart
return ($this->user_country && $this->user_country->currency) ? $this->user_country->currency_unit : false;
}
public function convertCurrency($value = 0, $decimals = null, $decimalPoint = null, $thousandSeperator = null){
if($this->isPriceCurrency()){
public function convertCurrency($value = 0, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->isPriceCurrency()) {
$faktor = isset($this->user_country->currency_faktor) ? $this->user_country->currency_faktor : 1;
$value = Util::reFormatNumber($value);
return $this->numberFormat($value, $decimals, $decimalPoint, $thousandSeperator);
}
return '';
return '';
}
public function getCurrencyByKey($key = false, CartItem $row = null, $decimals = null, $decimalPoint = null, $thousandSeperator = null){
if($this->isPriceCurrency()){
public function getCurrencyByKey($key = false, ?CartItem $row = null, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->isPriceCurrency()) {
$rNumber = 0;
$faktor = isset($this->user_country->currency_faktor) ? $this->user_country->currency_faktor : 1;
switch ($key) {
case 'rowPriceNetCurrency':
if($row){
$price = round($row->price / ((100 + $row->taxRate) /100), 4);
if ($row) {
$price = round($row->price / ((100 + $row->taxRate) / 100), 4);
$rNumber = $price * $faktor;
}
break;
case 'rowSubtotalCurrency':
if($row){
$price = round($row->price / ((100 + $row->taxRate) /100), 4);
$rNumber = $price * $faktor * $row->qty;
}
if ($row) {
$price = round($row->price / ((100 + $row->taxRate) / 100), 4);
$rNumber = $price * $faktor * $row->qty;
}
break;
case 'subtotal':
$rNumber = (float) ($this->subtotal(2, '.', '')) * $faktor;
$rNumber = (float) ($this->subtotal(2, '.', '')) * $faktor;
break;
case 'price':
$rNumber = (float) $row->price * $faktor;
break;
case 'shippingNet':
$rNumber = (float) ($this->shippingNet(2, '.', '')) * $faktor;
break;
$rNumber = (float) ($this->shippingNet(2, '.', '')) * $faktor;
break;
case 'subtotalWithShipping':
$rNumber = (float) ($this->subtotalWithShipping(2, '.', '')) * $faktor;
break;
$rNumber = (float) ($this->subtotalWithShipping(2, '.', '')) * $faktor;
break;
case 'taxWithShipping':
$rNumber = (float) ($this->taxWithShipping(2, '.', '')) * $faktor;
break;
$rNumber = (float) ($this->taxWithShipping(2, '.', '')) * $faktor;
break;
case 'totalWithShipping':
$rNumber = (float) ($this->totalWithShipping(2, '.', '')) * $faktor;
break;
break;
case 'total':
$rNumber = (float) ($this->total(2, '.', '')) * $faktor;
break;
break;
case 'shipping':
$rNumber = (float) ($this->shipping(2, '.', '')) * $faktor;
break;
break;
}
return $this->numberFormat($rNumber, $decimals, $decimalPoint, $thousandSeperator);
return $this->numberFormat($rNumber, $decimals, $decimalPoint, $thousandSeperator);
}
return '';
}
public function getNumComp(){
public function getNumComp()
{
return $this->num_comp;
}
public function getCompProductBy($comp, $product_id=false){
public function getCompProductBy($comp, $product_id = false)
{
foreach ($this->content() as $row) {
if($row->options->comp == $comp) {
if ($row->options->comp == $comp) {
return $row->options->product_id;
}
}
return false;
}
public function getContentByOrder(){
public function getContentByOrder()
{
$ret = [];
$comp = [];
foreach ($this->content() as $row) {
if($row->options->comp){
$comp[100+$row->options->comp] = $row;
}else{
foreach ($this->content() as $row) {
if ($row->options->comp) {
$comp[100 + $row->options->comp] = $row;
} else {
$ret[] = $row;
}
}
ksort($comp);
$ret = array_merge($ret, $comp);
return $ret;
}
/**
* Get the Formated number
*
* @param $value
* @param $decimals
* @param $decimalPoint
* @param $thousandSeperator
* @return string
*/
protected function numberFormat($value, $decimals, $decimalPoint, $thousandSeperator)
{
if(is_null($decimals)){
if (is_null($decimals)) {
$decimals = is_null(config('cart.format.decimals')) ? 2 : config('cart.format.decimals');
}
if(is_null($decimalPoint)){
if (is_null($decimalPoint)) {
$decimalPoint = is_null(config('cart.format.decimal_point')) ? '.' : config('cart.format.decimal_point');
}
if(is_null($thousandSeperator)){
if (is_null($thousandSeperator)) {
$thousandSeperator = is_null(config('cart.format.thousand_seperator')) ? ',' : config('cart.format.thousand_seperator');
}
return number_format($value, $decimals, $decimalPoint, $thousandSeperator);
}
public function myStore($identifier, array $eventOptions = [])
{
@ -667,22 +704,19 @@ class Yard extends Cart
$this->getConnection()->table($this->getTableName())->insert([
'identifier' => $identifier,
'instance' => $this->currentInstance(),
'content' => serialize($content)
'content' => serialize($content),
]);
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
], $eventOptions);
// $this->events->dispatch('cart.stored', $eventOptions);
// $this->events->dispatch('cart.stored', $eventOptions);
}
/**
* Restore the cart with the given identifier.
*
* @param mixed $identifier
* @param mixed $identifier
* @return void
*/
}
}