10.April 2026
This commit is contained in:
parent
a00c42e770
commit
f58c709945
208 changed files with 19280 additions and 2914 deletions
|
|
@ -8,12 +8,19 @@ use App\Models\ShoppingPayment;
|
|||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboItem;
|
||||
use App\Models\UserAboItemHistory;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AboHelper
|
||||
{
|
||||
/**
|
||||
* Mindestabstand (Kalendertage) vom Bestell-/Referenzdatum bis zur ersten Abo-Ausführung.
|
||||
*/
|
||||
public const MIN_DAYS_UNTIL_FIRST_ABO_EXECUTION = 10;
|
||||
|
||||
public static $txaction_filter_text = [
|
||||
'paid' => 'paymend_paid',
|
||||
'appointed' => 'paymend_open',
|
||||
|
|
@ -50,9 +57,19 @@ class AboHelper
|
|||
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
|
||||
$user_abo->update(['status' => $status]);
|
||||
if ($user_abo) {
|
||||
// Neuaktivierung nach erfolgreicher Zahlung (z. B. Payone paid): immer wieder auf abo_okay (2),
|
||||
// auch wenn das Abo vorher abo_hold (3) war (z. B. Cron-Zahlung fehlgeschlagen, spaeter bezahlt).
|
||||
if ($paid && (int) $status === 2) {
|
||||
$user_abo->update(['status' => 2]);
|
||||
} elseif ($user_abo->status < 2) {
|
||||
$user_abo->update(['status' => $status]);
|
||||
}
|
||||
}
|
||||
if (! $user_abo) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserAboOrder::where('user_abo_id', $user_abo->id)->where('shopping_order_id', $shopping_order->id)->update(['status' => $status, 'paid' => $paid]);
|
||||
}
|
||||
|
||||
|
|
@ -153,47 +170,111 @@ class AboHelper
|
|||
|
||||
public static function getFirstAboDate($date, $abo_interval)
|
||||
{
|
||||
$nextDate = Carbon::parse($date)->firstOfMonth()->addMonth(1);
|
||||
$nextDate->addDays($abo_interval - 1);
|
||||
$reference = Carbon::parse($date)->startOfDay();
|
||||
$candidate = self::computeFirstAboCandidateWithoutMinDays($reference, (int) $abo_interval);
|
||||
|
||||
return $nextDate->gt($date) ? $nextDate : $nextDate->addMonth(1);
|
||||
while ($reference->diffInDays($candidate->copy()->startOfDay(), true) < self::MIN_DAYS_UNTIL_FIRST_ABO_EXECUTION) {
|
||||
$candidate = self::advanceAboCandidateOneMonth($candidate, (int) $abo_interval);
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kalendertage von $from bis $to (nur Datum, ohne Uhrzeit).
|
||||
* Verhindert Abweichungen, wenn {@see now()} eine Tageszeit hat und Carbon {@see diffInDays} in 24h-Schritten zählt.
|
||||
*/
|
||||
public static function calendarDaysUntil(Carbon|string $from, Carbon $to): int
|
||||
{
|
||||
$start = Carbon::parse($from)->startOfDay();
|
||||
$end = $to->copy()->startOfDay();
|
||||
|
||||
return (int) $start->diffInDays($end, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erste mögliche Ausführung (nächster Monat, gewählter Liefertag) ohne Mindestabstand-Regel.
|
||||
*/
|
||||
private static function computeFirstAboCandidateWithoutMinDays(Carbon $reference, int $aboDayOfMonth): Carbon
|
||||
{
|
||||
$nextDate = $reference->copy()->firstOfMonth()->addMonth(1);
|
||||
$nextDate->addDays($aboDayOfMonth - 1);
|
||||
|
||||
if (! $nextDate->gt($reference)) {
|
||||
$nextDate->addMonth(1);
|
||||
}
|
||||
|
||||
return $nextDate->copy()->startOfDay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gleicher Liefertag im Folgemonat (Monatsende beachten).
|
||||
*/
|
||||
private static function advanceAboCandidateOneMonth(Carbon $candidate, int $aboDayOfMonth): Carbon
|
||||
{
|
||||
$next = $candidate->copy()->addMonthNoOverflow();
|
||||
$dim = $next->daysInMonth;
|
||||
$day = min($aboDayOfMonth, $dim);
|
||||
|
||||
return $next->day($day)->startOfDay();
|
||||
}
|
||||
|
||||
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) {
|
||||
$payment_transaction = $shopping_payment->payment_transactions->last();
|
||||
$order = $shopping_payment->shopping_order;
|
||||
if (! $order || ! $order->is_abo || (int) $order->abo_interval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,
|
||||
'shopping_user_id' => $shopping_payment->shopping_order->shopping_user_id,
|
||||
'email' => $shopping_payment->shopping_order->shopping_user->billing_email,
|
||||
'is_for' => $shopping_payment->shopping_order->shopping_user->is_for,
|
||||
'payone_userid' => $payment_transaction->userid,
|
||||
'clearingtype' => $shopping_payment->clearingtype,
|
||||
'wallettype' => $shopping_payment->wallettype,
|
||||
'carddata' => $shopping_payment->carddata,
|
||||
'amount' => $shopping_payment->amount,
|
||||
// Bereits verknüpft (z. B. Checkout-Erfolgsseite vor Callback) oder wiederholter Aufruf
|
||||
if (UserAboOrder::where('shopping_order_id', $order->id)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aboInterval = (int) ($shopping_payment->abo_interval ?? $order->abo_interval);
|
||||
if ($aboInterval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_transaction = $shopping_payment->payment_transactions->last();
|
||||
$payoneUserId = $payment_transaction ? (int) $payment_transaction->userid : 0;
|
||||
|
||||
// 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' => $order->auth_user_id,
|
||||
'member_id' => $order->member_id,
|
||||
'shopping_user_id' => $order->shopping_user_id,
|
||||
'email' => $order->shopping_user->billing_email,
|
||||
'is_for' => $order->shopping_user->is_for,
|
||||
'payone_userid' => $payoneUserId,
|
||||
'clearingtype' => $shopping_payment->clearingtype,
|
||||
'wallettype' => $shopping_payment->wallettype,
|
||||
'carddata' => $shopping_payment->carddata,
|
||||
'amount' => $shopping_payment->amount,
|
||||
'status' => 1,
|
||||
'abo_interval' => $aboInterval,
|
||||
'start_date' => now(),
|
||||
'last_date' => now(),
|
||||
'next_date' => self::getFirstAboDate(now(), $aboInterval),
|
||||
]);
|
||||
|
||||
if ($user_abo) {
|
||||
self::createAboItems($user_abo, $shopping_payment);
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $user_abo->id,
|
||||
'shopping_order_id' => $shopping_payment->shopping_order_id,
|
||||
'status' => 1,
|
||||
'abo_interval' => $shopping_payment->abo_interval,
|
||||
'start_date' => now(),
|
||||
'last_date' => now(),
|
||||
'next_date' => self::getFirstAboDate(now(), $shopping_payment->abo_interval),
|
||||
]);
|
||||
|
||||
if ($user_abo) {
|
||||
self::createAboItems($user_abo, $shopping_payment);
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $user_abo->id,
|
||||
'shopping_order_id' => $shopping_payment->shopping_order_id,
|
||||
'status' => 1,
|
||||
]);
|
||||
// Payone-Status-URL kann vor dem Checkout-Redirect laufen: dann existierte
|
||||
// noch kein UserAboOrder → Payment::paymentStatusPaidAction → trackAboActivated ohne Wirkung.
|
||||
// Nach Anlage hier erneut versuchen, wenn die Bestellung bereits als bezahlt gilt.
|
||||
$shopping_payment->shopping_order->refresh();
|
||||
if ($shopping_payment->shopping_order->paid) {
|
||||
IncentiveTracker::trackAboActivated($shopping_payment->shopping_order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -214,6 +295,57 @@ class AboHelper
|
|||
AboItemHistoryService::logInitialCreation($user_abo, 'system');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt Abo-Artikel aus der letzten Bestellung mit Positionen wieder her, wenn user_abo_items leer sind
|
||||
* (z. B. manuell angelegtes Abo ohne Checkout-AboItem-Anlage).
|
||||
*/
|
||||
public static function ensureUserAboItemsFromLatestOrder(UserAbo $userAbo): bool
|
||||
{
|
||||
if ($userAbo->user_abo_items()->exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$userAboOrders = $userAbo->user_abo_orders()
|
||||
->orderByDesc('id')
|
||||
->with(['shopping_order.shopping_order_items'])
|
||||
->get();
|
||||
|
||||
$order = null;
|
||||
foreach ($userAboOrders as $link) {
|
||||
$shoppingOrder = $link->shopping_order;
|
||||
if ($shoppingOrder && $shoppingOrder->shopping_order_items->isNotEmpty()) {
|
||||
$order = $shoppingOrder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $order) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($order->shopping_order_items as $item) {
|
||||
UserAboItem::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'product_id' => $item->product_id,
|
||||
'comp' => $item->comp ?? 0,
|
||||
'qty' => $item->qty,
|
||||
'status' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$userAbo->unsetRelation('user_abo_items');
|
||||
|
||||
if (! UserAboItemHistory::query()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->where('is_initial', true)
|
||||
->exists()) {
|
||||
$userAbo->load('user_abo_items');
|
||||
AboItemHistoryService::logInitialCreation($userAbo, 'system');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getTransStatusFilterText()
|
||||
{
|
||||
$ret = [];
|
||||
|
|
@ -287,4 +419,79 @@ class AboHelper
|
|||
|
||||
return array_values(array_unique($teamUserIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Anzahl aktiver Abos pro Monat für ein gegebenes Jahr.
|
||||
* Ein Abo gilt als aktiv in Monat M wenn:
|
||||
* - start_date <= letzter Tag von M
|
||||
* - cancel_date ist NULL oder >= erster Tag von M
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query Basis-Query (gefiltert nach User/Team etc.)
|
||||
* @param int $year Jahr für die Berechnung
|
||||
* @return int[] Array mit 12 Einträgen (Index 0 = Januar, 11 = Dezember)
|
||||
*/
|
||||
/**
|
||||
* Liefert die Abo-Zählung pro Monat für ein Jahr.
|
||||
*
|
||||
* Vergangene Monate → aus DB-Snapshot (eingefroren, unabhängig von Strukturänderungen).
|
||||
* Aktueller Monat → live berechnet.
|
||||
* Zukünftige Monate → null (kein Balken im Chart).
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $liveQuery Basis-Query für den aktuellen Monat
|
||||
* @param string $scope 'ot' | 'team_abos' | 'team_cust_abos'
|
||||
* @param int $userId Eingeloggter Berater
|
||||
* @return array<int, int|null> 12 Einträge (Index 0 = Jan), null = Zukunft
|
||||
*/
|
||||
public static function getMonthlyAboCounts(
|
||||
\Illuminate\Database\Eloquent\Builder $liveQuery,
|
||||
int $year,
|
||||
string $scope,
|
||||
int $userId
|
||||
): array {
|
||||
$data = [];
|
||||
$now = Carbon::now();
|
||||
$currentYear = (int) $now->year;
|
||||
$currentMonth = (int) $now->month;
|
||||
$lastCountableMonth = ($year === $currentYear) ? $currentMonth : 12;
|
||||
|
||||
// Alle vorhandenen Snapshots für diesen User/Scope/Jahr auf einmal laden
|
||||
$snapshots = \App\Models\AboChartSnapshot::where('user_id', $userId)
|
||||
->where('scope', $scope)
|
||||
->where('year', $year)
|
||||
->get()
|
||||
->keyBy('month');
|
||||
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
if ($month > $lastCountableMonth) {
|
||||
$data[] = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$isPastMonth = $year < $currentYear || ($year === $currentYear && $month < $currentMonth);
|
||||
|
||||
if ($isPastMonth && $snapshots->has($month)) {
|
||||
// Eingefroren – aus DB
|
||||
$data[] = $snapshots->get($month)->count;
|
||||
} else {
|
||||
// Aktueller Monat oder noch kein Snapshot → live
|
||||
$startOfMonth = Carbon::create($year, $month, 1)->startOfMonth();
|
||||
$endOfMonth = Carbon::create($year, $month, 1)->endOfMonth();
|
||||
$terminalStatuses = [4, 5];
|
||||
|
||||
$data[] = (clone $liveQuery)
|
||||
->whereDate('start_date', '<=', $endOfMonth)
|
||||
->where(function ($q) use ($startOfMonth, $terminalStatuses) {
|
||||
$q->whereDate('cancel_date', '>=', $startOfMonth)
|
||||
->orWhere(function ($q2) use ($terminalStatuses) {
|
||||
$q2->whereNull('cancel_date')
|
||||
->whereNotIn('status', $terminalStatuses);
|
||||
});
|
||||
})
|
||||
->count();
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ class AboOrderCart
|
|||
]);
|
||||
}
|
||||
|
||||
AboHelper::ensureUserAboItemsFromLatestOrder($user_abo);
|
||||
|
||||
// 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();
|
||||
|
|
|
|||
|
|
@ -1,55 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\BusinessPlan;
|
||||
|
||||
use App\User;
|
||||
use stdClass;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\UserLevel;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Services\TranslationHelper;
|
||||
use App\Models\UserBusinessStructure;
|
||||
|
||||
use App\Models\UserLevel;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use stdClass;
|
||||
|
||||
class BusinessUserItem
|
||||
{
|
||||
public $businessUserItems = [];
|
||||
|
||||
private $date;
|
||||
private $b_user;
|
||||
private $user_level_active_pos;
|
||||
|
||||
|
||||
private $b_user;
|
||||
|
||||
private $user_level_active_pos;
|
||||
|
||||
public function __construct($date)
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function makeUser($user_id){
|
||||
public function makeUser($user_id)
|
||||
{
|
||||
|
||||
//check for user an load is saved
|
||||
// check for user an load is saved
|
||||
$this->b_user = UserBusiness::where('user_id', $user_id)->where('month', $this->date->month)->where('year', $this->date->year)->first();
|
||||
if($this->b_user !== null){
|
||||
if ($this->b_user !== null) {
|
||||
return;
|
||||
}
|
||||
//read User here, can delete in stored data.
|
||||
// read User here, can delete in stored data.
|
||||
$user = User::find($user_id);
|
||||
if(!$user){
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
$user_level_active = $user->user_level ? $user->user_level : null;
|
||||
$this->user_level_active_pos = $user_level_active ? $user_level_active->pos : 0;
|
||||
$this->b_user = new UserBusiness();
|
||||
$this->b_user = new UserBusiness;
|
||||
$fill = [
|
||||
'user_id' => $user->id,
|
||||
'month' => $this->date->month,
|
||||
'year' => $this->date->year,
|
||||
'm_level_id' => $user->m_level,
|
||||
'user_level_name' => $user_level_active ? $user_level_active->name : '',
|
||||
'user_level_name' => $user_level_active ? $user_level_active->name : '',
|
||||
'active_account' => $user->payment_account ? Carbon::parse($user->payment_account)->gt(Carbon::parse($this->date->start_date)) : false,
|
||||
'payment_account_date' => $user->payment_account ? $user->getPaymentAccountDateFormat(false) : NULL,
|
||||
'active_date' => $user->active_date ? $user->active_date : NULL,
|
||||
'payment_account_date' => $user->payment_account ? $user->getPaymentAccountDateFormat(false) : null,
|
||||
'active_date' => $user->active_date ? $user->active_date : null,
|
||||
'm_account' => $user->account->m_account,
|
||||
'email' => $user->email,
|
||||
'first_name' => $user->account->first_name,
|
||||
|
|
@ -61,17 +61,17 @@ class BusinessUserItem
|
|||
'sales_volume_TP_points' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_TP_points'),
|
||||
'sales_volume_points_shop' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_shop'),
|
||||
|
||||
'sales_volume_points_KP_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_KP_sum'), //KP + Shop Points
|
||||
'sales_volume_points_TP_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_TP_sum'), //TP + Shop Points
|
||||
'sales_volume_points_KP_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_KP_sum'), // KP + Shop Points
|
||||
'sales_volume_points_TP_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_TP_sum'), // TP + Shop Points
|
||||
|
||||
'sales_volume_total' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_total'),
|
||||
'sales_volume_total_shop' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_total_shop'),
|
||||
'sales_volume_total_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_total_sum'),
|
||||
|
||||
'margin' => $user_level_active ? $user_level_active->margin : 0, //is fix Rabatt für Kundenbestellungen
|
||||
'margin_shop' => $user_level_active ? $user_level_active->margin_shop : 0, //is fix Rabatt für Shopbestellungen
|
||||
'qual_kp' => $user_level_active ? $user_level_active->qual_kp : 0, //KP Kundenpoints from level
|
||||
'qual_pp' => $user_level_active ? $user_level_active->qual_pp : 0, //PP Payline Points from level
|
||||
'margin' => $user_level_active ? $user_level_active->margin : 0, // is fix Rabatt für Kundenbestellungen
|
||||
'margin_shop' => $user_level_active ? $user_level_active->margin_shop : 0, // is fix Rabatt für Shopbestellungen
|
||||
'qual_kp' => $user_level_active ? $user_level_active->qual_kp : 0, // KP Kundenpoints from level
|
||||
'qual_pp' => $user_level_active ? $user_level_active->qual_pp : 0, // PP Payline Points from level
|
||||
|
||||
'payline_points' => 0,
|
||||
'commission_pp_total' => 0,
|
||||
|
|
@ -83,116 +83,133 @@ class BusinessUserItem
|
|||
$this->b_user->business_lines = [];
|
||||
$this->b_user->user_items = [];
|
||||
$this->b_user->commission_shop_sales = round($this->b_user->sales_volume_total_shop / 100 * $this->b_user->margin_shop, 2);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function getSalesVolumeTotalMargin()
|
||||
{
|
||||
return $this->b_user->getSalesVolumeTotalMargin();
|
||||
}
|
||||
|
||||
public function getSalesVolumeTotalMargin(){
|
||||
return $this->b_user->getSalesVolumeTotalMargin();
|
||||
}
|
||||
|
||||
public function addUserID(){
|
||||
public function addUserID()
|
||||
{
|
||||
TreeCalcBot::addUserID($this->b_user->user_id);
|
||||
}
|
||||
|
||||
public function getBUser(){
|
||||
public function getBUser()
|
||||
{
|
||||
return $this->b_user;
|
||||
}
|
||||
|
||||
public function addBusinessLineToUser($line, $obj){
|
||||
public function addBusinessLineToUser($line, $obj)
|
||||
{
|
||||
$this->b_user->business_lines[$line] = $obj;
|
||||
}
|
||||
|
||||
public function addBusinessLinePoints($line, $points){
|
||||
|
||||
public function addBusinessLinePoints($line, $points)
|
||||
{
|
||||
$obj = $this->business_lines[$line];
|
||||
$obj->points += $points;
|
||||
$this->b_user->business_lines[$line] = $obj;
|
||||
}
|
||||
|
||||
public function addTotalTP($points){
|
||||
public function addTotalTP($points)
|
||||
{
|
||||
$this->b_user->total_pp += $points;
|
||||
}
|
||||
|
||||
public function isQualKP(){
|
||||
|
||||
public function isQualKP()
|
||||
{
|
||||
return ($this->sales_volume_points_KP_sum >= $this->qual_kp) ? true : false;
|
||||
}
|
||||
|
||||
public function isQualLevel(){
|
||||
public function isQualLevel()
|
||||
{
|
||||
return ($this->qual_user_level) ? true : false;
|
||||
}
|
||||
|
||||
public function isQualEqualLevel(){
|
||||
if($this->qual_user_level){
|
||||
public function isQualEqualLevel()
|
||||
{
|
||||
if ($this->qual_user_level) {
|
||||
return ($this->m_level_id == $this->qual_user_level['id']) ? true : false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getQualLevelPaylines(){
|
||||
if($this->qual_user_level){
|
||||
public function getQualLevelPaylines()
|
||||
{
|
||||
if ($this->qual_user_level) {
|
||||
return $this->qual_user_level['paylines'];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function isQualLevelGrowth($line){
|
||||
if(isset($this->business_lines[$line])){
|
||||
public function isQualLevelGrowth($line)
|
||||
{
|
||||
if (isset($this->business_lines[$line])) {
|
||||
$object = $this->business_lines[$line];
|
||||
if(isset($object->growth_bonus)){
|
||||
if (isset($object->growth_bonus)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function getRestQualKP(){
|
||||
public function getRestQualKP()
|
||||
{
|
||||
$ret = $this->sales_volume_points_KP_sum - $this->qual_kp;
|
||||
|
||||
return $ret > 0 ? $ret : 0;
|
||||
}
|
||||
|
||||
public function getCommissionTotal(){
|
||||
public function getCommissionTotal()
|
||||
{
|
||||
return round($this->commission_shop_sales + $this->commission_pp_total + $this->commission_growth_total, 2);
|
||||
}
|
||||
//Provisierungslevel brechnen, Berechnung der Provisionen nach Level
|
||||
public function calcQualPP(){
|
||||
|
||||
//das ist der erreichte Provisierungslevel, nach paylinePoints und KP
|
||||
// Provisierungslevel brechnen, Berechnung der Provisionen nach Level
|
||||
public function calcQualPP()
|
||||
{
|
||||
|
||||
// das ist der erreichte Provisierungslevel, nach paylinePoints und KP
|
||||
$qualUserLevel = $this->calcuQualLevel();
|
||||
if($qualUserLevel !== NULL){
|
||||
//prüfe einen Aufsieg im KarriereLevel
|
||||
if ($qualUserLevel !== null) {
|
||||
// prüfe einen Aufsieg im KarriereLevel
|
||||
$this->setNextUserLevel();
|
||||
$this->b_user->qual_user_level = $qualUserLevel->toArray();
|
||||
//setzen nächsten ProvisionsLevel wenn not isQualEqualLevel
|
||||
// setzen nächsten ProvisionsLevel wenn not isQualEqualLevel
|
||||
$this->setQualNextLevel();
|
||||
//Berechnung der Provisionen in der Payline
|
||||
// Berechnung der Provisionen in der Payline
|
||||
$commission_pp_total = 0;
|
||||
$commission_growth_total = 0;
|
||||
for ($i=1; $i <= $qualUserLevel->paylines ; $i++) {
|
||||
if(isset($this->business_lines[$i])){
|
||||
for ($i = 1; $i <= $qualUserLevel->paylines; $i++) {
|
||||
if (isset($this->business_lines[$i])) {
|
||||
$object = $this->business_lines[$i];
|
||||
$object->margin = $this->qual_user_level['pr_line_'.$i]; //provision in %
|
||||
$object->commission = round($object->points / 100 * $object->margin, 2); //provision in points/euro
|
||||
$object->margin = $this->qual_user_level['pr_line_'.$i]; // provision in %
|
||||
$object->commission = round($object->points / 100 * $object->margin, 2); // provision in points/euro
|
||||
$object->payline = true;
|
||||
$commission_pp_total += $object->commission;
|
||||
$this->b_user->business_lines[$i] = $object;
|
||||
$this->b_user->business_lines[$i] = $object;
|
||||
}
|
||||
}
|
||||
//Berechnung der Provisionen nach WB
|
||||
if($qualUserLevel->growth_bonus){
|
||||
//['growth_bonus'] //
|
||||
// Berechnung der Provisionen nach WB
|
||||
if ($qualUserLevel->growth_bonus) {
|
||||
// ['growth_bonus'] //
|
||||
$payline = (int) $this->b_user->qual_user_level['paylines'] + 1;
|
||||
$maxlines = count($this->business_lines) + 1;
|
||||
$growth_bonus = (float) $this->b_user->qual_user_level['growth_bonus'];
|
||||
|
||||
for ($i=$payline; $i <= $maxlines ; $i++) {
|
||||
if(isset($this->business_lines[$i])){
|
||||
|
||||
for ($i = $payline; $i <= $maxlines; $i++) {
|
||||
if (isset($this->business_lines[$i])) {
|
||||
$object = $this->business_lines[$i];
|
||||
$object->margin = $growth_bonus; //provision in %
|
||||
$object->commission = round($object->points / 100 * $object->margin, 2); //provision in points/euro
|
||||
$object->margin = $growth_bonus; // provision in %
|
||||
$object->commission = round($object->points / 100 * $object->margin, 2); // provision in points/euro
|
||||
$object->growth_bonus = true;
|
||||
$commission_growth_total += $object->commission;
|
||||
$this->b_user->business_lines[$i] = $object;
|
||||
$this->b_user->business_lines[$i] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,73 +217,78 @@ class BusinessUserItem
|
|||
$this->b_user->commission_pp_total = $commission_pp_total;
|
||||
$this->b_user->commission_growth_total = $commission_growth_total;
|
||||
|
||||
}else{
|
||||
//erste Provisierungslevel als next setzen, hat keine oder wenig points
|
||||
} else {
|
||||
// erste Provisierungslevel als next setzen, hat keine oder wenig points
|
||||
$qualUserLevelNext = UserLevel::where('pos', '=', 1)->orderBy('qual_pp', 'asc')->first();
|
||||
if($qualUserLevelNext){
|
||||
if ($qualUserLevelNext) {
|
||||
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints)
|
||||
public function calcuQualLevel(){
|
||||
//alle levels wo die qual_kp erreicht ist, sortiert nach Rang >
|
||||
$qualUserLevels = UserLevel::where('qual_kp', '<=', $this->sales_volume_points_KP_sum)->where('pos', '<=', $this->user_level_active_pos)->orderBy('qual_pp', 'desc')->get();
|
||||
foreach($qualUserLevels as $qualUserLevel){
|
||||
//brechnet die Points nach der Payline
|
||||
// qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints)
|
||||
public function calcuQualLevel()
|
||||
{
|
||||
// alle levels wo die qual_kp erreicht ist, sortiert nach Rang >
|
||||
$qualUserLevels = UserLevel::where('qual_kp', '<=', $this->sales_volume_points_KP_sum)->where('pos', '<=', $this->user_level_active_pos)->orderBy('qual_pp', 'desc')->get();
|
||||
foreach ($qualUserLevels as $qualUserLevel) {
|
||||
// brechnet die Points nach der Payline
|
||||
$payline_points = $this->getPointsforPayline($qualUserLevel->paylines);
|
||||
$payline_points_qual_kp = $payline_points + $this->getRestQualKP();
|
||||
if($payline_points_qual_kp >= $qualUserLevel->qual_pp){
|
||||
//match payline_points erreicht, ist der akutelle Level für die Provision
|
||||
if ($payline_points_qual_kp >= $qualUserLevel->qual_pp) {
|
||||
// match payline_points erreicht, ist der akutelle Level für die Provision
|
||||
$this->b_user->payline_points = $payline_points;
|
||||
$this->b_user->payline_points_qual_kp = $payline_points_qual_kp;
|
||||
|
||||
return $qualUserLevel;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// PaylinePoints nach paylines / welche ebenen gezählt werden, 3,4,5,6 ...
|
||||
private function getPointsforPayline($paylines){
|
||||
private function getPointsforPayline($paylines)
|
||||
{
|
||||
$payline_points = 0;
|
||||
for ($i=1; $i <= $paylines ; $i++) {
|
||||
if(isset($this->business_lines[$i])){
|
||||
for ($i = 1; $i <= $paylines; $i++) {
|
||||
if (isset($this->business_lines[$i])) {
|
||||
$payline_points += $this->business_lines[$i]->points;
|
||||
}
|
||||
}
|
||||
|
||||
return $payline_points;
|
||||
}
|
||||
//wenn nicht erreicht, was wäre der nächste Provisionslevel?
|
||||
private function setQualNextLevel(){
|
||||
if(!$this->isQualEqualLevel()){
|
||||
|
||||
// wenn nicht erreicht, was wäre der nächste Provisionslevel?
|
||||
private function setQualNextLevel()
|
||||
{
|
||||
if (! $this->isQualEqualLevel()) {
|
||||
$qualUserLevelNext = UserLevel::where('id', '=', $this->b_user->qual_user_level['next_id'])->orderBy('qual_pp', 'asc')->first();
|
||||
if($qualUserLevelNext){
|
||||
if ($qualUserLevelNext) {
|
||||
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function setNextUserLevel(){
|
||||
// $this->b_user->payline_points_qual_kp // das sind die Payline Points + Rest KP
|
||||
//$this->b_user->total_qual_pp = $this->total_pp + $this->getRestQualKP(); //hier werden alle Linien TP gezähle
|
||||
//$this->b_user->total_qual_pp = $this->total_pp + $this->getRestQualKP(); //hier werden alle Linien TP gezähle
|
||||
|
||||
$nextQualUserLevel = UserLevel::where('qual_pp', '<=', $this->payline_points_qual_kp)->where('pos', '>', $this->user_level_active_pos)->orderBy('qual_pp', 'desc')->first();
|
||||
if($nextQualUserLevel && $this->isQualKP()){
|
||||
$this->b_user->next_qual_user_level = $nextQualUserLevel->toArray();
|
||||
}else{
|
||||
//wenn nicht erreicht, was wäre der nächste Karrierelevel?
|
||||
$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();
|
||||
}
|
||||
private function setNextUserLevel()
|
||||
{
|
||||
// $this->b_user->payline_points_qual_kp // das sind die Payline Points + Rest KP
|
||||
// $this->b_user->total_qual_pp = $this->total_pp + $this->getRestQualKP(); //hier werden alle Linien TP gezähle
|
||||
// $this->b_user->total_qual_pp = $this->total_pp + $this->getRestQualKP(); //hier werden alle Linien TP gezähle
|
||||
|
||||
$nextQualUserLevel = UserLevel::where('qual_pp', '<=', $this->payline_points_qual_kp)->where('pos', '>', $this->user_level_active_pos)->orderBy('qual_pp', 'desc')->first();
|
||||
if ($nextQualUserLevel && $this->isQualKP()) {
|
||||
$this->b_user->next_qual_user_level = $nextQualUserLevel->toArray();
|
||||
} else {
|
||||
// wenn nicht erreicht, was wäre der nächste Karrierelevel?
|
||||
$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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*public function storeUser(){
|
||||
|
|
@ -286,75 +308,81 @@ class BusinessUserItem
|
|||
$obj->line = $line;
|
||||
$obj->points = $userItem->sales_volume_points_sum;
|
||||
$obj->parents = $temp;
|
||||
$ret[] = $obj;
|
||||
$ret[] = $obj;
|
||||
}
|
||||
return $ret;
|
||||
}*/
|
||||
|
||||
public function readParentsBusinessUsers(){
|
||||
|
||||
$users = User::with('account')->select('users.*')
|
||||
->where('users.deleted_at', '=', null)
|
||||
->where('users.id', '!=', 1)
|
||||
->where('users.admin', "<", 4)
|
||||
->where('users.m_level', "!=", null)
|
||||
->where('users.m_sponsor', "=", $this->b_user->user_id) //<- need the id for parents / sponsors
|
||||
->where('users.payment_account', "!=", null)
|
||||
->where('users.active_date', "<=", $this->date->end_date) // wurde in dem Monat freigeschaltet
|
||||
->get();
|
||||
public function readParentsBusinessUsers()
|
||||
{
|
||||
|
||||
if($users){
|
||||
foreach($users as $user){
|
||||
$BusinessUserItem = new BusinessUserItem($this->date);
|
||||
$BusinessUserItem->makeUser($user->id);
|
||||
$BusinessUserItem->addUserID();
|
||||
$this->businessUserItems[] = $BusinessUserItem;
|
||||
}
|
||||
}
|
||||
foreach($this->businessUserItems as $businessUserItem){
|
||||
$businessUserItem->readParentsBusinessUsers();
|
||||
$users = User::with('account')->select('users.*')
|
||||
->where('users.deleted_at', '=', null)
|
||||
->where('users.id', '!=', 1)
|
||||
->where('users.admin', '<', 4)
|
||||
->where('users.m_level', '!=', null)
|
||||
->whereColumn('users.id', '!=', 'users.m_sponsor')
|
||||
->where('users.m_sponsor', '=', $this->b_user->user_id) // <- need the id for parents / sponsors
|
||||
->where('users.payment_account', '!=', null)
|
||||
->where('users.active_date', '<=', $this->date->end_date) // wurde in dem Monat freigeschaltet
|
||||
->get();
|
||||
|
||||
if ($users) {
|
||||
foreach ($users as $user) {
|
||||
$BusinessUserItem = new BusinessUserItem($this->date);
|
||||
$BusinessUserItem->makeUser($user->id);
|
||||
$BusinessUserItem->addUserID();
|
||||
$this->businessUserItems[] = $BusinessUserItem;
|
||||
}
|
||||
}
|
||||
foreach ($this->businessUserItems as $businessUserItem) {
|
||||
$businessUserItem->readParentsBusinessUsers();
|
||||
}
|
||||
}
|
||||
|
||||
public function readStoredParentsBusinessUsers($structure){
|
||||
public function readStoredParentsBusinessUsers($structure)
|
||||
{
|
||||
|
||||
$parents = $this->findParentsBusinessOnStored($this->b_user->user_id, $structure);
|
||||
if($parents){
|
||||
foreach($parents as $obj){
|
||||
if ($parents) {
|
||||
foreach ($parents as $obj) {
|
||||
$BusinessUserItem = new BusinessUserItem($this->date);
|
||||
$BusinessUserItem->makeUser($obj->user_id);
|
||||
$BusinessUserItem->addUserID();
|
||||
$this->businessUserItems[] = $BusinessUserItem;
|
||||
$this->businessUserItems[] = $BusinessUserItem;
|
||||
}
|
||||
foreach($this->businessUserItems as $businessUserItem){
|
||||
foreach ($this->businessUserItems as $businessUserItem) {
|
||||
$businessUserItem->readStoredParentsBusinessUsers($parents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function findParentsBusinessOnStored($user_id, $structures){
|
||||
if($structures){
|
||||
foreach($structures as $obj){
|
||||
if($user_id === $obj->user_id){
|
||||
private function findParentsBusinessOnStored($user_id, $structures)
|
||||
{
|
||||
if ($structures) {
|
||||
foreach ($structures as $obj) {
|
||||
if ($user_id === $obj->user_id) {
|
||||
return $obj->parents;
|
||||
}
|
||||
if($obj->parents){
|
||||
if($ret = $this->findParentsBusinessOnStored($user_id, $obj->parents)){
|
||||
if ($obj->parents) {
|
||||
if ($ret = $this->findParentsBusinessOnStored($user_id, $obj->parents)) {
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function checkSponsor($user){
|
||||
public function checkSponsor($user)
|
||||
{
|
||||
|
||||
//check is store? has ID
|
||||
if($this->b_user->isSave()){
|
||||
// check is store? has ID
|
||||
if ($this->b_user->isSave()) {
|
||||
return;
|
||||
}
|
||||
$sponsor = new stdClass();
|
||||
$sponsor = new stdClass;
|
||||
|
||||
$sponsor->is_sponsor = false;
|
||||
$sponsor->user_id = false;
|
||||
|
|
@ -364,34 +392,36 @@ class BusinessUserItem
|
|||
$sponsor->m_account = '';
|
||||
$sponsor->full_name = 'Keinen Sponsor zugewiesen';
|
||||
|
||||
if($user->m_sponsor){
|
||||
if ($user->m_sponsor) {
|
||||
|
||||
if($user->user_sponsor){
|
||||
if ($user->user_sponsor) {
|
||||
$sponsor->is_sponsor = true;
|
||||
$sponsor->user_id = $user->user_sponsor->id;
|
||||
if($user->user_sponsor->account){
|
||||
if ($user->user_sponsor->account) {
|
||||
$sponsor->full_name = substr('Sponsor: '.$user->user_sponsor->account->first_name.' '.$user->user_sponsor->account->last_name.' | '.$user->user_sponsor->email.' | '.$user->user_sponsor->account->m_account, 0, 250);
|
||||
$sponsor->first_name = $user->user_sponsor->account->last_name;
|
||||
$sponsor->last_name = $user->user_sponsor->account->first_name;
|
||||
$sponsor->m_account = $user->user_sponsor->account->m_account;
|
||||
}else{
|
||||
} else {
|
||||
$sponsor->full_name = 'Sponsor: '.$user->user_sponsor->email;
|
||||
}
|
||||
$sponsor->email = $user->user_sponsor->email;
|
||||
}else{
|
||||
$sponsor->full_name = 'Sponsor wurde gelöscht.';
|
||||
} else {
|
||||
$sponsor->full_name = 'Sponsor wurde gelöscht.';
|
||||
}
|
||||
}
|
||||
$this->b_user->sponsor = $sponsor;
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
public function isSave(){
|
||||
public function isSave()
|
||||
{
|
||||
return $this->b_user->isSave();
|
||||
}
|
||||
|
||||
public function __get($property) {
|
||||
if($this->b_user === null){
|
||||
public function __get($property)
|
||||
{
|
||||
if ($this->b_user === null) {
|
||||
return null;
|
||||
}
|
||||
if (property_exists($this->b_user, $property)) {
|
||||
|
|
@ -400,6 +430,5 @@ class BusinessUserItem
|
|||
if (isset($this->b_user->{$property})) {
|
||||
return $this->b_user->{$property};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,20 @@
|
|||
|
||||
namespace App\Services\BusinessPlan;
|
||||
|
||||
use App\User;
|
||||
use stdClass;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\UserLevel;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Models\UserAccount;
|
||||
|
||||
use App\Models\UserBusiness;
|
||||
use App\Models\UserLevel;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Optimierte Version der BusinessUserItem Klasse
|
||||
*
|
||||
*
|
||||
* Hauptverbesserungen:
|
||||
* - makeUserFromModel() für bereits geladene User-Objekte
|
||||
* - Bessere Error-Behandlung mit Logging
|
||||
* - Bessere Error-Behandlung mit Logging
|
||||
* - Optimierte Datenbankzugriffe durch Relations-Nutzung
|
||||
* - Input-Validierung und Boundary-Checks
|
||||
*/
|
||||
|
|
@ -25,10 +24,15 @@ class BusinessUserItemOptimized
|
|||
public $businessUserItems = [];
|
||||
|
||||
private $date;
|
||||
|
||||
private $b_user;
|
||||
|
||||
private ?TreeCalcBotOptimized $treeCalcBot = null;
|
||||
|
||||
private $user_level_active_pos;
|
||||
|
||||
private $needsQualificationRecalculation = false;
|
||||
|
||||
private $qualificationCalculated = false;
|
||||
|
||||
public function __construct($date, ?TreeCalcBotOptimized $treeCalcBot = null)
|
||||
|
|
@ -36,6 +40,7 @@ class BusinessUserItemOptimized
|
|||
$this->date = $date;
|
||||
$this->treeCalcBot = $treeCalcBot;
|
||||
$this->businessUserItems = []; // Initialize array
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -44,18 +49,17 @@ class BusinessUserItemOptimized
|
|||
return $this->qualificationCalculated;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Erstellt BusinessUser aus User-ID (Original-Methode für Rückwärtskompatibilität)
|
||||
*
|
||||
* @param int $user_id Die User-ID
|
||||
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
|
||||
*
|
||||
* @param int $user_id Die User-ID
|
||||
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
|
||||
*/
|
||||
public function makeUser($user_id, bool $forceLiveCalculation = false): void
|
||||
{
|
||||
try {
|
||||
// Prüfe nur nach gespeicherten Daten, wenn keine Live-Berechnung erzwungen wird
|
||||
if (!$forceLiveCalculation) {
|
||||
if (! $forceLiveCalculation) {
|
||||
$this->b_user = UserBusiness::where('user_id', $user_id)
|
||||
->where('month', $this->date->month)
|
||||
->where('year', $this->date->year)
|
||||
|
|
@ -85,8 +89,9 @@ class BusinessUserItemOptimized
|
|||
// Lade User mit Relations (weniger effizient als makeUserFromModel)
|
||||
$user = User::with(['account', 'user_level'])->find($user_id);
|
||||
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
\Log::warning("BusinessUserItem: User not found: {$user_id}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +103,7 @@ class BusinessUserItemOptimized
|
|||
$this->calcQualPP();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("BusinessUserItem: Error creating user {$user_id}: " . $e->getMessage());
|
||||
\Log::error("BusinessUserItem: Error creating user {$user_id}: ".$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
|
@ -106,20 +111,20 @@ class BusinessUserItemOptimized
|
|||
/**
|
||||
* NEUE OPTIMIERTE METHODE: Erstellt BusinessUser aus bereits geladenem User-Objekt
|
||||
* Konsistent zur ursprünglichen makeUser Logik - prüft explizit nach bereits berechneten Daten
|
||||
*
|
||||
* @param User $user Das User-Model
|
||||
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
|
||||
*
|
||||
* @param User $user Das User-Model
|
||||
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
|
||||
*/
|
||||
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) {
|
||||
if (! $user || ! $user->id) {
|
||||
throw new \InvalidArgumentException('Invalid user model provided');
|
||||
}
|
||||
|
||||
// Prüfe nur nach gespeicherten Daten, wenn keine Live-Berechnung erzwungen wird
|
||||
if (!$forceLiveCalculation) {
|
||||
if (! $forceLiveCalculation) {
|
||||
$this->b_user = UserBusiness::where('user_id', $user->id)
|
||||
->where('month', $this->date->month)
|
||||
->where('year', $this->date->year)
|
||||
|
|
@ -147,10 +152,10 @@ class BusinessUserItemOptimized
|
|||
// 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("BusinessUserItemOptimized: Error creating user from model {$user->id}: " . $e->getMessage());
|
||||
\Log::error("BusinessUserItemOptimized: Error creating user from model {$user->id}: ".$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
|
@ -170,7 +175,7 @@ class BusinessUserItemOptimized
|
|||
$this->user_level_active_pos = $user_level_active ? $user_level_active->pos : 0;
|
||||
|
||||
// Neues UserBusiness Objekt erstellen
|
||||
$this->b_user = new UserBusiness();
|
||||
$this->b_user = new UserBusiness;
|
||||
|
||||
// Account-Daten (mit intelligentem Laden und Error-Handling)
|
||||
$account = $this->getAccountForUser($user);
|
||||
|
|
@ -208,7 +213,7 @@ 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,
|
||||
'active_growth_bonus' => $user_level_active ? (float) $user_level_active->growth_bonus : 0,
|
||||
'growth_bonus_details' => null,
|
||||
|
||||
// Initialisierung
|
||||
|
|
@ -230,7 +235,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));
|
||||
\Log::debug('BusinessUserItemOptimized: b_user: '.json_encode($this->b_user));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -270,7 +275,7 @@ class BusinessUserItemOptimized
|
|||
|
||||
\Log::debug("BusinessUserItem: Enriched stored data for user {$user->id} with current user model data");
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("BusinessUserItem: Error enriching stored data for user {$user->id}: " . $e->getMessage());
|
||||
\Log::error("BusinessUserItem: Error enriching stored data for user {$user->id}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,12 +294,12 @@ class BusinessUserItemOptimized
|
|||
'sales_volume_points_TP_sum',
|
||||
'sales_volume_total',
|
||||
'sales_volume_total_shop',
|
||||
'sales_volume_total_sum'
|
||||
'sales_volume_total_sum',
|
||||
];
|
||||
|
||||
$needsUpdate = false;
|
||||
foreach ($fieldsToUpdate as $field) {
|
||||
if (!isset($this->b_user->{$field}) || $this->b_user->{$field} === null || $this->b_user->{$field} === 0) {
|
||||
if (! isset($this->b_user->{$field}) || $this->b_user->{$field} === null || $this->b_user->{$field} === 0) {
|
||||
$newValue = $this->getUserSalesVolumeOptimized($user, $field);
|
||||
$this->b_user->{$field} = $newValue;
|
||||
|
||||
|
|
@ -306,7 +311,7 @@ class BusinessUserItemOptimized
|
|||
}
|
||||
|
||||
// Aktualisiere Shop Commission falls nötig
|
||||
if (!isset($this->b_user->commission_shop_sales) || $this->b_user->commission_shop_sales === 0) {
|
||||
if (! isset($this->b_user->commission_shop_sales) || $this->b_user->commission_shop_sales === 0) {
|
||||
$shopVolume = (float) $this->b_user->sales_volume_total_shop;
|
||||
$shopMargin = (float) $this->b_user->margin_shop;
|
||||
|
||||
|
|
@ -322,7 +327,7 @@ class BusinessUserItemOptimized
|
|||
\Log::info("BusinessUserItem: Updated sales volume fields for user {$user->id}");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("BusinessUserItem: Error updating sales volume fields for user {$user->id}: " . $e->getMessage());
|
||||
\Log::error("BusinessUserItem: Error updating sales volume fields for user {$user->id}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,18 +339,18 @@ class BusinessUserItemOptimized
|
|||
{
|
||||
try {
|
||||
// Prüfe ob Level-Qualifikationsdaten vorhanden sind
|
||||
$hasNextQual = !empty($this->b_user->next_qual_user_level);
|
||||
$hasNextCan = !empty($this->b_user->next_can_user_level);
|
||||
$hasQualUserLevel = !empty($this->b_user->qual_user_level);
|
||||
$hasNextQual = ! empty($this->b_user->next_qual_user_level);
|
||||
$hasNextCan = ! empty($this->b_user->next_can_user_level);
|
||||
$hasQualUserLevel = ! empty($this->b_user->qual_user_level);
|
||||
// Wenn Level-Qualifikationsdaten fehlen, führe Neuberechnung durch
|
||||
if (!$hasNextQual && !$hasNextCan && !$hasQualUserLevel) {
|
||||
if (! $hasNextQual && ! $hasNextCan && ! $hasQualUserLevel) {
|
||||
\Log::debug("BusinessUserItem: Level qualification data missing for user {$this->b_user->user_id}, triggering recalculation");
|
||||
|
||||
// Setze Flag für notwendige Neuberechnung
|
||||
$this->needsQualificationRecalculation = true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning("BusinessUserItem: Error validating level qualification data for user {$this->b_user->user_id}: " . $e->getMessage());
|
||||
\Log::warning("BusinessUserItem: Error validating level qualification data for user {$this->b_user->user_id}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,14 +360,15 @@ class BusinessUserItemOptimized
|
|||
private function calculateActiveAccount(User $user): bool
|
||||
{
|
||||
try {
|
||||
if (!$user->payment_account) {
|
||||
if (! $user->payment_account) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verwende aktuelles Datum, nicht das Berechnungs-Startdatum
|
||||
return Carbon::parse($user->payment_account)->gt(Carbon::now());
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning("BusinessUserItem: Error calculating active account for user {$user->id}: " . $e->getMessage());
|
||||
\Log::warning("BusinessUserItem: Error calculating active account for user {$user->id}: ".$e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -378,12 +384,12 @@ class BusinessUserItemOptimized
|
|||
|
||||
// Log nur bei ersten Aufruf für diesen User (Performance)
|
||||
static $loggedUsers = [];
|
||||
if (!isset($loggedUsers[$user->id])) {
|
||||
if (! isset($loggedUsers[$user->id])) {
|
||||
$loggedUsers[$user->id] = true;
|
||||
|
||||
// Prüfe ob UserSalesVolume Daten existieren
|
||||
$userSalesVolume = $user->getUserSalesVolume($this->date->month, $this->date->year, 'first');
|
||||
if (!$userSalesVolume) {
|
||||
if (! $userSalesVolume) {
|
||||
\Log::info("BusinessUserItem: No UserSalesVolume found for user {$user->id} in {$this->date->month}/{$this->date->year}");
|
||||
|
||||
// Prüfe neueste verfügbare Daten
|
||||
|
|
@ -404,7 +410,8 @@ class BusinessUserItemOptimized
|
|||
|
||||
return $value;
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("BusinessUserItem: Error getting sales volume {$field} for user {$user->id}: " . $e->getMessage());
|
||||
\Log::error("BusinessUserItem: Error getting sales volume {$field} for user {$user->id}: ".$e->getMessage());
|
||||
|
||||
return 0; // Sicherer Fallback
|
||||
}
|
||||
}
|
||||
|
|
@ -422,7 +429,7 @@ class BusinessUserItemOptimized
|
|||
$this->treeCalcBot->addProcessedUserId($this->b_user->user_id);
|
||||
} else {
|
||||
// Fallback für Rückwärtskompatibilität - sollte in Logs sichtbar sein
|
||||
\Log::warning("BusinessUserItemOptimized: TreeCalcBotOptimized Referenz fehlt für User ID: " . $this->b_user->user_id);
|
||||
\Log::warning('BusinessUserItemOptimized: TreeCalcBotOptimized Referenz fehlt für User ID: '.$this->b_user->user_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +448,7 @@ class BusinessUserItemOptimized
|
|||
*/
|
||||
public function initBusinessLines(): void
|
||||
{
|
||||
if (!isset($this->b_user->business_lines) || !is_array($this->b_user->business_lines)) {
|
||||
if (! isset($this->b_user->business_lines) || ! is_array($this->b_user->business_lines)) {
|
||||
$this->b_user->business_lines = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -456,8 +463,9 @@ class BusinessUserItemOptimized
|
|||
|
||||
public function addBusinessLinePoints($line, $points)
|
||||
{
|
||||
if (!isset($this->b_user->business_lines[$line])) {
|
||||
if (! isset($this->b_user->business_lines[$line])) {
|
||||
\Log::warning("BusinessUserItem: Trying to add points to non-existent line {$line}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -468,7 +476,7 @@ class BusinessUserItemOptimized
|
|||
$obj['points'] = ($obj['points'] ?? 0) + (float) $points;
|
||||
} else {
|
||||
// Ensure it's an object
|
||||
if (!is_object($obj)) {
|
||||
if (! is_object($obj)) {
|
||||
$obj = (object) $obj;
|
||||
}
|
||||
$obj->points = ($obj->points ?? 0) + (float) $points;
|
||||
|
|
@ -490,18 +498,19 @@ class BusinessUserItemOptimized
|
|||
return [];
|
||||
}
|
||||
|
||||
if (!$this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) {
|
||||
if (! $this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$calculator = new GrowthBonusCalculator();
|
||||
$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());
|
||||
\Log::error('BusinessUserItem: Error getting growth bonus breakdown: '.$e->getMessage());
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -519,12 +528,12 @@ class BusinessUserItemOptimized
|
|||
return [];
|
||||
}
|
||||
|
||||
if (!$this->isQualLevel() || empty($this->b_user->qual_user_level['growth_bonus'])) {
|
||||
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 (! 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();
|
||||
}
|
||||
|
|
@ -538,13 +547,14 @@ class BusinessUserItemOptimized
|
|||
}
|
||||
|
||||
try {
|
||||
$calculator = new GrowthBonusCalculator();
|
||||
$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());
|
||||
\Log::error('BusinessUserItem: Error getting growth bonus matrix: '.$e->getMessage());
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -556,12 +566,12 @@ class BusinessUserItemOptimized
|
|||
|
||||
public function isQualKP(): bool
|
||||
{
|
||||
return ($this->b_user->sales_volume_points_KP_sum >= $this->b_user->qual_kp);
|
||||
return $this->b_user->sales_volume_points_KP_sum >= $this->b_user->qual_kp;
|
||||
}
|
||||
|
||||
public function isQualLevel(): bool
|
||||
{
|
||||
return !empty($this->b_user->qual_user_level);
|
||||
return ! empty($this->b_user->qual_user_level);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -587,15 +597,15 @@ class BusinessUserItemOptimized
|
|||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -627,23 +637,26 @@ class BusinessUserItemOptimized
|
|||
|
||||
public function isQualEqualLevel(): bool
|
||||
{
|
||||
if (!$this->b_user->qual_user_level) {
|
||||
if (! $this->b_user->qual_user_level) {
|
||||
return false;
|
||||
}
|
||||
return ($this->b_user->m_level_id == $this->b_user->qual_user_level['id']);
|
||||
|
||||
return $this->b_user->m_level_id == $this->b_user->qual_user_level['id'];
|
||||
}
|
||||
|
||||
public function getQualPaylines(): int
|
||||
{
|
||||
if (!$this->b_user->qual_user_level) {
|
||||
if (! $this->b_user->qual_user_level) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) $this->b_user->qual_user_level['paylines'];
|
||||
}
|
||||
|
||||
public function getRestQualKP(): float
|
||||
{
|
||||
$ret = $this->b_user->sales_volume_points_KP_sum - $this->b_user->qual_kp;
|
||||
|
||||
return max(0, $ret); // Boundary-Check
|
||||
}
|
||||
|
||||
|
|
@ -661,7 +674,7 @@ class BusinessUserItemOptimized
|
|||
|
||||
public function calcQualPP($force = false): void
|
||||
{
|
||||
if ($this->qualificationCalculated && !$force) {
|
||||
if ($this->qualificationCalculated && ! $force) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -670,9 +683,9 @@ class BusinessUserItemOptimized
|
|||
|
||||
try {
|
||||
$qualUserLevel = $this->calcuQualLevel();
|
||||
\Log::debug("BusinessUserItemOptimized: calcQualPP for user {$this->b_user->user_id}: " . json_encode($qualUserLevel));
|
||||
\Log::debug("BusinessUserItemOptimized: calcQualPP for user {$this->b_user->user_id}: ".json_encode($qualUserLevel));
|
||||
if ($qualUserLevel !== null) {
|
||||
//das erreichte level setzen
|
||||
// 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
|
||||
|
|
@ -681,17 +694,17 @@ class BusinessUserItemOptimized
|
|||
|
||||
\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
|
||||
// next_qual_user_level nächster qualifizierten level
|
||||
$this->setNextUserLevel($force);
|
||||
//qual_user_level_next nächste Provisions-Stufe,
|
||||
// qual_user_level_next nächste Provisions-Stufe,
|
||||
$this->setQualNextLevel($force);
|
||||
//provisionen berechnen
|
||||
// provisionen berechnen
|
||||
$this->calculateCommissions($qualUserLevel);
|
||||
} else {
|
||||
$this->setFirstQualLevel();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("BusinessUserItem: Error calculating qualifications for user {$this->b_user->user_id}: " . $e->getMessage());
|
||||
\Log::error("BusinessUserItem: Error calculating qualifications for user {$this->b_user->user_id}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -708,7 +721,7 @@ class BusinessUserItemOptimized
|
|||
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];
|
||||
$margin = (float) $this->b_user->qual_user_level['pr_line_'.$i];
|
||||
|
||||
// Handle both array and object types (JSON deserialization inconsistency)
|
||||
if (is_array($object)) {
|
||||
|
|
@ -730,7 +743,7 @@ class BusinessUserItemOptimized
|
|||
}
|
||||
|
||||
// Growth Bonus
|
||||
if (!empty($qualUserLevel->growth_bonus)) {
|
||||
if (! empty($qualUserLevel->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);
|
||||
|
|
@ -741,10 +754,9 @@ class BusinessUserItemOptimized
|
|||
} else {
|
||||
// Neue Logik ab Dezember 2025 - delegated to new Calculator service
|
||||
try {
|
||||
$growthCalculator = new GrowthBonusCalculator();
|
||||
$growthCalculator = new GrowthBonusCalculator;
|
||||
$commission_growth_total = $growthCalculator->calculate($this, $qualUserLevel);
|
||||
|
||||
|
||||
// Calculate matrix details for storage and total sum
|
||||
// This ensures that the stored details match the calculated total exactly
|
||||
$matrixDetails = $growthCalculator->getMatrixDetails($this, $qualUserLevel);
|
||||
|
|
@ -752,7 +764,7 @@ class BusinessUserItemOptimized
|
|||
// 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());
|
||||
\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;
|
||||
|
|
@ -789,7 +801,7 @@ class BusinessUserItemOptimized
|
|||
$object['growth_bonus'] = true;
|
||||
$commission_growth_total += $object['commission'];
|
||||
} else {
|
||||
if (!is_object($object)) {
|
||||
if (! is_object($object)) {
|
||||
$object = (object) $object;
|
||||
}
|
||||
$points = (float) ($object->points ?? 0);
|
||||
|
|
@ -824,13 +836,12 @@ class BusinessUserItemOptimized
|
|||
foreach ($qualUserLevels as $qualUserLevel) {
|
||||
// Berechne die Payline-Punkte für die spezifischen Paylines dieses Levels
|
||||
$payline_points = $this->getPointsforPayline($qualUserLevel->paylines);
|
||||
\Log::debug("BusinessUserItemOptimized: payline_points: " . $payline_points);
|
||||
\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
|
||||
|
|
@ -848,12 +859,13 @@ class BusinessUserItemOptimized
|
|||
}
|
||||
|
||||
\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));
|
||||
\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])) {
|
||||
|
|
@ -867,18 +879,20 @@ class BusinessUserItemOptimized
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $payline_points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt das nächste Provision-Level
|
||||
* Wenn das aktuelle Level nicht erreicht ist, dann wird bei aktuelle Provisions-Stufe die erreichte level angezeigt und berechnet
|
||||
* Zur Info wird das nächste level angezeigt, der folgt, sonst leer
|
||||
* Zur Info wird das nächste level angezeigt, der folgt, sonst leer
|
||||
*/
|
||||
private function setQualNextLevel($force = false): void
|
||||
{
|
||||
//ist der level nicht das aktuelle level, dann sucht es den nächsten level
|
||||
//isQualEqualLevel wenn das erreichte level das akutelle user level ist.
|
||||
if (!$this->isQualEqualLevel() && $this->b_user->qual_user_level['next_id'] != null) {
|
||||
// ist der level nicht das aktuelle level, dann sucht es den nächsten level
|
||||
// isQualEqualLevel wenn das erreichte level das akutelle user level ist.
|
||||
if (! $this->isQualEqualLevel() && $this->b_user->qual_user_level['next_id'] != null) {
|
||||
$qualUserLevelNext = UserLevel::where('id', '=', $this->b_user->qual_user_level['next_id'])
|
||||
->orderBy('qual_pp', 'asc')
|
||||
->first();
|
||||
|
|
@ -910,10 +924,11 @@ class BusinessUserItemOptimized
|
|||
->first();
|
||||
|
||||
// Wenn kein nächster Level existiert, beende
|
||||
if (!$nextLevel) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -935,6 +950,7 @@ class BusinessUserItemOptimized
|
|||
$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;
|
||||
}
|
||||
|
||||
|
|
@ -982,7 +998,7 @@ class BusinessUserItemOptimized
|
|||
'sales_volume_points_KP_sum' => 'sales_volume_points_KP_sum',
|
||||
'sales_volume_points_TP_sum' => 'sales_volume_points_TP_sum',
|
||||
'business_lines' => 'business_lines',
|
||||
'user_id' => 'user_id'
|
||||
'user_id' => 'user_id',
|
||||
];
|
||||
|
||||
if (isset($legacyMap[$name]) && isset($this->b_user->{$legacyMap[$name]})) {
|
||||
|
|
@ -1003,7 +1019,7 @@ class BusinessUserItemOptimized
|
|||
return;
|
||||
}
|
||||
|
||||
$sponsor = new stdClass();
|
||||
$sponsor = new stdClass;
|
||||
$sponsor->is_sponsor = false;
|
||||
$sponsor->user_id = false;
|
||||
$sponsor->first_name = '';
|
||||
|
|
@ -1019,9 +1035,9 @@ class BusinessUserItemOptimized
|
|||
|
||||
if ($user->user_sponsor->account) {
|
||||
$sponsor->full_name = substr(
|
||||
'Sponsor: ' . $user->user_sponsor->account->first_name . ' ' .
|
||||
$user->user_sponsor->account->last_name . ' | ' .
|
||||
$user->user_sponsor->email . ' | ' .
|
||||
'Sponsor: '.$user->user_sponsor->account->first_name.' '.
|
||||
$user->user_sponsor->account->last_name.' | '.
|
||||
$user->user_sponsor->email.' | '.
|
||||
$user->user_sponsor->account->m_account,
|
||||
0,
|
||||
250
|
||||
|
|
@ -1030,7 +1046,7 @@ class BusinessUserItemOptimized
|
|||
$sponsor->last_name = $user->user_sponsor->account->last_name;
|
||||
$sponsor->m_account = $user->user_sponsor->account->m_account;
|
||||
} else {
|
||||
$sponsor->full_name = 'Sponsor: ' . $user->user_sponsor->email;
|
||||
$sponsor->full_name = 'Sponsor: '.$user->user_sponsor->email;
|
||||
}
|
||||
$sponsor->email = $user->user_sponsor->email;
|
||||
} else {
|
||||
|
|
@ -1040,7 +1056,7 @@ class BusinessUserItemOptimized
|
|||
|
||||
$this->b_user->sponsor = $sponsor;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("BusinessUserItem: Error checking sponsor for user {$user->id}: " . $e->getMessage());
|
||||
Log::error("BusinessUserItem: Error checking sponsor for user {$user->id}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1054,6 +1070,7 @@ class BusinessUserItemOptimized
|
|||
$maxDepth = 20;
|
||||
if ($depth > $maxDepth) {
|
||||
Log::warning("BusinessUserItem: Maximale Rekursionstiefe ({$maxDepth}) erreicht für User {$this->b_user->user_id}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1065,6 +1082,7 @@ class BusinessUserItemOptimized
|
|||
->where('users.id', '!=', 1)
|
||||
->where('users.admin', '<', 4)
|
||||
->where('users.m_level', '!=', null)
|
||||
->whereColumn('users.id', '!=', 'users.m_sponsor')
|
||||
->where('users.m_sponsor', '=', $this->b_user->user_id)
|
||||
->where('users.payment_account', '!=', null)
|
||||
->where('users.active_date', '<=', $this->date->end_date)
|
||||
|
|
@ -1075,6 +1093,7 @@ class BusinessUserItemOptimized
|
|||
// KRITISCHER BUGFIX: Prüfe ob User bereits verarbeitet wurde
|
||||
if ($this->treeCalcBot && $this->treeCalcBot->isUserProcessed($user->id)) {
|
||||
Log::debug("BusinessUserItem: Überspringe bereits verarbeiteten User {$user->id} (zirkuläre Referenz verhindert)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1090,7 +1109,7 @@ class BusinessUserItemOptimized
|
|||
$businessUserItem->readParentsBusinessUsers($forceLiveCalculation, $depth + 1);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("BusinessUserItem: Error reading parent users for {$this->b_user->user_id} at depth {$depth}: " . $e->getMessage());
|
||||
Log::error("BusinessUserItem: Error reading parent users for {$this->b_user->user_id} at depth {$depth}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1104,6 +1123,7 @@ class BusinessUserItemOptimized
|
|||
$maxDepth = 50;
|
||||
if ($depth > $maxDepth) {
|
||||
Log::warning("BusinessUserItem: Maximale Rekursionstiefe ({$maxDepth}) erreicht für gespeicherte User {$this->b_user->user_id}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1115,6 +1135,7 @@ class BusinessUserItemOptimized
|
|||
// KRITISCHER BUGFIX: Prüfe ob User bereits verarbeitet wurde
|
||||
if ($this->treeCalcBot && $this->treeCalcBot->isUserProcessed($obj->user_id)) {
|
||||
Log::debug("BusinessUserItem: Überspringe bereits verarbeiteten gespeicherten User {$obj->user_id} (zirkuläre Referenz verhindert)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1129,7 +1150,7 @@ class BusinessUserItemOptimized
|
|||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("BusinessUserItem: Error reading stored parent users at depth {$depth}: " . $e->getMessage());
|
||||
Log::error("BusinessUserItem: Error reading stored parent users at depth {$depth}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1138,7 +1159,7 @@ class BusinessUserItemOptimized
|
|||
*/
|
||||
private function findParentsBusinessOnStored($user_id, $structures)
|
||||
{
|
||||
if (!$structures) {
|
||||
if (! $structures) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1147,7 +1168,7 @@ class BusinessUserItemOptimized
|
|||
return $obj->parents ?? null;
|
||||
}
|
||||
|
||||
if (!empty($obj->parents)) {
|
||||
if (! empty($obj->parents)) {
|
||||
$result = $this->findParentsBusinessOnStored($user_id, $obj->parents);
|
||||
if ($result) {
|
||||
return $result;
|
||||
|
|
@ -1175,6 +1196,7 @@ class BusinessUserItemOptimized
|
|||
if ($this->b_user && isset($this->b_user->qual_user_level) && $this->b_user->qual_user_level) {
|
||||
return $this->b_user->qual_user_level['paylines'] ?? 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1189,6 +1211,7 @@ class BusinessUserItemOptimized
|
|||
return $object->growth_bonus > 0;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1204,13 +1227,15 @@ class BusinessUserItemOptimized
|
|||
$account = $user->account;
|
||||
if ($account instanceof UserAccount) {
|
||||
\Log::debug("BusinessUserItem: Using pre-loaded account for user {$user->id}");
|
||||
|
||||
return $account;
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn User keine account_id hat, gibt es definitiv kein Account
|
||||
if (!$user->account_id) {
|
||||
if (! $user->account_id) {
|
||||
\Log::info("BusinessUserItem: User {$user->id} has no account_id - no account available");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1218,15 +1243,18 @@ class BusinessUserItemOptimized
|
|||
\Log::info("BusinessUserItem: Loading account for user {$user->id} (account_id: {$user->account_id})");
|
||||
$account = UserAccount::find($user->account_id);
|
||||
|
||||
if (!$account) {
|
||||
if (! $account) {
|
||||
\Log::warning("BusinessUserItem: Account {$user->account_id} not found for user {$user->id}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
\Log::debug("BusinessUserItem: Successfully loaded account {$account->id} for user {$user->id}");
|
||||
|
||||
return $account;
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("BusinessUserItem: Error loading account for user {$user->id}: " . $e->getMessage());
|
||||
\Log::error("BusinessUserItem: Error loading account for user {$user->id}: ".$e->getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Services\BusinessPlan;
|
|||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\Services\Util;
|
||||
use App\User;
|
||||
use stdClass;
|
||||
|
|
@ -122,7 +123,7 @@ class SalesPointsVolume
|
|||
}
|
||||
}
|
||||
|
||||
public static function addSalesPointsVolumeUser(ShoppingOrder $shoppingOrder)
|
||||
public static function User(ShoppingOrder $shoppingOrder)
|
||||
{
|
||||
|
||||
/*
|
||||
|
|
@ -311,6 +312,9 @@ class SalesPointsVolume
|
|||
// Neuberechnung für aktuellen Monat
|
||||
self::reCalculateSalesPointsVolume($original_sales_volume->user_id, $month, $year);
|
||||
|
||||
// Incentive: Track storno
|
||||
IncentiveTracker::trackStorno($original_sales_volume, $cancellation_sales_volume);
|
||||
|
||||
\Log::info('Punktekorrektur für Stornorechnung durchgeführt', [
|
||||
'original_invoice_id' => $original_sales_volume->user_invoice_id,
|
||||
'cancellation_invoice_id' => $cancellation_invoice_id,
|
||||
|
|
|
|||
|
|
@ -1,108 +1,111 @@
|
|||
<?php
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tag;
|
||||
use App\Models\DcTag;
|
||||
use App\Models\FileTag;
|
||||
use App\Models\DcFileTag;
|
||||
use App\Models\DcTag;
|
||||
|
||||
class DcHelper {
|
||||
|
||||
|
||||
class DcHelper
|
||||
{
|
||||
public static $points;
|
||||
|
||||
public static function getTransChange(){
|
||||
|
||||
$langs = [
|
||||
'de' => ['name' => 'German', 'script' => 'Latn', 'native' => 'Deutsch', 'regional' => 'de_DE'],
|
||||
'en' => ['name' => 'English', 'script' => 'Latn', 'native' => 'English', 'regional' => 'en_GB'],
|
||||
'es' => ['name' => 'Spanish', 'script' => 'Latn', 'native' => 'español', 'regional' => 'es_ES'],
|
||||
public static function getTransChange()
|
||||
{
|
||||
|
||||
$langs = [
|
||||
'de' => ['name' => 'German', 'script' => 'Latn', 'native' => 'Deutsch', 'regional' => 'de_DE'],
|
||||
'en' => ['name' => 'English', 'script' => 'Latn', 'native' => 'English', 'regional' => 'en_GB'],
|
||||
'es' => ['name' => 'Spanish', 'script' => 'Latn', 'native' => 'español', 'regional' => 'es_ES'],
|
||||
];
|
||||
|
||||
|
||||
$ret = [];
|
||||
foreach($langs as $code => $lang){
|
||||
$ret[strtolower($code)] = strtolower($lang['native']);
|
||||
foreach ($langs as $code => $lang) {
|
||||
$ret[strtolower($code)] = strtolower($lang['native']);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public static function makeNestableList($category_id){
|
||||
public static function makeNestableList($category_id)
|
||||
{
|
||||
$tags = DcTag::where('category_id', $category_id)->orderBy('pos')->get();
|
||||
$out = "";
|
||||
foreach ($tags as $tag){
|
||||
|
||||
$out .= '<li class="dd-item" data-id="'.$tag->id.'">
|
||||
<span class="pull-right">
|
||||
<a href="#" class="btn btn-sm mt-1 nestable_update_btn" data-action="update-tag-active" data-target="self" data-id="'.$tag->id.'" data-url="'.route('admin_downloadcenter_item_store', ['update_ajax']).'">
|
||||
' . ($tag->active ? '<i class="fa fa-eye text-success"></i>' : '<i class="fa fa-eye-slash text-danger"></i>') . '
|
||||
$out = '';
|
||||
foreach ($tags as $tag) {
|
||||
|
||||
$out .= '<li class="dd-item" data-id="'.$tag->id.'">
|
||||
<div style="display: inline-block;">
|
||||
<div class="ml-4">
|
||||
<a href="#" class="btn btn-sm mt-1 mt-0 nestable_update_btn" data-action="update-tag-active" data-target="self" data-id="'.$tag->id.'" data-url="'.route('admin_downloadcenter_item_store', ['update_ajax']).'">
|
||||
'.($tag->active ? '<i class="fa fa-eye text-success"></i>' : '<i class="fa fa-eye-slash text-danger"></i>').'
|
||||
</a>
|
||||
<a href="'.route('admin_downloadcenter_item_delete', ['obj' => 'tag', 'id'=> $tag->id]).'" class="btn btn-sm mt-1 nestable_list_delete"><i class="fa fa-trash text-danger"></i></a>
|
||||
</span>
|
||||
<a href="'.route('admin_downloadcenter_item_delete', ['obj' => 'tag', 'id' => $tag->id]).'" class="btn btn-sm mt-1 mt-0 nestable_list_delete"><i class="fa fa-trash text-danger"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dd-handle">
|
||||
'.$tag->name.'
|
||||
</div>
|
||||
</li>';
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function makeNestableListCheckbox($category_id, $file_id)
|
||||
{
|
||||
|
||||
public static function makeNestableListCheckbox($category_id, $file_id){
|
||||
|
||||
$tags = DcTag::where('category_id', $category_id)->orderBy('pos')->get();
|
||||
$file_tags = DcFileTag::where('file_id', $file_id)->get();
|
||||
|
||||
$search = array();
|
||||
|
||||
$search = [];
|
||||
foreach ($file_tags as $file_tag) {
|
||||
$search[] = $file_tag->tag_id;
|
||||
}
|
||||
|
||||
$out = "";
|
||||
foreach ($tags as $tag){
|
||||
$out .= '<li class="dd-item" data-id="'.$tag->id.'">
|
||||
|
||||
$out = '';
|
||||
foreach ($tags as $tag) {
|
||||
$out .= '<li class="dd-item" data-id="'.$tag->id.'">
|
||||
<div class="dd-handle dd-nodrag">
|
||||
<label class="custom-control custom-checkbox m-0" for="nestable_check_'.$tag->id.'">
|
||||
<input type="checkbox" class="custom-control-input" name="nestable_check[]" id="nestable_check_'.$tag->id.'" value="'.$tag->id.'" '.(array_search($tag->id, $search) !== FALSE ? 'checked="checked"' : '').'>
|
||||
<input type="checkbox" class="custom-control-input" name="nestable_check[]" id="nestable_check_'.$tag->id.'" value="'.$tag->id.'" '.(array_search($tag->id, $search) !== false ? 'checked="checked"' : '').'>
|
||||
<span class="custom-control-label"> '.$tag->name.' </span>
|
||||
</label>
|
||||
</div>
|
||||
</li>';
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function makeFilterList($filter_list, $split = false, $chunk = false){
|
||||
public static function makeFilterList($filter_list, $split = false, $chunk = false)
|
||||
{
|
||||
|
||||
$out = "";
|
||||
$out = '';
|
||||
$splitOn = 0;
|
||||
if($split){
|
||||
if ($split) {
|
||||
$count = count($filter_list);
|
||||
if($count > 0){
|
||||
if ($count > 0) {
|
||||
$splitOn = intval(ceil($count / $split));
|
||||
$filter_chunk = array_chunk($filter_list, $splitOn, true);
|
||||
$filter_list = $filter_chunk[$chunk];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach($filter_list as $category_id => $value){
|
||||
|
||||
foreach ($filter_list as $category_id => $value) {
|
||||
$out .= '<label class="form-label" for="category_'.$category_id.'">'.$value['name'].'</label>';
|
||||
$out .= '<select class="selectpicker category-filter" name="categories['.$category_id.'][]" id="category_'.$category_id.'" data-style="btn-light" data-live-search="true" multiple>';
|
||||
foreach($value['items'] as $tag){
|
||||
foreach ($value['items'] as $tag) {
|
||||
$out .= '<option value="'.$tag->id.'">'.$tag->name.' ('.$tag->count.')</option>';
|
||||
|
||||
}
|
||||
$out .= '</select>';
|
||||
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function getAttributesOptions($ids = array(), $all = true){
|
||||
$ret = "";
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private function getAttributesOptions($ids = [], $all = true)
|
||||
{
|
||||
$ret = '';
|
||||
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,9 @@ class HTMLHelper
|
|||
foreach ($values as $value) {
|
||||
$attr = ($value == $default) ? 'selected="selected"' : '';
|
||||
$str = self::getAboStrLang($value);
|
||||
$ret .= '<option value="'.$value.'" '.$attr.'>'.$str.'</option>\n';
|
||||
$nextDate = AboHelper::getFirstAboDate(now(), $value);
|
||||
$daysUntil = AboHelper::calendarDaysUntil(now(), $nextDate);
|
||||
$ret .= '<option value="'.$value.'" data-days="'.$daysUntil.'" data-date="'.$nextDate->format('d.m.Y').'" '.$attr.'>'.$str.'</option>\n';
|
||||
}
|
||||
|
||||
return $ret;
|
||||
|
|
|
|||
54
app/Services/Incentive/IncentiveCalculationService.php
Normal file
54
app/Services/Incentive/IncentiveCalculationService.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Incentive;
|
||||
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class IncentiveCalculationService
|
||||
{
|
||||
/**
|
||||
* Full recalculation of an incentive (batch / cron / manual).
|
||||
* Normal: Neuberechnung aus Tracking-Tabellen + Log.
|
||||
* Force: Kompletter Neuaufbau aus Quelldaten (Users, UserAbos, UserSalesVolumes).
|
||||
*/
|
||||
public function recalculate(Incentive $incentive, bool $force = false): array
|
||||
{
|
||||
$stats = ['participants' => 0, 'errors' => 0];
|
||||
|
||||
$participants = $incentive->participants()->with('user')->get();
|
||||
|
||||
foreach ($participants as $participant) {
|
||||
try {
|
||||
$this->recalculateParticipant($participant, $force);
|
||||
$stats['participants']++;
|
||||
} catch (\Throwable $e) {
|
||||
$stats['errors']++;
|
||||
Log::error('IncentiveCalculation error for participant '.$participant->id.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
IncentiveTracker::updateRanking($incentive);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate a single participant.
|
||||
* Force: Kompletter Neuaufbau aus Quelldaten.
|
||||
* Normal: Neuberechnung aus vorhandenen Tracking-Tabellen + Log.
|
||||
*/
|
||||
public function recalculateParticipant(IncentiveParticipant $participant, bool $force = false): void
|
||||
{
|
||||
if (! $participant->user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($force) {
|
||||
$participant->rebuildFromSourceTables()->save();
|
||||
} else {
|
||||
$participant->recalculateFromTrackingTables()->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
353
app/Services/Incentive/IncentivePointsLogRepairService.php
Normal file
353
app/Services/Incentive/IncentivePointsLogRepairService.php
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Incentive;
|
||||
|
||||
use App\Models\IncentiveNewAbo;
|
||||
use App\Models\IncentiveNewPartner;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\IncentivePointsLog;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
|
||||
class IncentivePointsLogRepairService
|
||||
{
|
||||
/**
|
||||
* Fehlende Neupartner-Tracking-Zeilen anlegen (Starterpaket / gleiche Regeln wie Neuaufbau A).
|
||||
* Nutzt IncentiveTracker::trackNewPartner, wenn eine qualifizierte Bestellung existiert.
|
||||
*
|
||||
* @return int Anzahl nachgezogener Partner-Trackings fuer diesen Teilnehmer
|
||||
*/
|
||||
public function syncMissingTrackingPartners(IncentiveParticipant $participant): int
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$added = 0;
|
||||
|
||||
$candidates = User::query()
|
||||
->where('m_sponsor', $participant->user_id)
|
||||
->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$incentive->qualification_end->copy()->endOfDay(),
|
||||
])
|
||||
->whereHas('shopping_orders', function ($q) {
|
||||
$q->wherePaidRegistrationIncludesStarterKit();
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($candidates as $partner) {
|
||||
if (IncentiveNewPartner::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_id', $partner->id)
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$order = ShoppingOrder::query()
|
||||
->where('auth_user_id', $partner->id)
|
||||
->wherePaidRegistrationIncludesStarterKit()
|
||||
->orderBy('id')
|
||||
->first();
|
||||
|
||||
if ($order) {
|
||||
IncentiveTracker::trackNewPartner($order);
|
||||
}
|
||||
|
||||
if (! IncentiveNewPartner::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_id', $partner->id)
|
||||
->exists()) {
|
||||
$newPartner = IncentiveNewPartner::create([
|
||||
'participant_id' => $participant->id,
|
||||
'user_id' => $partner->id,
|
||||
'registered_at' => $partner->created_at,
|
||||
]);
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $participant->id,
|
||||
'type' => 'partner',
|
||||
'source_type' => User::class,
|
||||
'source_id' => $partner->id,
|
||||
'source_label' => $partner->getFullName() ?: $partner->email ?: ('User #'.$partner->id),
|
||||
'month' => $partner->created_at->month,
|
||||
'year' => $partner->created_at->year,
|
||||
'points_onetime' => $incentive->points_partner_onetime,
|
||||
'points_accumulated' => 0,
|
||||
'incentive_new_partner_id' => $newPartner->id,
|
||||
]);
|
||||
}
|
||||
|
||||
if (IncentiveNewPartner::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_id', $partner->id)
|
||||
->exists()) {
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
|
||||
return $added;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fehlende Abo-Tracking-Zeilen anlegen (Kundenabo ot / Eigenabo me, wie IncentiveTracker).
|
||||
* Zuerst trackAboActivated ueber Erstbestellung; ohne UserAboOrder manuell wie Neuaufbau B.
|
||||
*
|
||||
* @return int Anzahl nachgezogener Abo-Trackings fuer diesen Teilnehmer
|
||||
*/
|
||||
public function syncMissingTrackingAbos(IncentiveParticipant $participant): int
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$added = 0;
|
||||
|
||||
$qualEnd = $incentive->qualification_end->copy()->endOfDay();
|
||||
|
||||
$candidatesOt = UserAbo::query()
|
||||
->where('is_for', 'ot')
|
||||
->where('status', 2)
|
||||
->where('member_id', $participant->user_id)
|
||||
->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$qualEnd,
|
||||
])
|
||||
->get();
|
||||
|
||||
$candidatesMe = UserAbo::query()
|
||||
->where('is_for', 'me')
|
||||
->where('status', 2)
|
||||
->where('user_id', $participant->user_id)
|
||||
->where(function ($q) use ($incentive, $qualEnd) {
|
||||
$q->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$qualEnd,
|
||||
])->orWhere('created_at', '<', $incentive->qualification_start);
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($candidatesOt->concat($candidatesMe) as $userAbo) {
|
||||
if (IncentiveNewAbo::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$order = UserAboOrder::query()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->orderBy('id')
|
||||
->with('shopping_order')
|
||||
->first();
|
||||
|
||||
$shoppingOrder = $order?->shopping_order;
|
||||
|
||||
if ($shoppingOrder) {
|
||||
IncentiveTracker::trackAboActivated($shoppingOrder);
|
||||
}
|
||||
|
||||
if (! IncentiveNewAbo::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->exists()) {
|
||||
$qualStart = $incentive->qualification_start->copy()->startOfDay();
|
||||
$activatedAt = $userAbo->created_at;
|
||||
$logMonth = (int) $userAbo->created_at->month;
|
||||
$logYear = (int) $userAbo->created_at->year;
|
||||
|
||||
if ($userAbo->is_for === 'me' && $userAbo->created_at->lt($qualStart)) {
|
||||
$activatedAt = $qualStart->copy();
|
||||
$logMonth = (int) $qualStart->month;
|
||||
$logYear = (int) $qualStart->year;
|
||||
}
|
||||
|
||||
$newAbo = IncentiveNewAbo::create([
|
||||
'participant_id' => $participant->id,
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'activated_at' => $activatedAt,
|
||||
]);
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $participant->id,
|
||||
'type' => 'abo',
|
||||
'source_type' => UserAbo::class,
|
||||
'source_id' => $userAbo->id,
|
||||
'source_label' => $userAbo->email ?: ('Abo #'.$userAbo->id),
|
||||
'month' => $logMonth,
|
||||
'year' => $logYear,
|
||||
'points_onetime' => $incentive->points_abo_onetime,
|
||||
'points_accumulated' => 0,
|
||||
'incentive_new_abo_id' => $newAbo->id,
|
||||
]);
|
||||
}
|
||||
|
||||
if (IncentiveNewAbo::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->exists()) {
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
|
||||
return $added;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt fehlende incentive_new_partner_id / incentive_new_abo_id an bestehenden Log-Zeilen.
|
||||
*
|
||||
* @return array{partner_fk: int, abo_fk: int, onetime_partner_fk: int, onetime_abo_fk: int}
|
||||
*/
|
||||
public function repairForeignKeys(IncentiveParticipant $participant): array
|
||||
{
|
||||
$stats = [
|
||||
'partner_fk' => 0,
|
||||
'abo_fk' => 0,
|
||||
'onetime_partner_fk' => 0,
|
||||
'onetime_abo_fk' => 0,
|
||||
];
|
||||
|
||||
$newPartnerByUserId = $participant->newPartners()->get()->keyBy('user_id');
|
||||
$newAboByUserAboId = $participant->newAbos()->get()->keyBy('user_abo_id');
|
||||
|
||||
foreach ($participant->pointsLog()->where('type', 'partner')->whereNull('incentive_new_partner_id')->cursor() as $log) {
|
||||
if ($log->source_type === User::class && $log->source_id) {
|
||||
$np = $newPartnerByUserId->get($log->source_id);
|
||||
if ($np) {
|
||||
$log->update(['incentive_new_partner_id' => $np->id]);
|
||||
$stats['onetime_partner_fk']++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($log->user_sales_volume_id) {
|
||||
$sv = UserSalesVolume::find($log->user_sales_volume_id);
|
||||
if ($sv && $sv->user_id) {
|
||||
$np = $newPartnerByUserId->get($sv->user_id);
|
||||
if ($np) {
|
||||
$log->update(['incentive_new_partner_id' => $np->id]);
|
||||
$stats['partner_fk']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$orderIdToUserAboId = [];
|
||||
foreach ($participant->pointsLog()->where('type', 'abo')->whereNull('incentive_new_abo_id')->cursor() as $log) {
|
||||
if ($log->source_type === UserAbo::class && $log->source_id) {
|
||||
$na = $newAboByUserAboId->get($log->source_id);
|
||||
if ($na) {
|
||||
$log->update(['incentive_new_abo_id' => $na->id]);
|
||||
$stats['onetime_abo_fk']++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($log->user_sales_volume_id) {
|
||||
$sv = UserSalesVolume::find($log->user_sales_volume_id);
|
||||
if ($sv && $sv->shopping_order_id) {
|
||||
if (! isset($orderIdToUserAboId[$sv->shopping_order_id])) {
|
||||
$orderIdToUserAboId[$sv->shopping_order_id] = UserAboOrder::where('shopping_order_id', $sv->shopping_order_id)->value('user_abo_id');
|
||||
}
|
||||
$userAboId = $orderIdToUserAboId[$sv->shopping_order_id] ?? null;
|
||||
if ($userAboId) {
|
||||
$na = $newAboByUserAboId->get($userAboId);
|
||||
if ($na) {
|
||||
$log->update(['incentive_new_abo_id' => $na->id]);
|
||||
$stats['abo_fk']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft IncentiveTracker::trackSalesVolume fuer Kandidaten-USVs auf, bei denen noch kein Log-Eintrag fuer diesen Teilnehmer existiert.
|
||||
*
|
||||
* @return int Anzahl neu angelegter Log-Zeilen (geschaetzt ueber Vorher/Nachher pro Teilnehmer)
|
||||
*/
|
||||
public function syncMissingSalesVolumeLogs(IncentiveParticipant $participant): int
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$synced = 0;
|
||||
$months = $incentive->getCalculationMonths();
|
||||
|
||||
$newPartnerUserIds = $participant->newPartners()->pluck('user_id')->filter()->values();
|
||||
$trackedUserAboIds = $participant->newAbos()->pluck('user_abo_id')->filter()->values();
|
||||
|
||||
$orderIdsForAbos = $trackedUserAboIds->isNotEmpty()
|
||||
? UserAboOrder::query()->whereIn('user_abo_id', $trackedUserAboIds)->pluck('shopping_order_id')->unique()->values()
|
||||
: collect();
|
||||
|
||||
foreach ($months as $period) {
|
||||
if ($newPartnerUserIds->isNotEmpty()) {
|
||||
$svs = UserSalesVolume::query()
|
||||
->whereIn('user_id', $newPartnerUserIds)
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->where('status', '!=', 6)
|
||||
->get();
|
||||
|
||||
foreach ($svs as $sv) {
|
||||
if ((int) abs($sv->points ?? 0) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($this->participantHasSalesVolumeLog($participant, $sv->id)) {
|
||||
continue;
|
||||
}
|
||||
IncentiveTracker::trackSalesVolume($sv);
|
||||
if ($this->participantHasSalesVolumeLog($participant, $sv->id)) {
|
||||
$synced++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($orderIdsForAbos->isNotEmpty()) {
|
||||
$svs = UserSalesVolume::query()
|
||||
->whereIn('shopping_order_id', $orderIdsForAbos)
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->where('status', '!=', 6)
|
||||
->get();
|
||||
|
||||
foreach ($svs as $sv) {
|
||||
if ((int) abs($sv->points ?? 0) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($this->participantHasSalesVolumeLog($participant, $sv->id)) {
|
||||
continue;
|
||||
}
|
||||
IncentiveTracker::trackSalesVolume($sv);
|
||||
if ($this->participantHasSalesVolumeLog($participant, $sv->id)) {
|
||||
$synced++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $synced;
|
||||
}
|
||||
|
||||
private function participantHasSalesVolumeLog(IncentiveParticipant $participant, int $userSalesVolumeId): bool
|
||||
{
|
||||
return IncentivePointsLog::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_sales_volume_id', $userSalesVolumeId)
|
||||
->where('is_storno', false)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
373
app/Services/Incentive/IncentiveTracker.php
Normal file
373
app/Services/Incentive/IncentiveTracker.php
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Incentive;
|
||||
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveNewAbo;
|
||||
use App\Models\IncentiveNewPartner;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\IncentivePointsLog;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class IncentiveTracker
|
||||
{
|
||||
/**
|
||||
* Track a new partner registration (Starterpaket bezahlt).
|
||||
* Fuegt Partner in Tracking-Tabelle ein + Log-Eintrag + Neuberechnung.
|
||||
*/
|
||||
public static function trackNewPartner(ShoppingOrder $shopping_order): void
|
||||
{
|
||||
try {
|
||||
if (! $shopping_order->qualifiesForIncentiveTrackedPartner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$new_user = User::find($shopping_order->auth_user_id);
|
||||
if (! $new_user || ! $new_user->m_sponsor) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sponsor_id = $new_user->m_sponsor;
|
||||
$registration_date = $shopping_order->created_at ?? Carbon::now();
|
||||
|
||||
$incentives = Incentive::query()
|
||||
->active()
|
||||
->where('qualification_start', '<=', $registration_date)
|
||||
->where('qualification_end', '>=', $registration_date)
|
||||
->get();
|
||||
|
||||
foreach ($incentives as $incentive) {
|
||||
$participant = IncentiveParticipant::ensureForIncentiveUser($incentive, $sponsor_id);
|
||||
|
||||
// Tracking-Tabelle: Partner erfassen (keine Duplikate)
|
||||
$newPartner = IncentiveNewPartner::firstOrCreate(
|
||||
['participant_id' => $participant->id, 'user_id' => $new_user->id],
|
||||
['registered_at' => $registration_date]
|
||||
);
|
||||
|
||||
// Log-Eintrag (Audit-Trail, keine Duplikate)
|
||||
self::writeLog($participant, 'partner', User::class, $new_user->id, $new_user->getFullName() ?: $new_user->email ?: ('User #'.$new_user->id), $registration_date, $incentive->points_partner_onetime, $newPartner->id);
|
||||
|
||||
// Neuberechnung aus Tracking-Tabellen
|
||||
$participant->recalculateFromTrackingTables()->save();
|
||||
|
||||
self::updateRanking($incentive);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('IncentiveTracker::trackNewPartner error: '.$e->getMessage(), [
|
||||
'shopping_order_id' => $shopping_order->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track an abo activation (Kundenabo is_for=ot oder Berater-Eigenabo is_for=me, bezahlt + aktiv).
|
||||
* Fuegt Abo in Tracking-Tabelle ein + Log-Eintrag + Neuberechnung.
|
||||
*/
|
||||
/**
|
||||
* Berater-ID für Incentive-Zuordnung: Bei Kundenabos (ot) sitzt der Berater in member_id,
|
||||
* bei Berater-Eigenabo (me) in user_id (vgl. AboHelper::createNewAbo).
|
||||
*/
|
||||
public static function consultantUserIdForAboIncentive(UserAbo $user_abo): ?int
|
||||
{
|
||||
if ($user_abo->is_for === 'ot') {
|
||||
return $user_abo->member_id ? (int) $user_abo->member_id : null;
|
||||
}
|
||||
|
||||
return $user_abo->user_id ? (int) $user_abo->user_id : null;
|
||||
}
|
||||
|
||||
public static function trackAboActivated(ShoppingOrder $shopping_order): void
|
||||
{
|
||||
try {
|
||||
$user_abo = $shopping_order->getUserAbo();
|
||||
if (! $user_abo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user_abo->is_for === 'ot') {
|
||||
$consultant_id = self::consultantUserIdForAboIncentive($user_abo);
|
||||
} elseif ($user_abo->is_for === 'me') {
|
||||
$consultant_id = $user_abo->user_id ? (int) $user_abo->user_id : null;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $consultant_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activation_date = $shopping_order->created_at ?? Carbon::now();
|
||||
|
||||
$incentives = Incentive::query()
|
||||
->active()
|
||||
->where('qualification_start', '<=', $activation_date)
|
||||
->where('qualification_end', '>=', $activation_date)
|
||||
->get();
|
||||
|
||||
foreach ($incentives as $incentive) {
|
||||
$participant = IncentiveParticipant::ensureForIncentiveUser($incentive, $consultant_id);
|
||||
|
||||
// Tracking-Tabelle: Abo erfassen (keine Duplikate)
|
||||
$newAbo = IncentiveNewAbo::firstOrCreate(
|
||||
['participant_id' => $participant->id, 'user_abo_id' => $user_abo->id],
|
||||
['activated_at' => $activation_date]
|
||||
);
|
||||
|
||||
// Log-Eintrag (Audit-Trail)
|
||||
self::writeLog($participant, 'abo', get_class($user_abo), $user_abo->id, $user_abo->email ?: ('Abo #'.$user_abo->id), $activation_date, $incentive->points_abo_onetime, null, $newAbo->id);
|
||||
|
||||
// Neuberechnung aus Tracking-Tabellen
|
||||
$participant->recalculateFromTrackingTables()->save();
|
||||
|
||||
self::updateRanking($incentive);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('IncentiveTracker::trackAboActivated error: '.$e->getMessage(), [
|
||||
'shopping_order_id' => $shopping_order->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track accumulated sales volume points.
|
||||
* Punkte werden NUR gezaehlt wenn der Umsatz von einem gettrackten
|
||||
* Neupartner oder Neuabo stammt.
|
||||
*/
|
||||
public static function trackSalesVolume(UserSalesVolume $user_sales_volume): void
|
||||
{
|
||||
try {
|
||||
$month = $user_sales_volume->month;
|
||||
$year = $user_sales_volume->year;
|
||||
|
||||
if (! $month || ! $year) {
|
||||
return;
|
||||
}
|
||||
|
||||
$points = (int) abs($user_sales_volume->points ?? 0);
|
||||
if ($points <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A. Pruefen ob der User ein gettrackter Neupartner ist
|
||||
$partner_trackings = IncentiveNewPartner::where('user_id', $user_sales_volume->user_id)
|
||||
->whereHas('participant.incentive', fn ($q) => $q->active())
|
||||
->with('participant.incentive')
|
||||
->get();
|
||||
|
||||
foreach ($partner_trackings as $tracking) {
|
||||
$participant = $tracking->participant;
|
||||
$incentive = $participant->incentive;
|
||||
|
||||
if (! $incentive->isDateInScope($month, $year)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = IncentivePointsLog::where('participant_id', $participant->id)
|
||||
->where('user_sales_volume_id', $user_sales_volume->id)
|
||||
->where('is_storno', false)
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $participant->id,
|
||||
'type' => 'partner',
|
||||
'source_type' => UserSalesVolume::class,
|
||||
'source_id' => $user_sales_volume->id,
|
||||
'source_label' => $user_sales_volume->message ?? ('SV '.$month.'/'.$year),
|
||||
'month' => $month,
|
||||
'year' => $year,
|
||||
'points_onetime' => 0,
|
||||
'points_accumulated' => $points,
|
||||
'user_sales_volume_id' => $user_sales_volume->id,
|
||||
'incentive_new_partner_id' => $tracking->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$participant->recalculateFromTrackingTables()->save();
|
||||
self::updateRanking($incentive);
|
||||
}
|
||||
|
||||
// B. Pruefen ob die Bestellung zu einem getrackten Neuabo gehoert (Kundenabo ot oder Berater me).
|
||||
// Bei Verlaengerung weicht shopping_order.shopping_user_id oft vom Stamm-user_abos.shopping_user_id ab (Replikat).
|
||||
if ($user_sales_volume->shopping_order_id) {
|
||||
$order = ShoppingOrder::find($user_sales_volume->shopping_order_id);
|
||||
|
||||
if ($order) {
|
||||
$userAboFromOrder = $order->getUserAbo();
|
||||
if (! $userAboFromOrder || ! in_array($userAboFromOrder->is_for, ['ot', 'me'], true)) {
|
||||
$userAboFromOrder = null;
|
||||
}
|
||||
|
||||
$abo_trackings = $userAboFromOrder
|
||||
? IncentiveNewAbo::query()
|
||||
->where('user_abo_id', $userAboFromOrder->id)
|
||||
->whereHas('participant.incentive', fn ($q) => $q->active())
|
||||
->with('participant.incentive')
|
||||
->get()
|
||||
: collect();
|
||||
foreach ($abo_trackings as $tracking) {
|
||||
$participant = $tracking->participant;
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive->isDateInScope($month, $year)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = IncentivePointsLog::where('participant_id', $participant->id)
|
||||
->where('user_sales_volume_id', $user_sales_volume->id)
|
||||
->where('is_storno', false)
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $participant->id,
|
||||
'type' => 'abo',
|
||||
'source_type' => UserSalesVolume::class,
|
||||
'source_id' => $user_sales_volume->id,
|
||||
'source_label' => $user_sales_volume->message ?? ('SV '.$month.'/'.$year),
|
||||
'month' => $month,
|
||||
'year' => $year,
|
||||
'points_onetime' => 0,
|
||||
'points_accumulated' => $points,
|
||||
'user_sales_volume_id' => $user_sales_volume->id,
|
||||
'incentive_new_abo_id' => $tracking->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$participant->recalculateFromTrackingTables()->save();
|
||||
self::updateRanking($incentive);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('IncentiveTracker::trackSalesVolume error: '.$e->getMessage(), [
|
||||
'user_sales_volume_id' => $user_sales_volume->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a storno (cancellation) of a sales volume.
|
||||
* Storno-Log + Neuberechnung aus Tracking-Tabellen.
|
||||
*/
|
||||
public static function trackStorno(UserSalesVolume $original, UserSalesVolume $cancellation): void
|
||||
{
|
||||
try {
|
||||
// Storno-Log-Eintraege schreiben
|
||||
$original_logs = IncentivePointsLog::where('user_sales_volume_id', $original->id)
|
||||
->where('is_storno', false)
|
||||
->get();
|
||||
|
||||
$affected_participants = collect();
|
||||
|
||||
foreach ($original_logs as $original_log) {
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $original_log->participant_id,
|
||||
'type' => $original_log->type,
|
||||
'source_type' => $original_log->source_type,
|
||||
'source_id' => $original_log->source_id,
|
||||
'source_label' => 'Storno: '.$original_log->source_label,
|
||||
'month' => $cancellation->month ?? $original_log->month,
|
||||
'year' => $cancellation->year ?? $original_log->year,
|
||||
'points_onetime' => -$original_log->points_onetime,
|
||||
'points_accumulated' => -$original_log->points_accumulated,
|
||||
'is_storno' => true,
|
||||
'storno_of_id' => $original_log->id,
|
||||
'user_sales_volume_id' => $cancellation->id,
|
||||
'incentive_new_partner_id' => $original_log->incentive_new_partner_id,
|
||||
'incentive_new_abo_id' => $original_log->incentive_new_abo_id,
|
||||
]);
|
||||
|
||||
$affected_participants->push($original_log->participant_id);
|
||||
}
|
||||
|
||||
// Auch ohne Log-Eintraege: alle Teilnehmer dieses Users neu berechnen
|
||||
if ($affected_participants->isEmpty() && $original->user_id) {
|
||||
$affected_participants = IncentiveParticipant::whereHas('incentive', function ($q) {
|
||||
$q->active();
|
||||
})->where('user_id', $original->user_id)->pluck('id');
|
||||
}
|
||||
|
||||
// Neuberechnung fuer alle betroffenen Teilnehmer
|
||||
foreach ($affected_participants->unique() as $participant_id) {
|
||||
$participant = IncentiveParticipant::with('incentive')->find($participant_id);
|
||||
if (! $participant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$participant->recalculateFromTrackingTables()->save();
|
||||
|
||||
if ($participant->incentive) {
|
||||
self::updateRanking($participant->incentive);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('IncentiveTracker::trackStorno error: '.$e->getMessage(), [
|
||||
'original_id' => $original->id,
|
||||
'cancellation_id' => $cancellation->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ranking for all participants of an incentive.
|
||||
*/
|
||||
public static function updateRanking(Incentive $incentive): void
|
||||
{
|
||||
// Nur Teilnehmer mit Punkten bekommen einen Rang (bei Punktgleichstand: Teilnahme bestaetigt vor anonym)
|
||||
$with_points = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->where('total_points', '>', 0)
|
||||
->orderByDesc('total_points')
|
||||
->orderByRaw('accepted_terms_at IS NOT NULL DESC')
|
||||
->get();
|
||||
|
||||
$rank = 1;
|
||||
foreach ($with_points as $participant) {
|
||||
$participant->rank = $rank;
|
||||
$participant->save();
|
||||
$rank++;
|
||||
}
|
||||
|
||||
// Teilnehmer ohne Punkte: Rang entfernen
|
||||
IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->where('total_points', '<=', 0)
|
||||
->whereNotNull('rank')
|
||||
->update(['rank' => null]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-Eintrag schreiben (Audit-Trail, keine Duplikate).
|
||||
*/
|
||||
private static function writeLog(IncentiveParticipant $participant, string $type, string $source_type, int $source_id, string $source_label, Carbon $date, int $points_onetime, ?int $incentive_new_partner_id = null, ?int $incentive_new_abo_id = null): void
|
||||
{
|
||||
$exists = IncentivePointsLog::where('participant_id', $participant->id)
|
||||
->where('type', $type)
|
||||
->where('source_type', $source_type)
|
||||
->where('source_id', $source_id)
|
||||
->where('is_storno', false)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $participant->id,
|
||||
'type' => $type,
|
||||
'source_type' => $source_type,
|
||||
'source_id' => $source_id,
|
||||
'source_label' => $source_label,
|
||||
'month' => $date->month,
|
||||
'year' => $date->year,
|
||||
'points_onetime' => $points_onetime,
|
||||
'points_accumulated' => 0,
|
||||
'incentive_new_partner_id' => $incentive_new_partner_id,
|
||||
'incentive_new_abo_id' => $incentive_new_abo_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
30
app/Services/LocaleGuard.php
Normal file
30
app/Services/LocaleGuard.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class LocaleGuard
|
||||
{
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function supportedLocaleCodes(): array
|
||||
{
|
||||
return array_keys(config('localization.supportedLocales'));
|
||||
}
|
||||
|
||||
public static function isSupported(string $locale): bool
|
||||
{
|
||||
return in_array(strtolower($locale), self::supportedLocaleCodes(), true);
|
||||
}
|
||||
|
||||
public static function normalize(?string $locale): ?string
|
||||
{
|
||||
if ($locale === null || $locale === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lower = strtolower($locale);
|
||||
|
||||
return self::isSupported($lower) ? $lower : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ use App\Models\UserCreditItem;
|
|||
use App\Models\UserLevel;
|
||||
use App\Repositories\InvoiceRepository;
|
||||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
|
|
@ -277,17 +278,47 @@ class Payment
|
|||
|
||||
// the Order is Pay, so we can set the Status in the Abo
|
||||
if ($shopping_order->is_abo) {
|
||||
// Payone-Server-Callback kann vor dem Checkout-Erfolgs-Redirect laufen; dann existiert
|
||||
// noch kein UserAbo/UserAboOrder — setAboActive wirkt erst nach Anlage.
|
||||
if ($paid && $shopping_payment) {
|
||||
$shopping_payment->loadMissing([
|
||||
'payment_transactions',
|
||||
'shopping_order.shopping_user',
|
||||
'shopping_order.shopping_order_items',
|
||||
]);
|
||||
if (! $shopping_order->getUserAbo()) {
|
||||
AboHelper::createNewAbo($shopping_payment);
|
||||
$shopping_order->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
AboHelper::setAboActive($shopping_order, 2, true);
|
||||
|
||||
// Incentive: Track activated customer abo
|
||||
IncentiveTracker::trackAboActivated($shopping_order);
|
||||
}
|
||||
|
||||
// Incentive: Track new partner registration (ggf. mit Starterpaket)
|
||||
if ($shopping_order->payment_for == 1) {
|
||||
IncentiveTracker::trackNewPartner($shopping_order);
|
||||
}
|
||||
|
||||
// make Invoice is not exist and is live
|
||||
// Wrapped in try/catch: Rechnungserstellung darf den Payment-Flow nicht crashen
|
||||
if ($shopping_order->mode === 'live' || Util::isTestSystem(true)) {
|
||||
// Reload the shopping order to check for invoice again (defense against race conditions)
|
||||
$shopping_order->refresh();
|
||||
|
||||
if (! $shopping_order->isInvoice()) {
|
||||
$invoice_repo = new InvoiceRepository($shopping_order);
|
||||
$invoice_repo->createAndSalesVolume();
|
||||
try {
|
||||
$invoice_repo = new InvoiceRepository($shopping_order);
|
||||
$invoice_repo->createAndSalesVolume();
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('Payment::paymentStatusPaidAction - Rechnungserstellung fehlgeschlagen', [
|
||||
'shopping_order_id' => $shopping_order->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This class is a wrapper to be able to send arrays of Payone request
|
||||
* to the Payone platform.
|
||||
|
|
@ -16,8 +17,8 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with Payone Connector. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @package Simple PHP Integration
|
||||
* @link https://www.bspayone.com/
|
||||
*
|
||||
* @copyright (C) BS PAYONE GmbH 2016, 2018
|
||||
* @author Florian Bender <florian.bender@bspayone.com>
|
||||
* @author Timo Kuchel <timo.kuchel@bspayone.com>
|
||||
|
|
@ -26,87 +27,124 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
|
||||
//require 'vendor/autoload.php';
|
||||
// require 'vendor/autoload.php';
|
||||
use Exception;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\BadResponseException;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Class Payone
|
||||
*/
|
||||
class Payone {
|
||||
|
||||
class Payone
|
||||
{
|
||||
/**
|
||||
* The URL of the Payone API
|
||||
*/
|
||||
const PAYONE_SERVER_API_URL = 'https://api.pay1.de/post-gateway/';
|
||||
|
||||
const PAYONE_CLIENT_API_URL = 'https://secure.pay1.de/client-api/';
|
||||
|
||||
/**
|
||||
* performing the HTTP POST request to the PAYONE platform
|
||||
*
|
||||
* @param array $request
|
||||
* @param string $responsetype
|
||||
* @throws Exception
|
||||
* @param array $request
|
||||
* @param string $responsetype
|
||||
* @param Client|null $client Optional Guzzle client (e.g. mocked in tests).
|
||||
* @return array|\Psr\Http\Message\StreamInterface Returns an array of response
|
||||
* parameters in "classic" mode, a Stream for any other mode.
|
||||
* parameters in "classic" mode, a Stream for any other mode.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function sendRequest($request, $responsetype = "")
|
||||
public static function sendRequest($request, $responsetype = '', ?Client $client = null)
|
||||
{
|
||||
if ($responsetype === "json") {
|
||||
// appends the accept: application/json header to the request
|
||||
// This is used to retrieve structured JSON in the response
|
||||
// $client = new Client(['headers' => ['accept' => 'application/json', 'content-type' => 'text/plain;charset=UTF-8']]);
|
||||
$client = new Client(['headers' => ['accept' => 'application/json']]);
|
||||
|
||||
}
|
||||
else {
|
||||
// if $responsetype is set to anything else than "json", use the standard request
|
||||
// $client = new Client(['headers' => ['content-type' => 'text/plain;charset=UTF-8']]);
|
||||
$client = new Client();
|
||||
if ($client === null) {
|
||||
if ($responsetype === 'json') {
|
||||
// appends the accept: application/json header to the request
|
||||
// This is used to retrieve structured JSON in the response
|
||||
// $client = new Client(['headers' => ['accept' => 'application/json', 'content-type' => 'text/plain;charset=UTF-8']]);
|
||||
$client = new Client(['headers' => ['accept' => 'application/json']]);
|
||||
} else {
|
||||
// if $responsetype is set to anything else than "json", use the standard request
|
||||
// $client = new Client(['headers' => ['content-type' => 'text/plain;charset=UTF-8']]);
|
||||
$client = new Client;
|
||||
}
|
||||
}
|
||||
|
||||
// echo "Requesting...";
|
||||
// echo "Requesting...";
|
||||
$begin = microtime(true);
|
||||
$userMessage = 'Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. Bitte versuchen Sie es später erneut. Fehlercode: 1002';
|
||||
|
||||
try {
|
||||
$response = $client->request('POST', self::PAYONE_SERVER_API_URL, ['form_params' => $request]);
|
||||
}
|
||||
catch (\GuzzleHttp\Exception\ClientException $e) {
|
||||
} catch (BadResponseException $e) {
|
||||
$error = $e->getResponse();
|
||||
$responseBodyAsString = $error->getBody()->getContents();
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error:1002 Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. App\Services\Payone::sendRequest Something went wrong during the HTTP request.',
|
||||
'payone',
|
||||
'error',
|
||||
'Error:1002 Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. App\Services\Payone::sendRequest HTTP-Fehlerantwort (4xx/5xx).',
|
||||
['error' => $error, 'responseBodyAsString' => $responseBodyAsString, 'request' => $request]
|
||||
);
|
||||
abort(403, 'Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. Bitte versuchen Sie es später erneut. Fehlercode: 1002');
|
||||
abort(403, $userMessage);
|
||||
} catch (ConnectException $e) {
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error:1002 Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. App\Services\Payone::sendRequest Netzwerk-/Transportfehler (keine HTTP-Antwort).',
|
||||
[
|
||||
'exception' => $e->getMessage(),
|
||||
'request' => $request,
|
||||
]
|
||||
);
|
||||
abort(403, $userMessage);
|
||||
} catch (RequestException $e) {
|
||||
if ($e->hasResponse()) {
|
||||
$error = $e->getResponse();
|
||||
$responseBodyAsString = $error !== null ? $error->getBody()->getContents() : '';
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error:1002 Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. App\Services\Payone::sendRequest HTTP-Fehlerantwort.',
|
||||
['error' => $error, 'responseBodyAsString' => $responseBodyAsString, 'request' => $request]
|
||||
);
|
||||
} else {
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error:1002 Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. App\Services\Payone::sendRequest Transportfehler (RequestException ohne Antwort).',
|
||||
[
|
||||
'exception' => $e->getMessage(),
|
||||
'request' => $request,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
abort(403, $userMessage);
|
||||
}
|
||||
if (isset($response)) {
|
||||
if (implode($response->getHeader('Content-Type')) == 'text/plain;charset=UTF-8'){
|
||||
if (implode($response->getHeader('Content-Type')) == 'text/plain;charset=UTF-8') {
|
||||
// if the content type is text/plain, parse response into array
|
||||
$return = self::parseResponse($response);
|
||||
// \Log::channel('payone')->error('App\Services\Payone::sendRequest content type is text/plain: '.$response);
|
||||
|
||||
} else {
|
||||
// if the content type is anything else, just return the response body
|
||||
$return = json_decode($response->getBody(),true);
|
||||
$return = json_decode($response->getBody(), true);
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error: App\Services\Payone::sendRequest content type is anything else',
|
||||
'payone',
|
||||
'error',
|
||||
'Error: App\Services\Payone::sendRequest content type is anything else',
|
||||
['error' => $return, 'response' => $response, 'request' => $request]
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error: App\Services\Payone::sendRequest Something went wrong during the HTTP request',
|
||||
'payone',
|
||||
'error',
|
||||
'Error: App\Services\Payone::sendRequest Something went wrong during the HTTP request',
|
||||
['request' => $request]
|
||||
);
|
||||
throw new Exception('Something went wrong during the HTTP request.');
|
||||
|
|
@ -114,60 +152,62 @@ class Payone {
|
|||
|
||||
$end = microtime(true);
|
||||
$duration = $end - $begin;
|
||||
if(!is_array($return)){
|
||||
if (! is_array($return)) {
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error: 1003 App\Http\Controllers\Pay\PayoneController::ResponseData response is non array: return:',
|
||||
['return'=>$return, 'response' => $response, 'request' => $request]
|
||||
'payone',
|
||||
'error',
|
||||
'Error: 1003 App\Http\Controllers\Pay\PayoneController::ResponseData response is non array: return:',
|
||||
['return' => $return, 'response' => $response, 'request' => $request]
|
||||
);
|
||||
abort(403, 'Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. Bitte versuchen Sie es später erneut. Fehlercode: 1003');
|
||||
}
|
||||
|
||||
if(!isset($return['status'])){
|
||||
if (! isset($return['status'])) {
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error: 1004 App\Http\Controllers\Pay\PayoneController::ResponseData response has non status',
|
||||
['return'=>$return, 'response' => $response, 'request' => $request]
|
||||
'payone',
|
||||
'error',
|
||||
'Error: 1004 App\Http\Controllers\Pay\PayoneController::ResponseData response has non status',
|
||||
['return' => $return, 'response' => $response, 'request' => $request]
|
||||
);
|
||||
abort(403, 'Der Zahlungsanbieter ist nicht erreichbar, die Zahlung konnte nicht durchgeführt werden. Bitte versuchen Sie es später erneut. Fehlercode: 1004');
|
||||
}
|
||||
/* echo "done.\n";
|
||||
echo "Request took " . $duration . " seconds.\n";
|
||||
echo "<br>";
|
||||
*/
|
||||
|
||||
/* echo "done.\n";
|
||||
echo "Request took " . $duration . " seconds.\n";
|
||||
echo "<br>";
|
||||
*/
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets response string an puts it into an array
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* @throws Exception
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function parseResponse(ResponseInterface $response)
|
||||
{
|
||||
$responseArray = array();
|
||||
$responseArray = [];
|
||||
$explode = explode("\n", $response->getBody());
|
||||
foreach ($explode as $e) {
|
||||
$keyValue = explode("=", $e);
|
||||
if (trim($keyValue[0]) != "") {
|
||||
$keyValue = explode('=', $e);
|
||||
if (trim($keyValue[0]) != '') {
|
||||
if (count($keyValue) == 2) {
|
||||
$responseArray[$keyValue[0]] = trim($keyValue[1]);
|
||||
} else {
|
||||
$key = $keyValue[0];
|
||||
unset($keyValue[0]);
|
||||
$value = implode("=", $keyValue);
|
||||
$value = implode('=', $keyValue);
|
||||
$responseArray[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*if ($responseArray['status'] == "ERROR") {
|
||||
$msg = "Payone returned an error:\n" . print_r($responseArray, true);
|
||||
throw new Exception($msg);
|
||||
}*/
|
||||
|
||||
/*if ($responseArray['status'] == "ERROR") {
|
||||
$msg = "Payone returned an error:\n" . print_r($responseArray, true);
|
||||
throw new Exception($msg);
|
||||
}*/
|
||||
return $responseArray;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
51
app/Services/ProductOrderContext.php
Normal file
51
app/Services/ProductOrderContext.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Product;
|
||||
|
||||
class ProductOrderContext
|
||||
{
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function allowedShowOnIds(bool $isAbo, string $shippingIsFor): array
|
||||
{
|
||||
if ($shippingIsFor === 'me' || $shippingIsFor === 'abo-me') {
|
||||
return $isAbo ? ['12', '13'] : ['2'];
|
||||
}
|
||||
|
||||
return $isAbo ? ['12', '13'] : ['3'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $allowedIds
|
||||
*/
|
||||
public static function productMatchesShowOn(Product $product, array $allowedIds): bool
|
||||
{
|
||||
$showOn = $product->show_on;
|
||||
if (! is_array($showOn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($allowedIds as $id) {
|
||||
foreach ($showOn as $value) {
|
||||
if ((string) $value === (string) $id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function isProductAllowedInContext(Product $product, bool $isAbo, string $shippingIsFor): bool
|
||||
{
|
||||
return self::productMatchesShowOn($product, self::allowedShowOnIds($isAbo, $shippingIsFor));
|
||||
}
|
||||
|
||||
public static function isProductAllowedInCustomerWebshop(Product $product): bool
|
||||
{
|
||||
return self::isProductAllowedInContext($product, false, 'ot-customer');
|
||||
}
|
||||
}
|
||||
127
app/Services/SyS/AboOrdersOverview.php
Normal file
127
app/Services/SyS/AboOrdersOverview.php
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\SyS;
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserAboOrder;
|
||||
|
||||
class AboOrdersOverview
|
||||
{
|
||||
/**
|
||||
* Payone-/Shop-Zahlungsstatus: tatsächlich eingezogen.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const SUCCESS_TXACTIONS = ['paid', 'extern_paid', 'invoice_paid'];
|
||||
|
||||
public static function show()
|
||||
{
|
||||
$filter = request('filter', 'all');
|
||||
|
||||
$aboOrders = UserAboOrder::with([
|
||||
'user_abo',
|
||||
'user_abo.user',
|
||||
'user_abo.user.account',
|
||||
'shopping_order',
|
||||
'shopping_order.shopping_user',
|
||||
'shopping_order.shopping_payments',
|
||||
])
|
||||
->whereHas('shopping_order')
|
||||
->when($filter === 'berater', fn ($q) => $q->whereHas('user_abo', fn ($q) => $q->where('is_for', 'me')))
|
||||
->when($filter === 'kunde', fn ($q) => $q->whereHas('user_abo', fn ($q) => $q->where('is_for', '!=', 'me')))
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
$summary = [
|
||||
'total_orders' => $aboOrders->count(),
|
||||
'total_diff' => 0.0,
|
||||
'affected_orders' => 0,
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($aboOrders as $aboOrder) {
|
||||
$order = $aboOrder->shopping_order;
|
||||
if (! $order) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subtotalWs = (float) $order->subtotal_ws;
|
||||
$totalShipping = (float) $order->total_shipping;
|
||||
$tax = (float) $order->tax;
|
||||
|
||||
$expectedCents = (int) round($totalShipping * 100);
|
||||
$actualCents = self::actualChargedCentsFromPayments($order);
|
||||
|
||||
$actualEur = $actualCents !== null ? round($actualCents / 100, 2) : null;
|
||||
$diff = ($actualCents !== null)
|
||||
? round(($expectedCents - $actualCents) / 100, 2)
|
||||
: null;
|
||||
|
||||
if ($diff !== null && abs($diff) <= 0.01) {
|
||||
$diff = 0;
|
||||
}
|
||||
|
||||
$payments = $order->shopping_payments;
|
||||
$paymentTxSummary = $payments->isEmpty()
|
||||
? null
|
||||
: $payments->pluck('txaction')->filter()->unique()->implode(', ');
|
||||
|
||||
$user = $aboOrder->user_abo->user ?? null;
|
||||
|
||||
$rows[] = [
|
||||
'abo_order_id' => $aboOrder->id,
|
||||
'abo_id' => $aboOrder->user_abo_id,
|
||||
'order_id' => '<a href='.route('admin_sales_customers_detail', [$aboOrder->shopping_order_id]).'>'.$aboOrder->shopping_order_id.'</a>',
|
||||
'user_id' => $user->id ?? null,
|
||||
'user_name' => $aboOrder->shopping_order->shopping_user ? ($aboOrder->shopping_order->shopping_user->billing_firstname ?? '').' '.($aboOrder->shopping_order->shopping_user->billing_lastname ?? '') : '-',
|
||||
'user_email' => $aboOrder->shopping_order->shopping_user ? $aboOrder->shopping_order->shopping_user->billing_email ?? '-' : '-',
|
||||
'is_for' => $aboOrder->user_abo->is_for ?? '-',
|
||||
'subtotal_ws' => $subtotalWs,
|
||||
'tax' => $tax,
|
||||
'total_shipping' => $totalShipping,
|
||||
'actual_charged_eur' => $actualEur,
|
||||
'payment_count' => $payments->count(),
|
||||
'payment_txactions' => $paymentTxSummary,
|
||||
'diff' => $diff,
|
||||
'status' => $aboOrder->status,
|
||||
'paid' => $aboOrder->paid,
|
||||
'txaction' => $order->txaction,
|
||||
'created_at' => $aboOrder->created_at,
|
||||
];
|
||||
|
||||
if ($diff !== null && abs($diff) >= 0.01) {
|
||||
$summary['total_diff'] += $diff;
|
||||
$summary['affected_orders']++;
|
||||
}
|
||||
}
|
||||
|
||||
$summary['total_diff'] = round($summary['total_diff'], 2);
|
||||
|
||||
return view('sys.tools.abo-orders-overview', [
|
||||
'rows' => $rows,
|
||||
'summary' => $summary,
|
||||
'filter' => $filter,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Summiert erfolgreiche Abbuchungen aus `shopping_payments` (Cent).
|
||||
* Kein Treffer bei erfolgreichen Status → null (kein belastbarer Eingang).
|
||||
*/
|
||||
public static function actualChargedCentsFromPayments(ShoppingOrder $order): ?int
|
||||
{
|
||||
$payments = $order->shopping_payments;
|
||||
if ($payments === null || $payments->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$successful = $payments->filter(
|
||||
fn ($p) => in_array($p->txaction, self::SUCCESS_TXACTIONS, true)
|
||||
);
|
||||
|
||||
$sum = (int) $successful->sum('amount');
|
||||
|
||||
return $sum > 0 ? $sum : null;
|
||||
}
|
||||
}
|
||||
536
app/Services/SyS/PayoneCallbackTestbench.php
Normal file
536
app/Services/SyS/PayoneCallbackTestbench.php
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\SyS;
|
||||
|
||||
use App\Console\Commands\UserMakeAboOrder;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserShop;
|
||||
use App\Services\AboHelper;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command as ConsoleCommand;
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\Support\Str;
|
||||
use ReflectionMethod;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Throwable;
|
||||
|
||||
class PayoneCallbackTestbench
|
||||
{
|
||||
public static function show()
|
||||
{
|
||||
self::ensureAllowed();
|
||||
|
||||
return view('sys.tools.payone-callback-testbench', [
|
||||
'fixture' => session('payone_testbench_fixture'),
|
||||
'simulateResult' => session('payone_testbench_simulate'),
|
||||
'checkoutSuccess' => session('payone_testbench_checkout_success'),
|
||||
'userAboId' => session('payone_testbench_user_abo_id'),
|
||||
'cronRenewal' => session('payone_testbench_cron_renewal'),
|
||||
'cronRenewalOrderId' => session('payone_testbench_cron_renewal_order_id'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function store()
|
||||
{
|
||||
self::ensureAllowed();
|
||||
|
||||
$action = request('action');
|
||||
|
||||
if ($action === 'create_fixture') {
|
||||
return self::createFixture();
|
||||
}
|
||||
|
||||
if ($action === 'simulate_paid') {
|
||||
return self::simulatePaidCallback();
|
||||
}
|
||||
|
||||
if ($action === 'simulate_checkout_success') {
|
||||
return self::simulateCheckoutSuccess();
|
||||
}
|
||||
|
||||
if ($action === 'simulate_cron_renewal') {
|
||||
return self::simulateCronRenewal();
|
||||
}
|
||||
|
||||
if ($action === 'clear_fixture') {
|
||||
session()->forget([
|
||||
'payone_testbench_fixture',
|
||||
'payone_testbench_simulate',
|
||||
'payone_testbench_checkout_success',
|
||||
'payone_testbench_user_abo_id',
|
||||
'payone_testbench_cron_renewal',
|
||||
'payone_testbench_cron_renewal_order_id',
|
||||
]);
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Unbekannte Aktion.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload wie Payone ihn an die Status-URL sendet (Route: api.{domain}/payment/status).
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function buildPayoneCallbackPayload(ShoppingOrder $order, ShoppingPayment $payment, ?int $txid = null): array
|
||||
{
|
||||
$txid = $txid ?? random_int(100_000_000, 999_999_999);
|
||||
$price = number_format(round($payment->amount / 100, 2), 2, '.', '');
|
||||
|
||||
return [
|
||||
'key' => (string) config('payone.defaults.key'),
|
||||
'param' => (string) $order->id,
|
||||
'userid' => '999999999',
|
||||
'txid' => (string) $txid,
|
||||
'reference' => $payment->reference,
|
||||
'price' => $price,
|
||||
'txaction' => 'paid',
|
||||
'mode' => $payment->mode ?? 'test',
|
||||
'clearingtype' => $payment->clearingtype,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständige URL der Payone-Server-zu-Server-Route (z. B. http://api.mivita.test/payment/status).
|
||||
*/
|
||||
public static function paymentStatusUrl(): string
|
||||
{
|
||||
return route('api.payment_status', [], true);
|
||||
}
|
||||
|
||||
private static function ensureAllowed(): void
|
||||
{
|
||||
if (app()->isProduction()) {
|
||||
abort(403, 'Payone-Testbench ist in Production deaktiviert.');
|
||||
}
|
||||
}
|
||||
|
||||
private static function createFixture()
|
||||
{
|
||||
$validated = request()->validate([
|
||||
'amount_eur' => ['required', 'numeric', 'min:0.01', 'max:99999.99'],
|
||||
'consultant_user_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'is_abo' => ['sometimes', 'boolean'],
|
||||
'is_for_ot' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$amountEur = round((float) $validated['amount_eur'], 2);
|
||||
$amountCents = (int) round($amountEur * 100);
|
||||
$isAbo = request()->boolean('is_abo');
|
||||
$isFor = request()->boolean('is_for_ot') ? 'ot' : 'me';
|
||||
$consultantId = (int) $validated['consultant_user_id'];
|
||||
|
||||
session()->forget(['payone_testbench_simulate', 'payone_testbench_checkout_success']);
|
||||
|
||||
$country = ShippingCountry::query()->first();
|
||||
if (! $country) {
|
||||
abort(500, 'Kein Eintrag in shipping_countries – bitte Stammdaten anlegen.');
|
||||
}
|
||||
|
||||
$userShop = UserShop::query()->first();
|
||||
if (! $userShop) {
|
||||
abort(500, 'Kein user_shops Eintrag – bitte Shop anlegen.');
|
||||
}
|
||||
|
||||
$fixture = DB::transaction(function () use ($amountEur, $amountCents, $isAbo, $isFor, $country, $userShop, $consultantId) {
|
||||
$email = 'payone-bench-'.Str::lower(Str::random(8)).'@example.test';
|
||||
|
||||
if ($isFor === 'me') {
|
||||
$shoppingUserAttrs = [
|
||||
'billing_firstname' => 'Bench',
|
||||
'billing_lastname' => 'Payone',
|
||||
'billing_email' => $email,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'is_for' => $isFor,
|
||||
'is_from' => 'user_order',
|
||||
'auth_user_id' => $consultantId,
|
||||
'member_id' => null,
|
||||
];
|
||||
$orderAttrs = [
|
||||
'auth_user_id' => $consultantId,
|
||||
'member_id' => null,
|
||||
];
|
||||
} else {
|
||||
$shoppingUserAttrs = [
|
||||
'billing_firstname' => 'Bench',
|
||||
'billing_lastname' => 'Payone',
|
||||
'billing_email' => $email,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'is_for' => $isFor,
|
||||
'is_from' => 'user_order',
|
||||
'auth_user_id' => null,
|
||||
'member_id' => $consultantId,
|
||||
];
|
||||
$orderAttrs = [
|
||||
'auth_user_id' => null,
|
||||
'member_id' => $consultantId,
|
||||
];
|
||||
}
|
||||
|
||||
$shoppingUser = ShoppingUser::create($shoppingUserAttrs);
|
||||
|
||||
$order = ShoppingOrder::create(array_merge([
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'country_id' => $country->id,
|
||||
'language' => app()->getLocale(),
|
||||
'user_shop_id' => $userShop->id,
|
||||
'payment_for' => $shoppingUser->getOrderPaymentFor(),
|
||||
'total' => $amountEur,
|
||||
'subtotal' => $amountEur,
|
||||
'shipping' => 0,
|
||||
'shipping_net' => 0,
|
||||
'subtotal_ws' => $amountEur,
|
||||
'tax' => 0,
|
||||
'total_shipping' => $amountEur,
|
||||
'points' => 0,
|
||||
'weight' => 0,
|
||||
'paid' => false,
|
||||
'is_abo' => $isAbo,
|
||||
'abo_interval' => $isAbo ? 30 : 0,
|
||||
'txaction' => 'prev',
|
||||
'mode' => 'test',
|
||||
], $orderAttrs));
|
||||
|
||||
$reference = self::generatePaymentReference();
|
||||
|
||||
$payment = ShoppingPayment::create([
|
||||
'shopping_order_id' => $order->id,
|
||||
'clearingtype' => 'wlt',
|
||||
'wallettype' => 'PPE',
|
||||
'onlinebanktransfertype' => '',
|
||||
'reference' => $reference,
|
||||
'amount' => $amountCents,
|
||||
'currency' => 'EUR',
|
||||
'txaction' => null,
|
||||
'mode' => 'test',
|
||||
'is_abo' => $isAbo,
|
||||
'abo_interval' => $isAbo ? ($order->abo_interval ?? 0) : 0,
|
||||
]);
|
||||
|
||||
return [
|
||||
'shopping_order_id' => $order->id,
|
||||
'shopping_payment_id' => $payment->id,
|
||||
'reference' => $reference,
|
||||
'amount_eur' => $amountEur,
|
||||
'amount_cents' => $amountCents,
|
||||
'is_abo' => $isAbo,
|
||||
'is_for' => $isFor,
|
||||
'consultant_user_id' => $consultantId,
|
||||
'assignment_note' => $isFor === 'me'
|
||||
? 'Berater: auth_user_id = Berater-ID, member_id leer'
|
||||
: 'Kunde: auth_user_id leer, member_id = Berater-ID (Zuordnung / Provision)',
|
||||
'api_url' => self::paymentStatusUrl(),
|
||||
'curl' => self::buildCurlExample(self::paymentStatusUrl(), self::buildPayoneCallbackPayload($order->fresh(), $payment->fresh())),
|
||||
];
|
||||
});
|
||||
|
||||
session()->put('payone_testbench_fixture', $fixture);
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entspricht dem Browser-Erfolg nach Zahlung: {@see CheckoutController::handleSuccessfulTransaction}
|
||||
* und {@see CheckoutController::transactionApproved} → {@see AboHelper::createNewAbo}.
|
||||
*/
|
||||
private static function simulateCheckoutSuccess()
|
||||
{
|
||||
$validated = request()->validate([
|
||||
'shopping_order_id' => ['required', 'integer', 'exists:shopping_orders,id'],
|
||||
]);
|
||||
|
||||
$order = ShoppingOrder::query()
|
||||
->with(['shopping_order_items', 'shopping_user', 'shopping_payments'])
|
||||
->findOrFail($validated['shopping_order_id']);
|
||||
|
||||
$payment = $order->shopping_payments->last();
|
||||
|
||||
if (! $payment) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Keine ShoppingPayment zu dieser Bestellung.');
|
||||
}
|
||||
|
||||
if (! $order->is_abo || (int) $order->abo_interval <= 0) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Bestellung ist kein Abo (is_abo / abo_interval).');
|
||||
}
|
||||
|
||||
if (UserAboOrder::query()->where('shopping_order_id', $order->id)->exists()) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Für diese Bestellung existiert bereits ein UserAboOrder – Abo wurde bereits angelegt.');
|
||||
}
|
||||
|
||||
$payment->load('payment_transactions');
|
||||
|
||||
if ($payment->payment_transactions->isEmpty()) {
|
||||
PaymentTransaction::create([
|
||||
'shopping_payment_id' => $payment->id,
|
||||
'request' => 'transaction',
|
||||
'txid' => random_int(1, 999_999_999),
|
||||
'userid' => 999_999_999,
|
||||
'status' => 'PAYONE',
|
||||
'key' => (string) config('payone.defaults.key'),
|
||||
'txaction' => 'paid',
|
||||
'transmitted_data' => [],
|
||||
'mode' => $payment->mode ?? 'test',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
AboHelper::createNewAbo($payment->fresh([
|
||||
'shopping_order.shopping_user',
|
||||
'shopping_order.shopping_order_items',
|
||||
'payment_transactions',
|
||||
]));
|
||||
} catch (Throwable $e) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'createNewAbo: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$userAboOrder = UserAboOrder::query()
|
||||
->where('shopping_order_id', $order->id)
|
||||
->with('user_abo')
|
||||
->first();
|
||||
|
||||
session()->put('payone_testbench_checkout_success', [
|
||||
'user_abo_id' => $userAboOrder?->user_abo_id,
|
||||
'user_abo_order_id' => $userAboOrder?->id,
|
||||
'shopping_order_id' => $order->id,
|
||||
'order_paid_after' => (bool) $order->fresh()->paid,
|
||||
'hint' => 'Erstbestellung: Abo-Stammdaten wie nach Checkout-Redirect. Die Bestätigung (paid, setAboActive, Incentive) folgt im nächsten Schritt über die Payone-API.',
|
||||
]);
|
||||
|
||||
if ($userAboOrder?->user_abo_id) {
|
||||
session()->put('payone_testbench_user_abo_id', $userAboOrder->user_abo_id);
|
||||
}
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function buildCurlExample(string $url, array $payload): string
|
||||
{
|
||||
$parts = [];
|
||||
foreach ($payload as $k => $v) {
|
||||
$parts[] = escapeshellarg($k).'='.escapeshellarg((string) $v);
|
||||
}
|
||||
|
||||
return 'curl -X POST '.escapeshellarg($url).' -d '.implode(' -d ', $parts);
|
||||
}
|
||||
|
||||
private static function generatePaymentReference(): string
|
||||
{
|
||||
return substr(str_replace('-', '', (string) Str::uuid()), 0, 16);
|
||||
}
|
||||
|
||||
private static function simulatePaidCallback()
|
||||
{
|
||||
$validated = request()->validate([
|
||||
'shopping_order_id' => ['required', 'integer', 'exists:shopping_orders,id'],
|
||||
]);
|
||||
|
||||
$order = ShoppingOrder::query()->with('shopping_payments')->findOrFail($validated['shopping_order_id']);
|
||||
$payment = $order->shopping_payments->last();
|
||||
|
||||
if (! $payment) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Keine ShoppingPayment zu dieser Bestellung.');
|
||||
}
|
||||
|
||||
if ($order->is_abo && (int) $order->abo_interval > 0) {
|
||||
if (! UserAboOrder::query()->where('shopping_order_id', $order->id)->exists()) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Bei einer Abo-Erstbestellung zuerst Schritt 2 (Checkout: createNewAbo) ausführen, danach erst die Payone-API (paid). So existiert ein UserAboOrder für setAboActive und trackAboActivated.');
|
||||
}
|
||||
}
|
||||
|
||||
$payload = self::buildPayoneCallbackPayload($order, $payment);
|
||||
|
||||
$request = Request::create(self::paymentStatusUrl(), 'POST', $payload);
|
||||
$response = app()->handle($request);
|
||||
|
||||
$order->refresh();
|
||||
|
||||
$result = [
|
||||
'http_status' => $response->getStatusCode(),
|
||||
'body' => $response->getContent(),
|
||||
'order_paid' => (bool) $order->paid,
|
||||
'order_txaction' => $order->txaction,
|
||||
'payload' => $payload,
|
||||
'hint' => 'Entspricht Payment::paymentStatusPaidAction: Abo bestätigen (setAboActive), ggf. Incentive trackAboActivated, Rechnung …',
|
||||
];
|
||||
|
||||
session()->put('payone_testbench_simulate', $result);
|
||||
|
||||
$userAbo = $order->getUserAbo();
|
||||
if ($userAbo) {
|
||||
session()->put('payone_testbench_user_abo_id', $userAbo->id);
|
||||
}
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie {@see UserMakeAboOrder::checkAbosToOrder}: next_date = heute und keine doppelte Verarbeitung am selben Tag.
|
||||
* Nur fuer Testbench (nicht Production).
|
||||
*/
|
||||
public static function prepareUserAboForCronRun(UserAbo $userAbo): void
|
||||
{
|
||||
self::ensureAllowed();
|
||||
|
||||
$today = Carbon::today()->format('Y-m-d');
|
||||
|
||||
DB::transaction(function () use ($userAbo, $today) {
|
||||
UserAboOrder::query()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->whereDate('created_at', $today)
|
||||
->delete();
|
||||
|
||||
$userAbo->update(['next_date' => $today]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Einmaliger Cron-Lauf: {@see UserMakeAboOrder::makeOrder} (Bestellung + Payone-Zahlung).
|
||||
* Danach ggf. Schritt 5: Payone-API (paid) fuer die Verlaengerungs-Bestellung.
|
||||
*/
|
||||
private static function simulateCronRenewal()
|
||||
{
|
||||
self::ensureAllowed();
|
||||
|
||||
$validated = request()->validate([
|
||||
'user_abo_id' => ['nullable', 'integer', 'exists:user_abos,id'],
|
||||
]);
|
||||
|
||||
$userAboId = $validated['user_abo_id'] ?? session('payone_testbench_user_abo_id');
|
||||
if (! $userAboId) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Kein user_abo_id – zuerst Abo-Erstkauf (Schritte 2–3) abschließen oder ID eintragen.');
|
||||
}
|
||||
|
||||
$userAbo = UserAbo::query()
|
||||
->with(['user_abo_items', 'shopping_user'])
|
||||
->findOrFail($userAboId);
|
||||
|
||||
AboHelper::ensureUserAboItemsFromLatestOrder($userAbo);
|
||||
$userAbo->refresh();
|
||||
$userAbo->load(['user_abo_items', 'shopping_user']);
|
||||
|
||||
if (! $userAbo->active || (int) $userAbo->status !== 2) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'UserAbo nicht aktiv oder nicht status 2 (abo_okay).');
|
||||
}
|
||||
if ($userAbo->user_abo_items->isEmpty()) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Keine Abo-Artikel – Verlängerung nicht möglich.');
|
||||
}
|
||||
|
||||
self::prepareUserAboForCronRun($userAbo);
|
||||
$userAbo->refresh();
|
||||
|
||||
$command = new UserMakeAboOrder;
|
||||
self::bindNullConsoleOutput($command);
|
||||
|
||||
$makeOrder = new ReflectionMethod(UserMakeAboOrder::class, 'makeOrder');
|
||||
$makeOrder->setAccessible(true);
|
||||
|
||||
try {
|
||||
/** @var ShoppingOrder|null $shoppingOrder */
|
||||
$shoppingOrder = $makeOrder->invoke($command, $userAbo->fresh(['user_abo_items', 'shopping_user']));
|
||||
} catch (Throwable $e) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Cron makeOrder: '.$e->getMessage());
|
||||
}
|
||||
|
||||
if (! $shoppingOrder) {
|
||||
session()->put('payone_testbench_cron_renewal', [
|
||||
'success' => false,
|
||||
'message' => 'makeOrder hat keine Bestellung zurückgegeben (typisch: makeShoppingOrder false, z. B. fehlende Referenz-Bestellung/user_shop_id, oder createShoppingUser liefert nichts).',
|
||||
'diagnosis' => self::cronRenewalDiagnosis($userAbo->fresh(['user_abo_items', 'shopping_user'])),
|
||||
]);
|
||||
session()->forget('payone_testbench_cron_renewal_order_id');
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
session()->put('payone_testbench_cron_renewal', [
|
||||
'success' => true,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'hint' => 'Entspricht user:make_abo_order / makeOrder. Für Rechnung und Incentive-Umsatz wie in Produktion: Schritt 5 (Payone paid) für diese Verlängerungs-Bestellung ausführen.',
|
||||
]);
|
||||
session()->put('payone_testbench_cron_renewal_order_id', $shoppingOrder->id);
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ohne Log-Datei: Ursachen fuer fehlgeschlagenes makeOrder eingrenzen.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function cronRenewalDiagnosis(UserAbo $userAbo): array
|
||||
{
|
||||
$out = [
|
||||
'user_abo_items' => $userAbo->user_abo_items->count(),
|
||||
];
|
||||
|
||||
$su = $userAbo->shopping_user;
|
||||
if (! $su) {
|
||||
$out['shopping_user'] = 'fehlt (UserAbo.shopping_user_id)';
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
$out['shopping_user_id'] = $su->id;
|
||||
$out['shopping_orders_count'] = $su->shopping_orders()->count();
|
||||
$ref = $su->shopping_orders()->orderByDesc('id')->first();
|
||||
$out['reference_order_id'] = $ref?->id;
|
||||
$out['reference_user_shop_id'] = $ref?->user_shop_id;
|
||||
|
||||
if ($ref && $ref->user_shop_id) {
|
||||
$shop = UserShop::withTrashed()->find($ref->user_shop_id);
|
||||
$out['user_shop_row_exists'] = $shop !== null;
|
||||
$out['user_shop_row_trashed'] = $shop !== null && $shop->trashed();
|
||||
$out['user_shop_relation_loaded'] = $ref->user_shop !== null;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ohne Artisan-Kontext ist $command->output null – info()/error() wuerden crashen.
|
||||
*/
|
||||
private static function bindNullConsoleOutput(ConsoleCommand $command): void
|
||||
{
|
||||
$command->setLaravel(app());
|
||||
|
||||
$input = new ArrayInput([]);
|
||||
$nullOutput = new NullOutput;
|
||||
$outputStyle = app()->make(OutputStyle::class, [
|
||||
'input' => $input,
|
||||
'output' => $nullOutput,
|
||||
]);
|
||||
|
||||
$reflection = new \ReflectionClass($command);
|
||||
$property = $reflection->getProperty('output');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($command, $outputStyle);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ShippingCountry;
|
||||
use App\User;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Yard;
|
||||
|
||||
class UserService
|
||||
{
|
||||
public static $user_country;
|
||||
|
||||
public static $shipping_country;
|
||||
|
||||
public static $shipping_free = false;
|
||||
|
||||
public static $user_tax_free = false;
|
||||
|
||||
public static $user_reverse_charge = false;
|
||||
|
||||
public static $instance = 'shopping';
|
||||
|
||||
public static function getTransChange(){
|
||||
public static function getTransChange()
|
||||
{
|
||||
|
||||
$langs = config('localization.supportedLocales');
|
||||
$ret = [];
|
||||
foreach($langs as $code => $lang){
|
||||
$ret[strtolower($code)] = strtolower($lang['native']);
|
||||
foreach ($langs as $code => $lang) {
|
||||
$ret[strtolower($code)] = strtolower($lang['native']);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public static function setInstance($instance){
|
||||
public static function setInstance($instance)
|
||||
{
|
||||
self::$instance = $instance;
|
||||
}
|
||||
|
||||
//init Yard for user order Customer
|
||||
public static function initCustomerYard($shopping_user, $for){
|
||||
// init Yard for user order Customer
|
||||
public static function initCustomerYard($shopping_user, $for)
|
||||
{
|
||||
self::$user_tax_free = false;
|
||||
if($shopping_user->same_as_billing){
|
||||
if ($shopping_user->same_as_billing) {
|
||||
self::$user_country = $shopping_user->billing_country;
|
||||
self::$shipping_country = $shopping_user->billing_country;
|
||||
}else{
|
||||
} else {
|
||||
self::$user_country = $shopping_user->billing_country;
|
||||
self::$shipping_country = $shopping_user->shipping_country;
|
||||
}
|
||||
if(self::$user_country->supply_country && self::$shipping_country->supply_country){
|
||||
if (self::$user_country->supply_country && self::$shipping_country->supply_country) {
|
||||
self::$user_tax_free = true;
|
||||
}
|
||||
$ShippingCountry = ShippingCountry::whereCountryId(self::$shipping_country->id)->first();
|
||||
self::$shipping_free = $ShippingCountry->shipping ? $ShippingCountry->shipping->free : false;
|
||||
|
||||
$shippingCountry = ShippingCountry::whereCountryId(self::$shipping_country->id)->first();
|
||||
if (! $shippingCountry) {
|
||||
$shippingCountry = ShippingCountry::query()
|
||||
->whereHas('shipping', fn ($q) => $q->where('active', true))
|
||||
->orderBy('id')
|
||||
->first();
|
||||
}
|
||||
if (! $shippingCountry) {
|
||||
$shippingCountry = ShippingCountry::query()->orderBy('id')->first();
|
||||
}
|
||||
if (! $shippingCountry) {
|
||||
throw new RuntimeException('Kein Eintrag in shipping_countries (Tabelle leer oder nicht migriert).');
|
||||
}
|
||||
|
||||
self::$shipping_free = $shippingCountry->shipping?->free ?? false;
|
||||
self::$shipping_free = self::$shipping_free !== null ? self::$shipping_free : false;
|
||||
Yard::instance(self::$instance)->setShippingCountryWithPrice($ShippingCountry->id, $for);
|
||||
Yard::instance(self::$instance)->setShippingCountryWithPrice($shippingCountry->id, $for);
|
||||
Yard::instance(self::$instance)->setUserPriceInfos(self::getYardInfo());
|
||||
}
|
||||
|
||||
//init Yard for user order Berater
|
||||
public static function initUserYard(User $user, $shipping_country_id, $for){
|
||||
// init Yard for user order Berater
|
||||
public static function initUserYard(User $user, $shipping_country_id, $for)
|
||||
{
|
||||
self::$shipping_free = false;
|
||||
self::checkUserTaxShippingCountry($user, $shipping_country_id,);
|
||||
self::checkUserTaxShippingCountry($user, $shipping_country_id);
|
||||
Yard::instance(self::$instance)->setShippingCountryWithPrice($shipping_country_id, $for);
|
||||
Yard::instance(self::$instance)->setUserPriceInfos(self::getYardInfo());
|
||||
}
|
||||
|
||||
|
||||
public static function checkUserTaxShippingCountry(User $user, $shipping_country_id) {
|
||||
|
||||
if(!$user->account || !$user->account->country_id){
|
||||
public static function checkUserTaxShippingCountry(User $user, $shipping_country_id)
|
||||
{
|
||||
|
||||
if (! $user->account || ! $user->account->country_id) {
|
||||
abort(403, 'Error: User hat kein Land!');
|
||||
}
|
||||
$ShippingCountry = ShippingCountry::findOrFail($shipping_country_id);
|
||||
self::$user_tax_free = self::performUserTaxShippingCountry($user, $ShippingCountry);
|
||||
|
||||
return $ShippingCountry;
|
||||
/*
|
||||
dump( self::$user_price_code );
|
||||
|
|
@ -73,50 +100,56 @@ class UserService
|
|||
*/
|
||||
}
|
||||
|
||||
public static function performUserTaxShippingCountry($user, $ShippingCountry){
|
||||
//preise für das Land
|
||||
public static function performUserTaxShippingCountry($user, $ShippingCountry)
|
||||
{
|
||||
// preise für das Land
|
||||
self::$user_country = $user->account->country;
|
||||
self::$shipping_country = $ShippingCountry->country;
|
||||
//ausgehend vom Land des Rechnungsempfänger $user->account->country
|
||||
//ist der Rechnungsempfänger im Drittland?
|
||||
if($user->account->country->supply_country){
|
||||
if($ShippingCountry->country->supply_country){
|
||||
//Lieferadresse im Drittland?
|
||||
// ausgehend vom Land des Rechnungsempfänger $user->account->country
|
||||
// ist der Rechnungsempfänger im Drittland?
|
||||
if ($user->account->country->supply_country) {
|
||||
if ($ShippingCountry->country->supply_country) {
|
||||
// Lieferadresse im Drittland?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//Rechnungsempfänger in der EU
|
||||
|
||||
//Lieferland mit RSV
|
||||
if($ShippingCountry->country->eu_country){
|
||||
//Rechnungsempfänger mit valid aktiv RSV
|
||||
if($user->account->reverse_charge && $user->account->reverse_charge_valid){
|
||||
//Rechnungsland ist auch Lieferland, dann RSV
|
||||
if(strtolower($user->account->reverse_charge_code) == strtolower($ShippingCountry->country->code)){
|
||||
// Rechnungsempfänger in der EU
|
||||
|
||||
// Lieferland mit RSV
|
||||
if ($ShippingCountry->country->eu_country) {
|
||||
// Rechnungsempfänger mit valid aktiv RSV
|
||||
if ($user->account->reverse_charge && $user->account->reverse_charge_valid) {
|
||||
// Rechnungsland ist auch Lieferland, dann RSV
|
||||
if (strtolower($user->account->reverse_charge_code) == strtolower($ShippingCountry->country->code)) {
|
||||
self::$user_reverse_charge = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Lieferland ohne RSV
|
||||
}
|
||||
|
||||
// Lieferland ohne RSV
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getYardInfo(){
|
||||
public static function getYardInfo()
|
||||
{
|
||||
return [
|
||||
'shipping_free' => self::$shipping_free,
|
||||
'shipping_free' => self::$shipping_free,
|
||||
'user_tax_free' => self::$user_tax_free,
|
||||
'user_reverse_charge' => self::$user_reverse_charge,
|
||||
'user_country_id' => self::$user_country->id,
|
||||
'shipping_country_id' => self::$shipping_country->id,
|
||||
'shipping_country_id' => self::$shipping_country->id,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getTaxFree(){
|
||||
public static function getTaxFree()
|
||||
{
|
||||
return self::$user_tax_free ? true : false;
|
||||
}
|
||||
|
||||
public static function getUserPriceInfos(){
|
||||
public static function getUserPriceInfos()
|
||||
{
|
||||
return [
|
||||
'user_tax_free' => self::$user_tax_free,
|
||||
'user_reverse_charge' => self::$user_reverse_charge,
|
||||
|
|
@ -124,8 +157,9 @@ class UserService
|
|||
];
|
||||
}
|
||||
|
||||
public static function getOrderInfo($key = false){
|
||||
if(!self::$user_country){
|
||||
public static function getOrderInfo($key = false)
|
||||
{
|
||||
if (! self::$user_country) {
|
||||
return '';
|
||||
}
|
||||
switch ($key) {
|
||||
|
|
@ -139,22 +173,21 @@ class UserService
|
|||
return self::$user_tax_free ? __('no') : __('yes');
|
||||
break;
|
||||
case 'user_reverse_charge':
|
||||
return self::$user_reverse_charge ? __('yes') : __('no');
|
||||
return self::$user_reverse_charge ? __('yes') : __('no');
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static function createConfirmationCode() {
|
||||
public static function createConfirmationCode()
|
||||
{
|
||||
$unique = false;
|
||||
do{
|
||||
do {
|
||||
$confirmation_code = Str::random(30);
|
||||
if(User::where('confirmation_code', '=', $confirmation_code)->count() == 0){
|
||||
if (User::where('confirmation_code', '=', $confirmation_code)->count() == 0) {
|
||||
$unique = true;
|
||||
}
|
||||
}
|
||||
while(!$unique);
|
||||
} while (! $unique);
|
||||
|
||||
return $confirmation_code;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserHistory;
|
||||
use App\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Request;
|
||||
use Yard;
|
||||
|
|
@ -21,7 +23,7 @@ class Util
|
|||
$uuid = (string) Str::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;
|
||||
|
|
@ -76,7 +78,7 @@ class Util
|
|||
if (strlen($str) > $length) {
|
||||
$str = substr($str, 0, $length);
|
||||
// $str = substr($str, 0, strrpos($str, " "));
|
||||
$str = $str . ' ...';
|
||||
$str = $str.' ...';
|
||||
}
|
||||
|
||||
return $str;
|
||||
|
|
@ -329,9 +331,9 @@ class Util
|
|||
public static function getMyMivitaShopUrl($add_url = '')
|
||||
{
|
||||
if (\Session::has('user_shop_domain')) {
|
||||
$url = \Session::get('user_shop_domain') . $add_url;
|
||||
$url = \Session::get('user_shop_domain').$add_url;
|
||||
if (! str_starts_with($url, 'http')) {
|
||||
$url = 'https://' . ltrim($url, '/');
|
||||
$url = 'https://'.ltrim($url, '/');
|
||||
}
|
||||
|
||||
return $url;
|
||||
|
|
@ -339,22 +341,124 @@ class Util
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständige URL zum Warenkorb (User-Shop) nach „Nachbestellen“ im Portal.
|
||||
* Verhindert Weiterleitung auf Portal/CRM/Checkout, wo /user/card/show nicht existiert (404).
|
||||
*/
|
||||
public static function getCustomerReorderCartUrl(?ShoppingOrder $shoppingOrder = null): string
|
||||
{
|
||||
$cartPath = '/user/card/show';
|
||||
$candidates = [];
|
||||
|
||||
if ($shoppingOrder?->member?->shop) {
|
||||
$candidates[] = config('app.protocol').$shoppingOrder->member->shop->slug.'.'.config('app.domain').config('app.tld_care');
|
||||
}
|
||||
|
||||
if (\Auth::guard('customers')->check()) {
|
||||
$stored = \Auth::guard('customers')->user()->user_shop_domain;
|
||||
if ($stored) {
|
||||
$candidates[] = $stored;
|
||||
}
|
||||
}
|
||||
|
||||
if (\Session::has('user_shop_domain')) {
|
||||
$candidates[] = \Session::get('user_shop_domain');
|
||||
}
|
||||
|
||||
$user = User::find(6);
|
||||
if ($user?->shop) {
|
||||
$candidates[] = config('app.protocol').$user->shop->slug.'.'.config('app.domain').config('app.tld_care');
|
||||
}
|
||||
|
||||
$defaultSlug = config('domains.domains.shop.default_user_shop', 'aloevera');
|
||||
$candidates[] = config('app.protocol').$defaultSlug.'.'.config('app.domain').config('app.tld_care');
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
$normalized = self::normalizeShopBaseUrl($candidate);
|
||||
if ($normalized === null || self::isShopBaseUrlInvalidForUserCard($normalized)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $normalized.$cartPath;
|
||||
}
|
||||
|
||||
return config('domains.protocol').config('domains.domains.shop.host').$cartPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal, CRM und Checkout hosten keine User-Shop-Warenkorb-Route unter /user/card/show.
|
||||
*/
|
||||
public static function isShopBaseUrlInvalidForUserCard(?string $baseUrl): bool
|
||||
{
|
||||
if ($baseUrl === null || $baseUrl === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$host = self::extractHostFromUrl($baseUrl);
|
||||
if ($host === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$host = strtolower($host);
|
||||
|
||||
$invalidHosts = array_filter([
|
||||
config('domains.domains.portal.host'),
|
||||
config('domains.domains.crm.host'),
|
||||
config('domains.domains.checkout.host'),
|
||||
]);
|
||||
|
||||
foreach ($invalidHosts as $invalid) {
|
||||
if ($invalid !== null && $invalid !== '' && strtolower($invalid) === $host) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function normalizeShopBaseUrl(?string $url): ?string
|
||||
{
|
||||
if ($url === null || trim($url) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$u = trim($url);
|
||||
if (! str_starts_with($u, 'http')) {
|
||||
$u = 'https://'.ltrim($u, '/');
|
||||
}
|
||||
|
||||
return rtrim($u, '/');
|
||||
}
|
||||
|
||||
private static function extractHostFromUrl(string $url): ?string
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if (! empty($host)) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
$stripped = preg_replace('#^https?://#i', '', $url);
|
||||
$parts = explode('/', $stripped, 2);
|
||||
|
||||
return $parts[0] !== '' ? $parts[0] : null;
|
||||
}
|
||||
|
||||
public static function getMyMivitaPortalUrl($protocol = true)
|
||||
{
|
||||
$pro = $protocol ? config('app.protocol') : '';
|
||||
|
||||
return $pro . config('app.pre_url_portal') . config('app.domain') . config('app.tld_care');
|
||||
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') : '';
|
||||
|
||||
return $pro . config('app.pre_url_crm') . config('app.domain') . config('app.tld_care');
|
||||
return $pro.config('app.pre_url_crm').config('app.domain').config('app.tld_care');
|
||||
}
|
||||
|
||||
public static function getUserPaymentFor($instance = 'shopping')
|
||||
|
|
@ -377,11 +481,11 @@ 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');
|
||||
return config('app.protocol').config('app.domain').config('app.tld_care');
|
||||
}
|
||||
|
||||
public static function getUserCardBackUrl($uri, $instance = 'shopping')
|
||||
|
|
@ -393,36 +497,36 @@ 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');
|
||||
return config('app.protocol').config('app.domain').config('app.tld_care');
|
||||
}
|
||||
|
||||
public static function isMivitaShop()
|
||||
{
|
||||
if (Request::getHost() === 'checkout.' . config('app.domain') . config('app.tld_care')) {
|
||||
if (Request::getHost() === 'checkout.'.config('app.domain').config('app.tld_care')) {
|
||||
if ($user_shop = \Session::get('user_shop')) {
|
||||
if ($user_shop->slug === 'aloevera' || $user_shop->slug === 'naturcosmetic') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Request::getHost() === 'naturcosmetic.' . config('app.domain') . config('app.tld_care')) {
|
||||
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');
|
||||
return \Config::get('app.url') === config('app.domain').config('app.tld_shop');
|
||||
}
|
||||
|
||||
public static function isTestSystem($dev = false)
|
||||
|
|
@ -445,7 +549,7 @@ class Util
|
|||
$base = log($size) / log(1024);
|
||||
$suffixes = [' bytes', ' KB', ' MB', ' GB', ' TB'];
|
||||
|
||||
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
|
||||
return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)];
|
||||
} else {
|
||||
return $size;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue