10.April 2026
This commit is contained in:
parent
a00c42e770
commit
f58c709945
208 changed files with 19280 additions and 2914 deletions
169
app/Console/Commands/AboStoreChartSnapshots.php
Normal file
169
app/Console/Commands/AboStoreChartSnapshots.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AboChartSnapshot;
|
||||
use App\Models\UserAbo;
|
||||
use App\Services\AboHelper;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class AboStoreChartSnapshots extends Command
|
||||
{
|
||||
protected $signature = 'abo:store-chart-snapshots
|
||||
{--user= : Nur einen bestimmten User berechnen (user_id)}
|
||||
{--force : Bereits vorhandene Snapshots überschreiben}';
|
||||
|
||||
protected $description = 'Speichert monatliche Abo-Zählungen aller vergangenen Monate in der Datenbank (einmalig je Monat)';
|
||||
|
||||
private const SCOPES = ['ot', 'team_abos', 'team_cust_abos'];
|
||||
|
||||
private const START_YEAR = 2026;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$now = Carbon::now();
|
||||
$force = (bool) $this->option('force');
|
||||
|
||||
// Monate die eingefroren werden sollen: von START_YEAR/01 bis letzten Monat
|
||||
$months = $this->getPastMonths($now);
|
||||
if (empty($months)) {
|
||||
$this->info('Keine vergangenen Monate zum Speichern.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// User ermitteln
|
||||
$userQuery = User::whereNotNull('m_level')
|
||||
->whereNotNull('payment_account')
|
||||
->where('admin', '<', 4)
|
||||
->whereNull('deleted_at');
|
||||
|
||||
if ($userId = $this->option('user')) {
|
||||
$userQuery->where('id', $userId);
|
||||
}
|
||||
|
||||
$users = $userQuery->select('id')->get();
|
||||
$total = $users->count();
|
||||
$this->info("Berechne Snapshots für {$total} User, ".count($months).' Monate, '.count(self::SCOPES).' Scopes...');
|
||||
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$bar->start();
|
||||
|
||||
$inserted = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Bereits vorhandene Snapshots für diesen User laden (zum Überspringen)
|
||||
$existing = AboChartSnapshot::where('user_id', $user->id)
|
||||
->get()
|
||||
->keyBy(fn ($s) => "{$s->scope}_{$s->year}_{$s->month}");
|
||||
|
||||
$teamUserIds = AboHelper::getTeamUserIds($user->id);
|
||||
$rows = [];
|
||||
|
||||
foreach ($months as [$year, $month]) {
|
||||
$startOfMonth = Carbon::create($year, $month, 1)->startOfMonth();
|
||||
$endOfMonth = Carbon::create($year, $month, 1)->endOfMonth();
|
||||
|
||||
foreach (self::SCOPES as $scope) {
|
||||
$key = "{$scope}_{$year}_{$month}";
|
||||
|
||||
if (! $force && $existing->has($key)) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = $this->calculateCount($scope, $user->id, $teamUserIds, $startOfMonth, $endOfMonth);
|
||||
$rows[] = [
|
||||
'user_id' => $user->id,
|
||||
'scope' => $scope,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'count' => $count,
|
||||
'calculated_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($rows)) {
|
||||
if ($force) {
|
||||
foreach ($rows as $row) {
|
||||
AboChartSnapshot::updateOrInsert(
|
||||
['user_id' => $row['user_id'], 'scope' => $row['scope'], 'year' => $row['year'], 'month' => $row['month']],
|
||||
$row
|
||||
);
|
||||
}
|
||||
} else {
|
||||
AboChartSnapshot::insertOrIgnore($rows);
|
||||
}
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
$this->info("Fertig. Gespeichert: {$inserted}, Übersprungen (bereits vorhanden): {$skipped}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Abo-Anzahl für einen Scope/User/Monat anhand der tatsächlichen Daten zum Zeitpunkt der Berechnung.
|
||||
*
|
||||
* @param int[] $teamUserIds
|
||||
*/
|
||||
private function calculateCount(string $scope, int $userId, array $teamUserIds, Carbon $startOfMonth, Carbon $endOfMonth): int
|
||||
{
|
||||
$terminalStatuses = [4, 5];
|
||||
|
||||
$query = match ($scope) {
|
||||
'ot' => UserAbo::where('member_id', $userId)
|
||||
->where('is_for', 'ot')
|
||||
->where('status', '>', 1),
|
||||
'team_abos' => UserAbo::whereIn('user_id', $teamUserIds)
|
||||
->where('is_for', 'me')
|
||||
->where('status', '>', 1),
|
||||
'team_cust_abos' => UserAbo::whereIn('member_id', $teamUserIds)
|
||||
->where('is_for', 'ot')
|
||||
->where('status', '>', 1),
|
||||
};
|
||||
|
||||
return $query
|
||||
->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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle abgeschlossenen Monate von START_YEAR/01 bis letzten Monat.
|
||||
*
|
||||
* @return array<array{int, int}>
|
||||
*/
|
||||
private function getPastMonths(Carbon $now): array
|
||||
{
|
||||
$months = [];
|
||||
$cursor = Carbon::create(self::START_YEAR, 1, 1);
|
||||
$lastMonth = $now->copy()->subMonth()->endOfMonth();
|
||||
|
||||
while ($cursor->lte($lastMonth)) {
|
||||
$months[] = [(int) $cursor->year, (int) $cursor->month];
|
||||
$cursor->addMonth();
|
||||
}
|
||||
|
||||
return $months;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@ use App\Cron\UserPaymentCredits;
|
|||
use App\Models\Setting;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Models\UserBusinessStructure;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||||
use App\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class BusinessStoreOptimized extends Command
|
||||
|
|
@ -163,6 +166,10 @@ class BusinessStoreOptimized extends Command
|
|||
$this->userLevelUpdate();
|
||||
});
|
||||
|
||||
$this->executeWithErrorHandling('Monthly Qual-KP Bonus Points', function () {
|
||||
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized Monthly Qual-KP Bonus Points');
|
||||
$this->assignMonthlyQualKpBonusPoints();
|
||||
});
|
||||
// Auskommentierte Prozesse bleiben inaktiv
|
||||
// $this->userCreatePaymentCreditsPDF();
|
||||
// $this->storeBusinessStructureUsersDetailPeriod(1, 6);
|
||||
|
|
@ -377,6 +384,58 @@ class BusinessStoreOptimized extends Command
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt ausgewählten Usern einmal pro Monat ihre Level-qual_kp als KP-Bonus gut.
|
||||
* Idempotent: Ein bereits vorhandener Eintrag für diesen Monat/Jahr wird übersprungen.
|
||||
*
|
||||
* @var array<int> User-IDs die den monatlichen Qual-KP-Bonus erhalten sollen
|
||||
*/
|
||||
private function assignMonthlyQualKpBonusPoints(): void
|
||||
{
|
||||
$bonusUserIds = [486];
|
||||
|
||||
$month = date('m');
|
||||
$year = date('Y');
|
||||
|
||||
$users = User::query()
|
||||
->whereIn('id', $bonusUserIds)
|
||||
->whereNotNull('m_level')
|
||||
->with('user_level')
|
||||
->get()
|
||||
->filter(fn (User $user) => $user->user_level && $user->user_level->qual_kp > 0);
|
||||
|
||||
$assigned = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
$alreadyExists = UserSalesVolume::where('user_id', $user->id)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->where('info', 'qual_kp_bonus')
|
||||
->exists();
|
||||
|
||||
if ($alreadyExists) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
SalesPointsVolume::addSalesPointsVolume([
|
||||
'user_id' => $user->id,
|
||||
'points' => $user->user_level->qual_kp,
|
||||
'status_points' => 2,
|
||||
'total_net' => 0,
|
||||
'status_turnover' => 1,
|
||||
'info' => 'qual_kp_bonus',
|
||||
]);
|
||||
|
||||
$assigned++;
|
||||
}
|
||||
|
||||
$this->info("Qual-KP Bonus: {$assigned} zugewiesen, {$skipped} übersprungen (bereits vorhanden)");
|
||||
\Log::channel('cron')->info("Qual-KP Bonus Points: assigned={$assigned}, skipped={$skipped}");
|
||||
}
|
||||
|
||||
private function logExecutionTime($message)
|
||||
{
|
||||
$diff = microtime(true) - $this->timeStart;
|
||||
|
|
|
|||
205
app/Console/Commands/IncentiveCalculate.php
Normal file
205
app/Console/Commands/IncentiveCalculate.php
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Services\Incentive\IncentivePointsLogRepairService;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Batch-Neuberechnung fuer Incentives.
|
||||
*
|
||||
* Abo-Wertung (nicht hier codiert, sondern in {@see IncentiveParticipant::rebuildFromSourceTables}
|
||||
* und {@see IncentivePointsLogRepairService::syncMissingTrackingAbos}):
|
||||
*
|
||||
* - Eigenabo (me): zaehlt auch wenn es vor dem Qualifikationszeitraum abgeschlossen wurde;
|
||||
* Einmalpunkte wirken dann ab Qualifikationsbeginn (aktiviert_at/Log-Monat auf Start des Zeitraums).
|
||||
* - Kundenabo (ot): nur wenn im Qualifikationszeitraum neu abgeschlossen (created_at im Zeitraum).
|
||||
*
|
||||
* Zu Beginn werden fuer alle Berater (User mit m_level) fehlende Teilnehmerzeilen ohne
|
||||
* accepted_terms angelegt ({@see IncentiveParticipant::ensureConsultantsForIncentive}), damit
|
||||
* Punkte ohne Checkbox mitlaufen; die Rangliste blendet Namen erst nach Zustimmung ein.
|
||||
*/
|
||||
class IncentiveCalculate extends Command
|
||||
{
|
||||
protected $signature = 'incentive:calculate
|
||||
{incentive_id? : ID des Incentives (leer = alle aktiven)}
|
||||
{--force : Tracking-Tabellen + Log loeschen und komplett neu aufbauen}
|
||||
{--skip-repair : Kein Nachziehen von Trackings/FKs/SV-Logs (nur Summen aus bestehendem Log)}
|
||||
{--verbose-details : Zeigt Details pro Teilnehmer}';
|
||||
|
||||
protected $description = 'Incentive-Punkte: fehlende Partner-/Abo-Trackings, FK-Reparatur, fehlende SV-Logs, Summen/Ranking; --force = kompletter Neuaufbau aus Quelldaten';
|
||||
|
||||
public function handle(IncentivePointsLogRepairService $repairService): int
|
||||
{
|
||||
if ($id = $this->argument('incentive_id')) {
|
||||
$incentive = Incentive::find($id);
|
||||
if (! $incentive) {
|
||||
$this->error("Incentive #{$id} nicht gefunden.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return $this->processIncentive($incentive, $repairService);
|
||||
}
|
||||
|
||||
$incentives = Incentive::active()->get();
|
||||
if ($incentives->isEmpty()) {
|
||||
$this->info('Keine aktiven Incentives gefunden.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$exitCode = self::SUCCESS;
|
||||
foreach ($incentives as $incentive) {
|
||||
if ($this->processIncentive($incentive, $repairService) !== self::SUCCESS) {
|
||||
$exitCode = self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
private function processIncentive(Incentive $incentive, IncentivePointsLogRepairService $repairService): int
|
||||
{
|
||||
$force = $this->option('force');
|
||||
$skipRepair = $this->option('skip-repair');
|
||||
$verbose = $this->option('verbose-details');
|
||||
|
||||
$this->info("=== {$incentive->name} (ID: {$incentive->id}) ===");
|
||||
$this->info(" Zeitraum: {$incentive->qualification_start->format('d.m.Y')} - {$incentive->qualification_end->format('d.m.Y')}");
|
||||
if ($force) {
|
||||
$this->info(' Modus: FORCE (Tracking + Log aus Quelldaten neu aufbauen)');
|
||||
} elseif ($skipRepair) {
|
||||
$this->info(' Modus: Nur Neuberechnung (Summen/Ranking aus bestehendem Log)');
|
||||
} else {
|
||||
$this->info(' Modus: Tracking nachziehen + FK-Reparatur + SV-Logs + Neuberechnung');
|
||||
}
|
||||
|
||||
$stubAdded = IncentiveParticipant::ensureConsultantsForIncentive($incentive);
|
||||
if ($stubAdded > 0) {
|
||||
$this->info(" Berater-Teilnehmer neu angelegt (ohne Zustimmung): {$stubAdded}");
|
||||
}
|
||||
|
||||
$participants = $incentive->participants()->with('user', 'user.account')->get();
|
||||
$this->info(" Teilnehmer: {$participants->count()}");
|
||||
$this->newLine();
|
||||
|
||||
$stats = [
|
||||
'processed' => 0,
|
||||
'errors' => 0,
|
||||
'with_points' => 0,
|
||||
'with_partners' => 0,
|
||||
'with_abos' => 0,
|
||||
'tracking_partner_added' => 0,
|
||||
'tracking_abo_added' => 0,
|
||||
'repair_partner_fk' => 0,
|
||||
'repair_abo_fk' => 0,
|
||||
'repair_onetime_partner_fk' => 0,
|
||||
'repair_onetime_abo_fk' => 0,
|
||||
'sv_logs_added' => 0,
|
||||
];
|
||||
$errors = [];
|
||||
|
||||
$bar = $this->output->createProgressBar($participants->count());
|
||||
$bar->start();
|
||||
|
||||
foreach ($participants as $participant) {
|
||||
try {
|
||||
if (! $participant->user) {
|
||||
$bar->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($force) {
|
||||
$participant->rebuildFromSourceTables()->save();
|
||||
} else {
|
||||
if (! $skipRepair) {
|
||||
$stats['tracking_partner_added'] += $repairService->syncMissingTrackingPartners($participant);
|
||||
$stats['tracking_abo_added'] += $repairService->syncMissingTrackingAbos($participant);
|
||||
$r = $repairService->repairForeignKeys($participant);
|
||||
$stats['repair_partner_fk'] += $r['partner_fk'];
|
||||
$stats['repair_abo_fk'] += $r['abo_fk'];
|
||||
$stats['repair_onetime_partner_fk'] += $r['onetime_partner_fk'];
|
||||
$stats['repair_onetime_abo_fk'] += $r['onetime_abo_fk'];
|
||||
$stats['sv_logs_added'] += $repairService->syncMissingSalesVolumeLogs($participant);
|
||||
}
|
||||
$participant->recalculateFromTrackingTables()->save();
|
||||
}
|
||||
|
||||
$stats['processed']++;
|
||||
|
||||
if ($participant->total_points > 0) {
|
||||
$stats['with_points']++;
|
||||
}
|
||||
if ($participant->qualified_partners > 0) {
|
||||
$stats['with_partners']++;
|
||||
}
|
||||
if ($participant->qualified_abos > 0) {
|
||||
$stats['with_abos']++;
|
||||
}
|
||||
|
||||
if ($verbose && $participant->total_points > 0) {
|
||||
$name = $participant->user->account
|
||||
? $participant->user->account->first_name.' '.$participant->user->account->last_name
|
||||
: ($participant->user->email ?? 'User #'.$participant->user_id);
|
||||
$bar->clear();
|
||||
$this->line(" {$name}: {$participant->total_points} Pkt, {$participant->qualified_partners} Partner, {$participant->qualified_abos} Abos");
|
||||
$bar->display();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$stats['errors']++;
|
||||
$errors[] = "Participant #{$participant->id} (User #{$participant->user_id}): {$e->getMessage()}";
|
||||
Log::error('IncentiveCalculation error for participant '.$participant->id.': '.$e->getMessage());
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
IncentiveTracker::updateRanking($incentive);
|
||||
$ranked = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->whereNotNull('rank')
|
||||
->count();
|
||||
|
||||
$tableRows = [
|
||||
['Verarbeitet', (string) $stats['processed']],
|
||||
['Fehler', (string) $stats['errors']],
|
||||
['Mit Punkten', (string) $stats['with_points']],
|
||||
['Mit Partnern', (string) $stats['with_partners']],
|
||||
['Mit Abos', (string) $stats['with_abos']],
|
||||
['Im Ranking', (string) $ranked],
|
||||
];
|
||||
|
||||
if (! $force) {
|
||||
$tableRows[] = ['Neupartner-Trackings nachgezogen', (string) $stats['tracking_partner_added']];
|
||||
$tableRows[] = ['Neuabo-Trackings nachgezogen', (string) $stats['tracking_abo_added']];
|
||||
$tableRows[] = ['FK Partner (akkum.) repariert', (string) $stats['repair_partner_fk']];
|
||||
$tableRows[] = ['FK Abo (akkum.) repariert', (string) $stats['repair_abo_fk']];
|
||||
$tableRows[] = ['FK Partner (Einmal) repariert', (string) $stats['repair_onetime_partner_fk']];
|
||||
$tableRows[] = ['FK Abo (Einmal) repariert', (string) $stats['repair_onetime_abo_fk']];
|
||||
$tableRows[] = ['Neue SV-Log-Eintraege', (string) $stats['sv_logs_added']];
|
||||
}
|
||||
|
||||
$this->table(['Metrik', 'Wert'], $tableRows);
|
||||
|
||||
if (! empty($errors)) {
|
||||
$this->error('Fehler:');
|
||||
foreach ($errors as $err) {
|
||||
$this->line(" - {$err}");
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Fertig.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
196
app/Console/Commands/IncentiveDebugTrackPartner.php
Normal file
196
app/Console/Commands/IncentiveDebugTrackPartner.php
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveNewPartner;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class IncentiveDebugTrackPartner extends Command
|
||||
{
|
||||
protected $signature = 'incentive:debug-track-partner {order_id : Shopping Order ID}';
|
||||
|
||||
protected $description = 'Debuggt trackNewPartner Schritt fuer Schritt fuer eine bestimmte Bestellung';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$order_id = $this->argument('order_id');
|
||||
$shopping_order = ShoppingOrder::find($order_id);
|
||||
|
||||
if (! $shopping_order) {
|
||||
$this->error("Shopping Order #{$order_id} nicht gefunden.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("=== Debug trackNewPartner fuer Order #{$order_id} ===");
|
||||
$this->newLine();
|
||||
|
||||
// 1. Bestelldaten
|
||||
$this->info('[1] Bestelldaten:');
|
||||
$this->table(['Feld', 'Wert'], [
|
||||
['id', $shopping_order->id],
|
||||
['auth_user_id', $shopping_order->auth_user_id ?? 'NULL'],
|
||||
['member_id', $shopping_order->member_id ?? 'NULL'],
|
||||
['payment_for', $shopping_order->payment_for],
|
||||
['paid', $shopping_order->paid],
|
||||
['txaction', $shopping_order->txaction],
|
||||
['mode', $shopping_order->mode],
|
||||
['created_at', $shopping_order->created_at],
|
||||
]);
|
||||
|
||||
// 2. Prüfe payment_for == 1 (Voraussetzung im Payment.php)
|
||||
if ($shopping_order->payment_for != 1) {
|
||||
$this->warn("[!] payment_for = {$shopping_order->payment_for} (nicht 1/registration). trackNewPartner wird nur bei payment_for=1 aufgerufen!");
|
||||
}
|
||||
|
||||
// 3. Neuer User
|
||||
$this->newLine();
|
||||
$this->info('[2] Neuer User (auth_user_id):');
|
||||
|
||||
if (! $shopping_order->auth_user_id) {
|
||||
$this->error(' auth_user_id ist NULL -> ABBRUCH (return)');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$new_user = User::find($shopping_order->auth_user_id);
|
||||
if (! $new_user) {
|
||||
$this->error(" User #{$shopping_order->auth_user_id} nicht gefunden -> ABBRUCH (return)");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->table(['Feld', 'Wert'], [
|
||||
['id', $new_user->id],
|
||||
['email', $new_user->email],
|
||||
['m_sponsor', $new_user->m_sponsor ?? 'NULL'],
|
||||
['active', $new_user->active],
|
||||
['created_at', $new_user->created_at],
|
||||
]);
|
||||
|
||||
if (! $new_user->m_sponsor) {
|
||||
$this->error(' m_sponsor ist NULL -> ABBRUCH (return)');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$sponsor_id = $new_user->m_sponsor;
|
||||
$this->info(" Sponsor ID: {$sponsor_id}");
|
||||
|
||||
// 4. Registration Date
|
||||
$registration_date = $shopping_order->created_at;
|
||||
$this->newLine();
|
||||
$this->info("[3] Registration Date: {$registration_date}");
|
||||
|
||||
// 5. Aktive Incentives
|
||||
$this->newLine();
|
||||
$this->info('[4] Aktive Incentives pruefen:');
|
||||
|
||||
$all_incentives = Incentive::query()->get();
|
||||
$this->info(" Incentives gesamt: {$all_incentives->count()}");
|
||||
|
||||
foreach ($all_incentives as $incentive) {
|
||||
$is_active = $incentive->status == 1;
|
||||
$in_range = $registration_date >= $incentive->qualification_start
|
||||
&& $registration_date <= $incentive->qualification_end;
|
||||
|
||||
$status_icon = $is_active ? 'AKTIV' : 'INAKTIV';
|
||||
$range_icon = $in_range ? 'IM ZEITRAUM' : 'AUSSERHALB';
|
||||
|
||||
$this->table(['Feld', 'Wert'], [
|
||||
['Incentive', "#{$incentive->id}: {$incentive->name}"],
|
||||
['Status', "{$incentive->status} ({$status_icon})"],
|
||||
['qualification_start', $incentive->qualification_start],
|
||||
['qualification_end', $incentive->qualification_end],
|
||||
['Registration Date', "{$registration_date} ({$range_icon})"],
|
||||
]);
|
||||
|
||||
if (! $is_active) {
|
||||
$this->warn(' -> Uebersprungen: Incentive nicht aktiv');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $in_range) {
|
||||
$this->warn(' -> Uebersprungen: Registration Date ausserhalb Qualifikationszeitraum');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->info(" -> MATCH! Incentive #{$incentive->id} ist aktiv und Registration Date liegt im Zeitraum.");
|
||||
|
||||
// 6. Participant prüfen
|
||||
$this->newLine();
|
||||
$this->info("[5] Participant-Check: Sponsor #{$sponsor_id} in Incentive #{$incentive->id}");
|
||||
|
||||
$participant = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->where('user_id', $sponsor_id)
|
||||
->first();
|
||||
|
||||
if (! $participant) {
|
||||
$this->error(" Sponsor #{$sponsor_id} ist KEIN Teilnehmer in Incentive #{$incentive->id} -> SKIP");
|
||||
|
||||
// Zeige alle Teilnehmer-User-IDs
|
||||
$participant_ids = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
$this->info(' Teilnehmer User-IDs: '.implode(', ', array_slice($participant_ids, 0, 20))
|
||||
.(count($participant_ids) > 20 ? '... (+'.count($participant_ids) - 20 .')' : ''));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->info(" Participant gefunden: #{$participant->id} (User #{$participant->user_id})");
|
||||
$this->table(['Feld', 'Wert'], [
|
||||
['participant.id', $participant->id],
|
||||
['user_id', $participant->user_id],
|
||||
['total_points', $participant->total_points],
|
||||
['qualified_partners', $participant->qualified_partners],
|
||||
['accepted_terms_at', $participant->accepted_terms_at ?? 'NULL'],
|
||||
]);
|
||||
|
||||
// 7. Tracking-Eintrag prüfen
|
||||
$this->newLine();
|
||||
$this->info('[6] Tracking-Eintrag (incentive_new_partners):');
|
||||
|
||||
$existing = IncentiveNewPartner::where('participant_id', $participant->id)
|
||||
->where('user_id', $new_user->id)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$this->warn(" Eintrag existiert bereits: #{$existing->id} (erstellt: {$existing->created_at})");
|
||||
} else {
|
||||
$this->info(' Kein Eintrag vorhanden -> wuerde neu erstellt werden.');
|
||||
}
|
||||
|
||||
// 8. Zusammenfassung
|
||||
$this->newLine();
|
||||
$this->info('=== ERGEBNIS ===');
|
||||
$this->info('trackNewPartner WUERDE erfolgreich laufen fuer:');
|
||||
$this->info(" Neuer Partner: User #{$new_user->id} ({$new_user->email})");
|
||||
$this->info(" Sponsor/Teilnehmer: User #{$sponsor_id} (Participant #{$participant->id})");
|
||||
$this->info(" Incentive: #{$incentive->id} ({$incentive->name})");
|
||||
$this->info(" Einmalpunkte: {$incentive->points_partner_onetime}");
|
||||
}
|
||||
|
||||
// Prüfe den Query wie er im Code steht
|
||||
$this->newLine();
|
||||
$this->info('[7] Exakter Query wie im Code:');
|
||||
$matched_incentives = Incentive::query()
|
||||
->active()
|
||||
->where('qualification_start', '<=', $registration_date)
|
||||
->where('qualification_end', '>=', $registration_date)
|
||||
->get();
|
||||
$this->info(" Incentive::active()->where(start <= {$registration_date})->where(end >= {$registration_date})");
|
||||
$this->info(" Ergebnis: {$matched_incentives->count()} Incentive(s)");
|
||||
foreach ($matched_incentives as $mi) {
|
||||
$this->info(" -> #{$mi->id}: {$mi->name}");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
241
app/Console/Commands/IncentiveDebugTrackSalesVolume.php
Normal file
241
app/Console/Commands/IncentiveDebugTrackSalesVolume.php
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveNewAbo;
|
||||
use App\Models\IncentiveNewPartner;
|
||||
use App\Models\IncentivePointsLog;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class IncentiveDebugTrackSalesVolume extends Command
|
||||
{
|
||||
protected $signature = 'incentive:debug-track-sv {sv_id : UserSalesVolume ID}';
|
||||
|
||||
protected $description = 'Debuggt trackSalesVolume Schritt fuer Schritt fuer einen bestimmten SalesVolume-Eintrag';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$sv_id = $this->argument('sv_id');
|
||||
$usv = UserSalesVolume::find($sv_id);
|
||||
|
||||
if (! $usv) {
|
||||
$this->error("UserSalesVolume #{$sv_id} nicht gefunden.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("=== Debug trackSalesVolume fuer USV #{$sv_id} ===");
|
||||
$this->newLine();
|
||||
|
||||
// 1. SalesVolume-Daten
|
||||
$this->info('[1] SalesVolume-Daten:');
|
||||
$this->table(['Feld', 'Wert'], [
|
||||
['id', $usv->id],
|
||||
['user_id', $usv->user_id ?? 'NULL'],
|
||||
['shopping_order_id', $usv->shopping_order_id ?? 'NULL'],
|
||||
['user_invoice_id', $usv->user_invoice_id ?? 'NULL'],
|
||||
['month', $usv->month ?? 'NULL'],
|
||||
['year', $usv->year ?? 'NULL'],
|
||||
['points', $usv->getRawOriginal('points') ?? 'NULL'],
|
||||
['status', $usv->status.' ('.($usv->getStatusType() ?: '-').')'],
|
||||
['status_points', $usv->status_points ?? 'NULL'],
|
||||
['status_turnover', $usv->status_turnover ?? 'NULL'],
|
||||
['message', $usv->message ?? 'NULL'],
|
||||
]);
|
||||
|
||||
// 2. Fruehe Abbruch-Checks
|
||||
$month = $usv->month;
|
||||
$year = $usv->year;
|
||||
|
||||
if (! $month || ! $year) {
|
||||
$this->error('[ABBRUCH] month oder year ist NULL -> return');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$points = (int) abs($usv->getRawOriginal('points') ?? 0);
|
||||
if ($points <= 0) {
|
||||
$this->error("[ABBRUCH] points = {$points} (<= 0) -> return");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(" Effektive Punkte: {$points}");
|
||||
|
||||
// 3. Aktive Incentives
|
||||
$this->newLine();
|
||||
$this->info('[2] Aktive Incentives:');
|
||||
$active_incentives = Incentive::query()->active()->get();
|
||||
$this->info(" Anzahl aktive: {$active_incentives->count()}");
|
||||
|
||||
foreach ($active_incentives as $incentive) {
|
||||
$in_scope = $incentive->isDateInScope($month, $year);
|
||||
$scope_label = $in_scope ? 'IM SCOPE' : 'AUSSERHALB';
|
||||
$this->info(" #{$incentive->id} {$incentive->name}: {$month}/{$year} -> {$scope_label}");
|
||||
$this->info(" Qualification: {$incentive->qualification_start} - {$incentive->qualification_end}, Calc End: {$incentive->calculation_end}");
|
||||
}
|
||||
|
||||
// ===== TEIL A: Neupartner-Check =====
|
||||
$this->newLine();
|
||||
$this->info('========================================');
|
||||
$this->info('[A] NEUPARTNER-CHECK: Ist User #'.$usv->user_id.' ein gettrackter Neupartner?');
|
||||
$this->info('========================================');
|
||||
|
||||
$partner_trackings = IncentiveNewPartner::where('user_id', $usv->user_id)
|
||||
->with('participant.incentive')
|
||||
->get();
|
||||
|
||||
$this->info(" IncentiveNewPartner-Eintraege fuer user_id={$usv->user_id}: {$partner_trackings->count()}");
|
||||
|
||||
if ($partner_trackings->isEmpty()) {
|
||||
$this->warn(' -> User ist KEIN gettrackter Neupartner in irgendeinem Incentive.');
|
||||
}
|
||||
|
||||
foreach ($partner_trackings as $tracking) {
|
||||
$participant = $tracking->participant;
|
||||
$incentive = $participant->incentive ?? null;
|
||||
|
||||
$this->newLine();
|
||||
$this->table(['Feld', 'Wert'], [
|
||||
['NewPartner #', $tracking->id],
|
||||
['participant_id', $tracking->participant_id],
|
||||
['Participant User', $participant->user_id],
|
||||
['Incentive', $incentive ? "#{$incentive->id}: {$incentive->name}" : 'NULL'],
|
||||
['Incentive Status', $incentive ? $incentive->status : 'NULL'],
|
||||
['Incentive aktiv?', $incentive && $incentive->status == 1 ? 'JA' : 'NEIN'],
|
||||
]);
|
||||
|
||||
if (! $incentive || $incentive->status != 1) {
|
||||
$this->warn(' -> Incentive nicht aktiv -> SKIP');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$in_scope = $incentive->isDateInScope($month, $year);
|
||||
$this->info(" isDateInScope({$month}, {$year}): ".($in_scope ? 'JA' : 'NEIN'));
|
||||
|
||||
if (! $in_scope) {
|
||||
$this->warn(' -> Monat/Jahr ausserhalb Scope -> SKIP');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Duplikat-Check
|
||||
$exists = IncentivePointsLog::where('participant_id', $participant->id)
|
||||
->where('user_sales_volume_id', $usv->id)
|
||||
->where('is_storno', false)
|
||||
->exists();
|
||||
|
||||
$this->info(' Log-Eintrag existiert bereits: '.($exists ? 'JA (Duplikat -> kein neuer Eintrag)' : 'NEIN -> wuerde erstellt'));
|
||||
|
||||
$this->info(' ==> MATCH! Punkte wuerden Participant #'.$participant->id." (User #{$participant->user_id}) gutgeschrieben");
|
||||
$this->info(" Typ: partner, Punkte: {$points} (accumulated)");
|
||||
}
|
||||
|
||||
// ===== TEIL B: Neuabo-Check =====
|
||||
$this->newLine();
|
||||
$this->info('========================================');
|
||||
$this->info('[B] NEUABO-CHECK: Stammt die Bestellung von einem gettrackten Abo-Kunden?');
|
||||
$this->info('========================================');
|
||||
|
||||
if (! $usv->shopping_order_id) {
|
||||
$this->warn(' shopping_order_id ist NULL -> Abo-Check uebersprungen.');
|
||||
} else {
|
||||
$order = ShoppingOrder::find($usv->shopping_order_id);
|
||||
|
||||
if (! $order) {
|
||||
$this->error(" ShoppingOrder #{$usv->shopping_order_id} nicht gefunden.");
|
||||
} else {
|
||||
$this->table(['Feld', 'Wert'], [
|
||||
['Order ID', $order->id],
|
||||
['shopping_user_id', $order->shopping_user_id ?? 'NULL'],
|
||||
['auth_user_id', $order->auth_user_id ?? 'NULL'],
|
||||
['member_id', $order->member_id ?? 'NULL'],
|
||||
['payment_for', $order->payment_for],
|
||||
['is_abo', $order->is_abo ? 'JA' : 'NEIN'],
|
||||
]);
|
||||
|
||||
if (! $order->shopping_user_id) {
|
||||
$this->warn(' shopping_user_id ist NULL -> kein Abo-Matching moeglich.');
|
||||
} else {
|
||||
$abo_trackings = IncentiveNewAbo::whereHas(
|
||||
'userAbo',
|
||||
fn ($q) => $q->where('shopping_user_id', $order->shopping_user_id)
|
||||
)
|
||||
->with('participant.incentive', 'userAbo')
|
||||
->get();
|
||||
|
||||
$this->info(" IncentiveNewAbo mit shopping_user_id={$order->shopping_user_id}: {$abo_trackings->count()}");
|
||||
|
||||
if ($abo_trackings->isEmpty()) {
|
||||
$this->warn(' -> Keine gettrackten Abos fuer diesen Kunden.');
|
||||
}
|
||||
|
||||
foreach ($abo_trackings as $tracking) {
|
||||
$participant = $tracking->participant;
|
||||
$incentive = $participant->incentive ?? null;
|
||||
$abo = $tracking->userAbo;
|
||||
|
||||
$this->newLine();
|
||||
$this->table(['Feld', 'Wert'], [
|
||||
['NewAbo #', $tracking->id],
|
||||
['user_abo_id', $tracking->user_abo_id],
|
||||
['Abo shopping_user_id', $abo ? $abo->shopping_user_id : 'NULL'],
|
||||
['participant_id', $tracking->participant_id],
|
||||
['Participant User', $participant->user_id],
|
||||
['Incentive', $incentive ? "#{$incentive->id}: {$incentive->name}" : 'NULL'],
|
||||
['Incentive aktiv?', $incentive && $incentive->status == 1 ? 'JA' : 'NEIN'],
|
||||
]);
|
||||
|
||||
if (! $incentive || $incentive->status != 1) {
|
||||
$this->warn(' -> Incentive nicht aktiv -> SKIP');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$in_scope = $incentive->isDateInScope($month, $year);
|
||||
$this->info(" isDateInScope({$month}, {$year}): ".($in_scope ? 'JA' : 'NEIN'));
|
||||
|
||||
if (! $in_scope) {
|
||||
$this->warn(' -> Monat/Jahr ausserhalb Scope -> SKIP');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = IncentivePointsLog::where('participant_id', $participant->id)
|
||||
->where('user_sales_volume_id', $usv->id)
|
||||
->where('is_storno', false)
|
||||
->exists();
|
||||
|
||||
$this->info(' Log-Eintrag existiert bereits: '.($exists ? 'JA (Duplikat)' : 'NEIN -> wuerde erstellt'));
|
||||
|
||||
$this->info(' ==> MATCH! Punkte wuerden Participant #'.$participant->id." (User #{$participant->user_id}) gutgeschrieben");
|
||||
$this->info(" Typ: abo, Punkte: {$points} (accumulated)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Zusammenfassung =====
|
||||
$this->newLine();
|
||||
$this->info('=== ZUSAMMENFASSUNG ===');
|
||||
|
||||
$total_partner = $partner_trackings->filter(function ($t) use ($month, $year) {
|
||||
return $t->participant->incentive
|
||||
&& $t->participant->incentive->status == 1
|
||||
&& $t->participant->incentive->isDateInScope($month, $year);
|
||||
})->count();
|
||||
|
||||
$this->info(" Neupartner-Matches: {$total_partner}");
|
||||
$this->info(' Neuabo-Matches: siehe oben');
|
||||
|
||||
if ($total_partner === 0) {
|
||||
$this->warn(' -> Keine Punkte wuerden vergeben (kein Match).');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
419
app/Console/Commands/PayoneFailedPaypalReport.php
Normal file
419
app/Console/Commands/PayoneFailedPaypalReport.php
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PayoneFailedPaypalReport extends Command
|
||||
{
|
||||
protected $signature = 'payone:failed-paypal-report
|
||||
{--from=2026-04-02 : Start-Datum (YYYY-MM-DD)}
|
||||
{--to= : End-Datum (YYYY-MM-DD), Standard: heute}
|
||||
{--output=storage/reports/paypal-failed-report.csv : Ausgabedatei}';
|
||||
|
||||
protected $description = 'Erstellt einen Schadenbericht über fehlgeschlagene PayPal-Zahlungen (Error 923)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$from = $this->option('from');
|
||||
$to = $this->option('to') ?: now()->format('Y-m-d');
|
||||
$outputPath = $this->option('output');
|
||||
|
||||
$this->info("Schadenbericht PayPal-Ausfälle: {$from} bis {$to}");
|
||||
$this->newLine();
|
||||
|
||||
$orders = $this->getAffectedOrders($from, $to);
|
||||
|
||||
if ($orders->isEmpty()) {
|
||||
$this->warn('Keine fehlgeschlagenen PayPal-Zahlungen im angegebenen Zeitraum gefunden.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->displaySummary($orders, $from, $to);
|
||||
|
||||
$fullPath = base_path($outputPath);
|
||||
$dir = dirname($fullPath);
|
||||
if (! is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$this->writeCsvReport($fullPath, $orders, $from, $to);
|
||||
$this->writeTxtReport(str_replace('.csv', '.txt', $fullPath), $orders, $from, $to);
|
||||
$this->writeEmailLists($dir, $orders);
|
||||
|
||||
$this->newLine();
|
||||
$this->info("CSV-Bericht: {$fullPath}");
|
||||
$this->info('TXT-Bericht: ' . str_replace('.csv', '.txt', $fullPath));
|
||||
$this->info("E-Mail Berater: {$dir}/emails-berater.csv");
|
||||
$this->info("E-Mail Shop-Kunden: {$dir}/emails-shop-kunden.csv");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function getAffectedOrders(string $from, string $to): \Illuminate\Support\Collection
|
||||
{
|
||||
return DB::table('shopping_orders')
|
||||
->join('shopping_payments', function ($join) {
|
||||
$join->on('shopping_payments.shopping_order_id', '=', 'shopping_orders.id')
|
||||
->where('shopping_payments.clearingtype', '=', 'wlt')
|
||||
->where('shopping_payments.wallettype', '=', 'PPE');
|
||||
})
|
||||
->join('payment_transactions', function ($join) {
|
||||
$join->on('payment_transactions.shopping_payment_id', '=', 'shopping_payments.id')
|
||||
->where('payment_transactions.errorcode', '=', 923);
|
||||
})
|
||||
->join('shopping_users', 'shopping_users.id', '=', 'shopping_orders.shopping_user_id')
|
||||
->whereBetween('payment_transactions.created_at', ["{$from} 00:00:00", "{$to} 23:59:59"])
|
||||
->select(
|
||||
'shopping_orders.id as order_id',
|
||||
'shopping_orders.total_shipping',
|
||||
'shopping_orders.paid',
|
||||
'shopping_orders.txaction',
|
||||
'shopping_orders.mode',
|
||||
'shopping_orders.payment_for',
|
||||
'shopping_orders.auth_user_id',
|
||||
'shopping_orders.created_at as order_date',
|
||||
'shopping_users.billing_email',
|
||||
'shopping_users.billing_firstname',
|
||||
'shopping_users.billing_lastname',
|
||||
'shopping_payments.id as payment_id',
|
||||
'shopping_payments.reference',
|
||||
'shopping_payments.amount as amount_cents',
|
||||
'shopping_payments.currency',
|
||||
'payment_transactions.id as tx_id',
|
||||
'payment_transactions.errorcode',
|
||||
'payment_transactions.errormessage',
|
||||
'payment_transactions.created_at as error_date',
|
||||
)
|
||||
->orderBy('payment_transactions.created_at')
|
||||
->get();
|
||||
}
|
||||
|
||||
private function displaySummary(\Illuminate\Support\Collection $rows, string $from, string $to): void
|
||||
{
|
||||
$uniqueOrders = $rows->unique('order_id');
|
||||
$paidOrders = $uniqueOrders->where('paid', 1);
|
||||
$unpaidOrders = $uniqueOrders->where('paid', 0);
|
||||
|
||||
$this->table(
|
||||
['Kennzahl', 'Wert'],
|
||||
[
|
||||
['Zeitraum', "{$from} bis {$to}"],
|
||||
['Fehlgeschlagene Transaktionen (Error 923)', $rows->count()],
|
||||
['Betroffene Bestellungen (eindeutig)', $uniqueOrders->count()],
|
||||
['Davon nachträglich bezahlt (andere Zahlungsart)', $paidOrders->count()],
|
||||
['Nicht bezahlt (offen/verloren)', $unpaidOrders->count()],
|
||||
['Summe nicht bezahlter Bestellungen', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.') . ' EUR'],
|
||||
['Summe aller betroffenen Bestellungen', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '.') . ' EUR'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function writeCsvReport(string $path, \Illuminate\Support\Collection $rows, string $from, string $to): void
|
||||
{
|
||||
$fp = fopen($path, 'w');
|
||||
|
||||
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
fputcsv($fp, [
|
||||
'Fehler-Datum',
|
||||
'Bestell-Nr',
|
||||
'Bestell-Datum',
|
||||
'Transaktions-ID',
|
||||
'Payment-Referenz',
|
||||
'Betrag (EUR)',
|
||||
'Fehlercode',
|
||||
'Fehlermeldung',
|
||||
'Modus',
|
||||
'Nachträglich bezahlt',
|
||||
'Aktueller Status',
|
||||
], ';');
|
||||
|
||||
$uniqueOrders = $rows->unique('order_id');
|
||||
$unpaidOrders = $uniqueOrders->where('paid', 0);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($fp, [
|
||||
$row->error_date,
|
||||
$row->order_id,
|
||||
$row->order_date,
|
||||
$row->tx_id,
|
||||
$row->reference,
|
||||
number_format($row->total_shipping, 2, ',', ''),
|
||||
$row->errorcode,
|
||||
$row->errormessage,
|
||||
$row->mode,
|
||||
$row->paid ? 'Ja' : 'Nein',
|
||||
$row->txaction,
|
||||
], ';');
|
||||
}
|
||||
|
||||
fputcsv($fp, [], ';');
|
||||
fputcsv($fp, ['ZUSAMMENFASSUNG'], ';');
|
||||
fputcsv($fp, ['Zeitraum', "{$from} bis {$to}"], ';');
|
||||
fputcsv($fp, ['Fehlgeschlagene Transaktionen', $rows->count()], ';');
|
||||
fputcsv($fp, ['Betroffene Bestellungen', $uniqueOrders->count()], ';');
|
||||
fputcsv($fp, ['Nicht bezahlt (offen/verloren)', $unpaidOrders->count()], ';');
|
||||
fputcsv($fp, ['Summe nicht bezahlter Bestellungen', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
|
||||
fputcsv($fp, ['Summe aller betroffenen Bestellungen', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
|
||||
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
private function writeTxtReport(string $path, \Illuminate\Support\Collection $rows, string $from, string $to): void
|
||||
{
|
||||
$uniqueOrders = $rows->unique('order_id');
|
||||
$paidOrders = $uniqueOrders->where('paid', 1);
|
||||
$unpaidOrders = $uniqueOrders->where('paid', 0);
|
||||
|
||||
$lines = [];
|
||||
$lines[] = '================================================================================';
|
||||
$lines[] = ' SCHADENBERICHT: Fehlgeschlagene PayPal-Zahlungen (PAYONE Error 923)';
|
||||
$lines[] = '================================================================================';
|
||||
$lines[] = '';
|
||||
$lines[] = "Zeitraum: {$from} bis {$to}";
|
||||
$lines[] = 'Erstellt am: ' . now()->format('d.m.Y H:i:s');
|
||||
$lines[] = 'Ursache: PayPal-Kontoverknüpfung bei PAYONE nicht migriert (Vertragsübernahme GmbH)';
|
||||
$lines[] = 'Portal-ID: 2030693';
|
||||
$lines[] = 'Merchant-ID: 42504';
|
||||
$lines[] = 'Sub-Account-ID: 43065';
|
||||
$lines[] = '';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = ' ZUSAMMENFASSUNG';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = '';
|
||||
$lines[] = sprintf(' Fehlgeschlagene Transaktionen (Error 923): %d', $rows->count());
|
||||
$lines[] = sprintf(' Betroffene Bestellungen (eindeutig): %d', $uniqueOrders->count());
|
||||
$lines[] = sprintf(' Davon nachträglich bezahlt (andere Methode): %d', $paidOrders->count());
|
||||
$lines[] = sprintf(' Nicht bezahlt (offen/verloren): %d', $unpaidOrders->count());
|
||||
$lines[] = '';
|
||||
$lines[] = sprintf(' Summe nicht bezahlter Bestellungen: %s EUR', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.'));
|
||||
$lines[] = sprintf(' Summe nachträglich bezahlter Bestellungen: %s EUR', number_format($paidOrders->sum('total_shipping'), 2, ',', '.'));
|
||||
$lines[] = sprintf(' Summe ALLER betroffenen Bestellungen: %s EUR', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '.'));
|
||||
$lines[] = '';
|
||||
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = ' AUFSCHLÜSSELUNG NACH TAG';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = '';
|
||||
|
||||
$byDate = $rows->groupBy(fn($r) => substr($r->error_date, 0, 10));
|
||||
foreach ($byDate as $date => $dayRows) {
|
||||
$dayOrders = $dayRows->unique('order_id');
|
||||
$dayUnpaid = $dayOrders->where('paid', 0);
|
||||
$lines[] = sprintf(
|
||||
' %s: %3d Fehler | %3d Bestellungen | %3d nicht bezahlt | %s EUR offen',
|
||||
$date,
|
||||
$dayRows->count(),
|
||||
$dayOrders->count(),
|
||||
$dayUnpaid->count(),
|
||||
number_format($dayUnpaid->sum('total_shipping'), 2, ',', '.')
|
||||
);
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = ' NICHT BEZAHLTE BESTELLUNGEN (DETAIL)';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = '';
|
||||
$lines[] = sprintf(
|
||||
' %-12s %-20s %-18s %-14s %s',
|
||||
'Bestell-Nr',
|
||||
'Datum',
|
||||
'Referenz',
|
||||
'Betrag (EUR)',
|
||||
'Status'
|
||||
);
|
||||
$lines[] = ' ' . str_repeat('-', 80);
|
||||
|
||||
foreach ($unpaidOrders->sortBy('order_date') as $order) {
|
||||
$lines[] = sprintf(
|
||||
' %-12s %-20s %-18s %14s %s',
|
||||
$order->order_id,
|
||||
$order->order_date,
|
||||
$order->reference,
|
||||
number_format($order->total_shipping, 2, ',', '.'),
|
||||
$order->txaction
|
||||
);
|
||||
}
|
||||
|
||||
$lines[] = ' ' . str_repeat('-', 80);
|
||||
$lines[] = sprintf(
|
||||
' %-12s %-20s %-18s %14s',
|
||||
'GESAMT',
|
||||
'',
|
||||
$unpaidOrders->count() . ' Bestellungen',
|
||||
number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.')
|
||||
);
|
||||
|
||||
$lines[] = '';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = ' NACHTRÄGLICH BEZAHLTE BESTELLUNGEN (andere Zahlungsart)';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = '';
|
||||
|
||||
if ($paidOrders->isEmpty()) {
|
||||
$lines[] = ' Keine.';
|
||||
} else {
|
||||
$lines[] = sprintf(
|
||||
' %-12s %-20s %-18s %-14s %s',
|
||||
'Bestell-Nr',
|
||||
'Datum',
|
||||
'Referenz',
|
||||
'Betrag (EUR)',
|
||||
'Status'
|
||||
);
|
||||
$lines[] = ' ' . str_repeat('-', 80);
|
||||
|
||||
foreach ($paidOrders->sortBy('order_date') as $order) {
|
||||
$lines[] = sprintf(
|
||||
' %-12s %-20s %-18s %14s %s',
|
||||
$order->order_id,
|
||||
$order->order_date,
|
||||
$order->reference,
|
||||
number_format($order->total_shipping, 2, ',', '.'),
|
||||
$order->txaction
|
||||
);
|
||||
}
|
||||
|
||||
$lines[] = ' ' . str_repeat('-', 80);
|
||||
$lines[] = sprintf(
|
||||
' %-12s %-20s %-18s %14s',
|
||||
'GESAMT',
|
||||
'',
|
||||
$paidOrders->count() . ' Bestellungen',
|
||||
number_format($paidOrders->sum('total_shipping'), 2, ',', '.')
|
||||
);
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
$this->appendEmailSectionToTxt($lines, $unpaidOrders);
|
||||
$lines[] = '';
|
||||
$lines[] = '================================================================================';
|
||||
$lines[] = ' Ende des Berichts';
|
||||
$lines[] = '================================================================================';
|
||||
$lines[] = '';
|
||||
|
||||
file_put_contents($path, implode("\n", $lines));
|
||||
}
|
||||
|
||||
private function appendEmailSectionToTxt(array &$lines, \Illuminate\Support\Collection $unpaidOrders): void
|
||||
{
|
||||
$berater = $unpaidOrders->filter(fn($o) => ! empty($o->auth_user_id))->sortBy('billing_email');
|
||||
$shopKunden = $unpaidOrders->filter(fn($o) => empty($o->auth_user_id))->sortBy('billing_email');
|
||||
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = ' BETROFFENE BERATER (mit Auth-User-ID) - nicht bezahlt';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = '';
|
||||
$lines[] = sprintf(' %-12s %-8s %-30s %-30s %14s', 'Bestell-Nr', 'User-ID', 'Name', 'E-Mail', 'Betrag (EUR)');
|
||||
$lines[] = ' ' . str_repeat('-', 100);
|
||||
|
||||
$beraterSum = 0;
|
||||
foreach ($berater as $order) {
|
||||
$name = trim(($order->billing_firstname ?? '') . ' ' . ($order->billing_lastname ?? ''));
|
||||
$lines[] = sprintf(
|
||||
' %-12s %-8s %-30s %-30s %14s',
|
||||
$order->order_id,
|
||||
$order->auth_user_id,
|
||||
mb_substr($name, 0, 28),
|
||||
mb_substr($order->billing_email ?? '-', 0, 28),
|
||||
number_format($order->total_shipping, 2, ',', '.')
|
||||
);
|
||||
$beraterSum += $order->total_shipping;
|
||||
}
|
||||
|
||||
$lines[] = ' ' . str_repeat('-', 100);
|
||||
$lines[] = sprintf(' %-12s %-8s %-30s %-30s %14s', 'GESAMT', '', $berater->count() . ' Bestellungen', '', number_format($beraterSum, 2, ',', '.'));
|
||||
|
||||
$lines[] = '';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = ' BETROFFENE SHOP-KUNDEN (ohne Auth-User-ID) - nicht bezahlt';
|
||||
$lines[] = '--------------------------------------------------------------------------------';
|
||||
$lines[] = '';
|
||||
$lines[] = sprintf(' %-12s %-30s %-30s %14s', 'Bestell-Nr', 'Name', 'E-Mail', 'Betrag (EUR)');
|
||||
$lines[] = ' ' . str_repeat('-', 90);
|
||||
|
||||
$shopSum = 0;
|
||||
foreach ($shopKunden as $order) {
|
||||
$name = trim(($order->billing_firstname ?? '') . ' ' . ($order->billing_lastname ?? ''));
|
||||
$lines[] = sprintf(
|
||||
' %-12s %-30s %-30s %14s',
|
||||
$order->order_id,
|
||||
mb_substr($name, 0, 28),
|
||||
mb_substr($order->billing_email ?? '-', 0, 28),
|
||||
number_format($order->total_shipping, 2, ',', '.')
|
||||
);
|
||||
$shopSum += $order->total_shipping;
|
||||
}
|
||||
|
||||
$lines[] = ' ' . str_repeat('-', 90);
|
||||
$lines[] = sprintf(' %-12s %-30s %-30s %14s', 'GESAMT', $shopKunden->count() . ' Bestellungen', '', number_format($shopSum, 2, ',', '.'));
|
||||
}
|
||||
|
||||
private function writeEmailLists(string $dir, \Illuminate\Support\Collection $rows): void
|
||||
{
|
||||
$unpaidOrders = $rows->unique('order_id')->where('paid', 0);
|
||||
|
||||
$berater = $unpaidOrders->filter(fn($o) => ! empty($o->auth_user_id))->sortBy('order_date');
|
||||
$shopKunden = $unpaidOrders->filter(fn($o) => empty($o->auth_user_id))->sortBy('order_date');
|
||||
|
||||
$this->writeEmailCsv("{$dir}/emails-berater.csv", $berater, true);
|
||||
$this->writeEmailCsv("{$dir}/emails-shop-kunden.csv", $shopKunden, false);
|
||||
|
||||
$this->newLine();
|
||||
$this->table(
|
||||
['Kategorie', 'Bestellungen', 'Eindeutige E-Mails', 'Summe (EUR)'],
|
||||
[
|
||||
[
|
||||
'Berater (mit Auth-User-ID)',
|
||||
$berater->count(),
|
||||
$berater->pluck('billing_email')->filter()->unique()->count(),
|
||||
number_format($berater->sum('total_shipping'), 2, ',', '.') . ' EUR',
|
||||
],
|
||||
[
|
||||
'Shop-Kunden (ohne Auth-User-ID)',
|
||||
$shopKunden->count(),
|
||||
$shopKunden->pluck('billing_email')->filter()->unique()->count(),
|
||||
number_format($shopKunden->sum('total_shipping'), 2, ',', '.') . ' EUR',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function writeEmailCsv(string $path, \Illuminate\Support\Collection $orders, bool $includeUserId): void
|
||||
{
|
||||
$fp = fopen($path, 'w');
|
||||
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
$headers = ['Bestell-Nr', 'Bestell-Datum', 'Vorname', 'Nachname', 'E-Mail', 'Betrag (EUR)', 'Status'];
|
||||
if ($includeUserId) {
|
||||
array_splice($headers, 1, 0, 'Auth-User-ID');
|
||||
}
|
||||
fputcsv($fp, $headers, ';');
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$row = [
|
||||
$order->order_id,
|
||||
$order->order_date,
|
||||
$order->billing_firstname ?? '',
|
||||
$order->billing_lastname ?? '',
|
||||
$order->billing_email ?? '',
|
||||
number_format($order->total_shipping, 2, ',', ''),
|
||||
$order->txaction,
|
||||
];
|
||||
if ($includeUserId) {
|
||||
array_splice($row, 1, 0, $order->auth_user_id);
|
||||
}
|
||||
fputcsv($fp, $row, ';');
|
||||
}
|
||||
|
||||
fputcsv($fp, [], ';');
|
||||
fputcsv($fp, ['GESAMT', '', '', '', $orders->count() . ' Bestellungen', number_format($orders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
|
||||
fputcsv($fp, ['Eindeutige E-Mail-Adressen', $orders->pluck('billing_email')->filter()->unique()->count()], ';');
|
||||
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
220
app/Console/Commands/RepairMissingAboFromOrders.php
Normal file
220
app/Console/Commands/RepairMissingAboFromOrders.php
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RepairMissingAboFromOrders extends Command
|
||||
{
|
||||
protected $signature = 'abo:repair-missing
|
||||
{--fix : Reparatur ausfuehren (ohne: nur Abgleich/Vorschau)}
|
||||
{--force : Mit --fix: ohne Rueckfrage (Skripte/CI)}
|
||||
{--since= : Nur Bestellungen mit created_at >= (Y-m-d)}
|
||||
{--until= : Nur Bestellungen mit created_at <= Ende dieses Tages (Y-m-d)}
|
||||
{--order= : Komma-getrennte shopping_order IDs (Filter)}
|
||||
{--mode=live : Modus: live, test, dev oder all}
|
||||
{--stats : Zusaetzliche Statistik: bezahlte Abo-Bestellungen vs. mit/ohne UserAboOrder}';
|
||||
|
||||
protected $description = 'Abgleich und Reparatur: bezahlte Abo-Bestellungen (Checkout) ohne Verknuepfung user_abo_orders — z. B. nach Payone-Callback vor Erfolgs-Redirect';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$missing = $this->queryMissingOrders()->orderBy('id')->get();
|
||||
|
||||
$this->info('Abgleich: Bestellungen mit is_abo, abo_interval>0, als bezahlt markiert, ohne user_abo_orders-Eintrag.');
|
||||
$this->newLine();
|
||||
|
||||
if ($this->option('stats')) {
|
||||
$this->printStats();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
$this->info('Treffer (fehlende Verknuepfung): '.$missing->count());
|
||||
|
||||
if ($missing->isEmpty()) {
|
||||
$this->info('Keine Diskrepanz — nichts zu tun.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['ID', 'shopping_user_id', 'mode', 'txaction', 'paid', 'created_at'],
|
||||
$missing->take(200)->map(fn (ShoppingOrder $o) => [
|
||||
$o->id,
|
||||
$o->shopping_user_id,
|
||||
$o->mode,
|
||||
$o->txaction,
|
||||
$o->paid ? '1' : '0',
|
||||
$o->created_at?->format('Y-m-d H:i'),
|
||||
])
|
||||
);
|
||||
|
||||
if ($missing->count() > 200) {
|
||||
$this->warn('… und weitere '.($missing->count() - 200).' Eintraege (Ausgabe gekuerzt).');
|
||||
}
|
||||
|
||||
if (! $this->option('fix')) {
|
||||
$this->newLine();
|
||||
$this->warn('Trockenlauf. Nutze --fix zur Reparatur (mit Bestaetigung).');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && ! $this->confirm('Wirklich '.$missing->count().' Bestellung(en) reparieren?')) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$ok = 0;
|
||||
$fail = 0;
|
||||
$bar = $this->output->createProgressBar($missing->count());
|
||||
$bar->start();
|
||||
|
||||
foreach ($missing as $order) {
|
||||
try {
|
||||
DB::transaction(function () use ($order) {
|
||||
$this->repairSingleOrder($order);
|
||||
});
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
$fail++;
|
||||
$this->newLine();
|
||||
$this->error("Order #{$order->id}: {$e->getMessage()}");
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
$this->info("Fertig: {$ok} repariert, {$fail} Fehler.");
|
||||
|
||||
return $fail > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Builder<ShoppingOrder>
|
||||
*/
|
||||
private function queryMissingOrders(): \Illuminate\Database\Eloquent\Builder
|
||||
{
|
||||
$q = ShoppingOrder::query()
|
||||
->where('is_abo', true)
|
||||
->where('abo_interval', '>', 0)
|
||||
->where(function ($sub) {
|
||||
$sub->where('paid', true)
|
||||
->orWhere('paid', 1);
|
||||
})
|
||||
->whereIn('txaction', ['paid', 'invoice_paid', 'extern_paid'])
|
||||
->whereNotNull('shopping_user_id')
|
||||
->whereHas('shopping_payments')
|
||||
->whereNotExists(function ($sub) {
|
||||
$sub->select(DB::raw('1'))
|
||||
->from('user_abo_orders')
|
||||
->whereColumn('user_abo_orders.shopping_order_id', 'shopping_orders.id');
|
||||
});
|
||||
|
||||
if ($ids = $this->parseOrderIds()) {
|
||||
$q->whereIn('id', $ids);
|
||||
}
|
||||
|
||||
if ($since = $this->option('since')) {
|
||||
$q->where('created_at', '>=', $since.' 00:00:00');
|
||||
}
|
||||
|
||||
if ($until = $this->option('until')) {
|
||||
$q->where('created_at', '<=', $until.' 23:59:59');
|
||||
}
|
||||
|
||||
$mode = (string) $this->option('mode');
|
||||
if ($mode !== 'all') {
|
||||
$q->where('mode', $mode);
|
||||
}
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
private function parseOrderIds(): array
|
||||
{
|
||||
$raw = $this->option('order');
|
||||
if ($raw === null || $raw === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map('intval', explode(',', (string) $raw))));
|
||||
}
|
||||
|
||||
private function printStats(): void
|
||||
{
|
||||
$mode = (string) $this->option('mode');
|
||||
$base = ShoppingOrder::query()
|
||||
->where('is_abo', true)
|
||||
->where('abo_interval', '>', 0)
|
||||
->where(function ($sub) {
|
||||
$sub->where('paid', true)->orWhere('paid', 1);
|
||||
})
|
||||
->whereIn('txaction', ['paid', 'invoice_paid', 'extern_paid']);
|
||||
|
||||
if ($since = $this->option('since')) {
|
||||
$base->where('created_at', '>=', $since.' 00:00:00');
|
||||
}
|
||||
if ($until = $this->option('until')) {
|
||||
$base->where('created_at', '<=', $until.' 23:59:59');
|
||||
}
|
||||
if ($mode !== 'all') {
|
||||
$base->where('mode', $mode);
|
||||
}
|
||||
if ($ids = $this->parseOrderIds()) {
|
||||
$base->whereIn('id', $ids);
|
||||
}
|
||||
|
||||
$totalPaidAbo = (clone $base)->count();
|
||||
|
||||
$withLink = (clone $base)->whereExists(function ($sub) {
|
||||
$sub->select(DB::raw('1'))
|
||||
->from('user_abo_orders')
|
||||
->whereColumn('user_abo_orders.shopping_order_id', 'shopping_orders.id');
|
||||
})->count();
|
||||
|
||||
$this->table(
|
||||
['Kennzahl', 'Anzahl'],
|
||||
[
|
||||
['Bezahlte Abo-Bestellungen (Filter)', $totalPaidAbo],
|
||||
['Davon mit user_abo_orders', $withLink],
|
||||
['Davon ohne user_abo_orders', max(0, $totalPaidAbo - $withLink)],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function repairSingleOrder(ShoppingOrder $order): void
|
||||
{
|
||||
$payment = ShoppingPayment::query()
|
||||
->where('shopping_order_id', $order->id)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if (! $payment) {
|
||||
throw new \RuntimeException('Kein ShoppingPayment zur Bestellung.');
|
||||
}
|
||||
|
||||
$order->loadMissing(['shopping_user', 'shopping_order_items']);
|
||||
$payment->loadMissing(['payment_transactions']);
|
||||
$payment->setRelation('shopping_order', $order);
|
||||
|
||||
AboHelper::createNewAbo($payment);
|
||||
|
||||
$order->refresh();
|
||||
|
||||
if (! $order->getUserAbo()) {
|
||||
throw new \RuntimeException('createNewAbo hat kein UserAbo erzeugt (pruefen: abo_interval, Bestellpositionen, ShoppingPayment.abo_interval).');
|
||||
}
|
||||
|
||||
AboHelper::setAboActive($order, 2, true);
|
||||
IncentiveTracker::trackAboActivated($order);
|
||||
}
|
||||
}
|
||||
129
app/Console/Commands/RepairMissingInvoices.php
Normal file
129
app/Console/Commands/RepairMissingInvoices.php
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Repositories\InvoiceRepository;
|
||||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RepairMissingInvoices extends Command
|
||||
{
|
||||
protected $signature = 'repair:missing-invoices
|
||||
{--fix : Tatsaechlich reparieren (ohne Flag nur Vorschau)}
|
||||
{--no-mail : Keine Rechnungs-Mails versenden}
|
||||
{--since=2026-02-20 : Ab welchem Datum suchen}';
|
||||
|
||||
protected $description = 'Repariert fehlende Rechnungen und SalesVolumes fuer bezahlte Bestellungen (Bug: addSalesPointsVolumeUser)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$since = $this->option('since') ?? '2026-03-16';
|
||||
$fix = $this->option('fix') ?? false;
|
||||
|
||||
$orders = ShoppingOrder::query()
|
||||
->where('mode', 'live')
|
||||
->where('paid', 0)
|
||||
->where('txaction', 'paid')
|
||||
->where('created_at', '>=', $since)
|
||||
->whereNull('deleted_at')
|
||||
->whereDoesntHave('user_invoice')
|
||||
->whereDoesntHave('user_sales_volume')
|
||||
// ->whereDoesntHave('shopping_payments', fn($q) => $q->where('clearingtype', 'vor'))
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
|
||||
$this->info("Betroffene Bestellungen seit {$since}: {$orders->count()}");
|
||||
|
||||
if ($orders->isEmpty()) {
|
||||
$this->info('Keine betroffenen Bestellungen gefunden.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// Zusammenfassung
|
||||
$total = $orders->sum('total');
|
||||
$byPaymentFor = $orders->groupBy('payment_for')->map->count();
|
||||
$this->table(
|
||||
['payment_for', 'Anzahl'],
|
||||
$byPaymentFor->map(fn ($count, $type) => [$type, $count])->values()
|
||||
);
|
||||
$this->info("Gesamtwert: {$total} EUR");
|
||||
|
||||
if (! $fix) {
|
||||
$this->warn('Trockenlauf! Nutze --fix um die Reparatur durchzufuehren.');
|
||||
$this->newLine();
|
||||
|
||||
// Erste 10 anzeigen
|
||||
$this->table(
|
||||
['ID', 'payment_for', 'total', 'txaction', 'created_at'],
|
||||
$orders->take(100)->map(fn ($o) => [
|
||||
$o->id,
|
||||
$o->payment_for,
|
||||
$o->total,
|
||||
$o->txaction,
|
||||
$o->created_at->format('Y-m-d H:i'),
|
||||
])
|
||||
);
|
||||
|
||||
if ($orders->count() > 100) {
|
||||
$this->info('... und '.($orders->count() - 100).' weitere');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$send_mail = ! $this->option('no-mail');
|
||||
|
||||
if ($send_mail) {
|
||||
$this->info('Rechnungs-Mails werden versendet. Nutze --no-mail um dies zu unterdruecken.');
|
||||
} else {
|
||||
$this->warn('Rechnungs-Mails werden NICHT versendet.');
|
||||
}
|
||||
|
||||
if (! $this->confirm("Wirklich {$orders->count()} Bestellungen reparieren?")) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
$errors = 0;
|
||||
$bar = $this->output->createProgressBar($orders->count());
|
||||
$bar->start();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
try {
|
||||
// 1. SalesVolume erstellen
|
||||
$user_sales_volume = SalesPointsVolume::User($order);
|
||||
|
||||
// 2. Rechnung erstellen (mit Mail-Versand)
|
||||
$invoice_repo = new InvoiceRepository($order);
|
||||
$user_invoice = $invoice_repo->create([
|
||||
'invoice_send_mail' => $send_mail,
|
||||
]);
|
||||
|
||||
// 3. SalesVolume mit Rechnung verknuepfen
|
||||
$user_sales_volume->user_invoice_id = $user_invoice->id;
|
||||
$user_sales_volume->save();
|
||||
|
||||
// 4. Incentive tracking (falls relevant)
|
||||
IncentiveTracker::trackSalesVolume($user_sales_volume);
|
||||
|
||||
$success++;
|
||||
$this->info("Order #{$order->id}: Reparatur erfolgreich");
|
||||
} catch (\Throwable $e) {
|
||||
$errors++;
|
||||
$this->newLine();
|
||||
$this->error("Order #{$order->id}: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
$this->info("Fertig: {$success} repariert, {$errors} Fehler.");
|
||||
|
||||
return $errors > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
}
|
||||
297
app/Console/Commands/RetryFailedPaypalAbos.php
Normal file
297
app/Console/Commands/RetryFailedPaypalAbos.php
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Cron\UserMakeOrder;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\Services\MyLog;
|
||||
use App\Services\Payment;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RetryFailedPaypalAbos extends Command
|
||||
{
|
||||
protected $signature = 'abo:retry-failed-paypal
|
||||
{--dry-run : Nur anzeigen, keine Bestellungen ausführen}
|
||||
{--abo-id= : Nur ein bestimmtes Abo erneut ausführen}';
|
||||
|
||||
protected $description = 'Führt Abo-Bestellungen erneut aus, die aufgrund der PayPal-Panne (Error 923) fehlgeschlagen sind';
|
||||
|
||||
private float $timeStart;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->timeStart = microtime(true);
|
||||
$dryRun = $this->option('dry-run');
|
||||
$singleAboId = $this->option('abo-id');
|
||||
|
||||
\Log::channel('abo_order')->info('RetryFailedPaypalAbos: Gestartet', [
|
||||
'dry_run' => $dryRun,
|
||||
'abo_id' => $singleAboId,
|
||||
]);
|
||||
|
||||
$this->info($dryRun ? '=== DRY-RUN Modus (keine Bestellungen) ===' : '=== LIVE Modus ===');
|
||||
$this->newLine();
|
||||
|
||||
$abos = $this->getAffectedAbos($singleAboId);
|
||||
|
||||
if ($abos->isEmpty()) {
|
||||
$this->warn('Keine betroffenen PayPal-Abos gefunden.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->displayAboList($abos);
|
||||
|
||||
if (! $dryRun && ! $singleAboId) {
|
||||
if (! $this->confirm("Sollen alle {$abos->count()} Abos jetzt erneut ausgeführt werden?")) {
|
||||
$this->info('Abgebrochen.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$results = ['success' => 0, 'error' => 0, 'skipped' => 0];
|
||||
|
||||
foreach ($abos as $userAbo) {
|
||||
if ($dryRun) {
|
||||
$this->info(" [DRY-RUN] Abo #{$userAbo->id} würde ausgeführt werden");
|
||||
$results['skipped']++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->retryAboOrder($userAbo);
|
||||
if ($result) {
|
||||
$results['success']++;
|
||||
} else {
|
||||
$results['error']++;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$results['error']++;
|
||||
\Log::channel('abo_order')->error('RetryFailedPaypalAbos: Exception', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->error(" Abo #{$userAbo->id}: Exception - {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->table(
|
||||
['Ergebnis', 'Anzahl'],
|
||||
[
|
||||
['Erfolgreich', $results['success']],
|
||||
['Fehlgeschlagen', $results['error']],
|
||||
['Übersprungen (Dry-Run)', $results['skipped']],
|
||||
]
|
||||
);
|
||||
|
||||
$executionTime = $this->getExecutionTime();
|
||||
$this->info("Abgeschlossen in {$executionTime}");
|
||||
\Log::channel('abo_order')->info("RetryFailedPaypalAbos: Abgeschlossen in {$executionTime}", $results);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, UserAbo>
|
||||
*/
|
||||
private function getAffectedAbos(?string $singleAboId): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
$query = UserAbo::query()
|
||||
->where('status', 3)
|
||||
->where('active', true)
|
||||
->where('clearingtype', 'wlt')
|
||||
->where('wallettype', 'PPE')
|
||||
->whereRaw("DATE(next_date) = '2026-04-05'")
|
||||
->with(['shopping_user', 'user_abo_items']);
|
||||
|
||||
if ($singleAboId) {
|
||||
$query->where('id', $singleAboId);
|
||||
}
|
||||
|
||||
return $query->orderBy('id')->get();
|
||||
}
|
||||
|
||||
private function displayAboList(\Illuminate\Database\Eloquent\Collection $abos): void
|
||||
{
|
||||
$rows = $abos->map(fn (UserAbo $abo) => [
|
||||
$abo->id,
|
||||
$abo->user_id ?? '-',
|
||||
$abo->is_for,
|
||||
$abo->email,
|
||||
$abo->abo_interval,
|
||||
$abo->getRawOriginal('next_date'),
|
||||
$abo->user_abo_items->count().' Artikel',
|
||||
]);
|
||||
|
||||
$this->table(
|
||||
['Abo-ID', 'User-ID', 'Typ', 'E-Mail', 'Intervall', 'Next-Date', 'Artikel'],
|
||||
$rows->toArray()
|
||||
);
|
||||
|
||||
$this->info("Betroffene Abos: {$abos->count()}");
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function retryAboOrder(UserAbo $userAbo): bool
|
||||
{
|
||||
$this->info(" Verarbeite Abo #{$userAbo->id} ({$userAbo->email})...");
|
||||
|
||||
\Log::channel('abo_order')->info('RetryFailedPaypalAbos: Verarbeite Abo', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'email' => $userAbo->email,
|
||||
'payone_userid' => $userAbo->payone_userid,
|
||||
]);
|
||||
|
||||
$alreadyPaidToday = UserAboOrder::where('user_abo_id', $userAbo->id)
|
||||
->whereDate('created_at', now()->toDateString())
|
||||
->where('paid', true)
|
||||
->exists();
|
||||
|
||||
if ($alreadyPaidToday) {
|
||||
$this->warn(" Abo #{$userAbo->id}: Bereits heute bezahlt - übersprungen");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
AboHelper::ensureUserAboItemsFromLatestOrder($userAbo);
|
||||
|
||||
$shoppingOrder = null;
|
||||
$userOrder = new UserMakeOrder($userAbo);
|
||||
|
||||
try {
|
||||
if (! $userOrder->createShoppingUser()) {
|
||||
$this->error(" Abo #{$userAbo->id}: Shopping-User konnte nicht erstellt werden");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$shoppingOrder = $userOrder->makeShoppingOrder();
|
||||
if (! $shoppingOrder) {
|
||||
$this->error(" Abo #{$userAbo->id}: Bestellung konnte nicht erstellt werden");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info(" Bestellung #{$shoppingOrder->id} erstellt (Betrag: {$shoppingOrder->total_shipping} EUR)");
|
||||
|
||||
$response = $userOrder->makePayment();
|
||||
if (is_object($response)) {
|
||||
$response = (array) $response;
|
||||
}
|
||||
|
||||
if (! isset($response['status'])) {
|
||||
$this->error(" Abo #{$userAbo->id}: Ungültige Zahlungsantwort");
|
||||
$this->markAboError($userAbo, $shoppingOrder);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($response['status'] === 'APPROVED') {
|
||||
$this->info(" Zahlung ERFOLGREICH für Abo #{$userAbo->id}");
|
||||
$this->markAboSuccess($userAbo, $shoppingOrder);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$errorCode = $response['errorcode'] ?? '-';
|
||||
$errorMsg = $response['errormessage'] ?? '-';
|
||||
$this->error(" Zahlung FEHLGESCHLAGEN für Abo #{$userAbo->id}: [{$errorCode}] {$errorMsg}");
|
||||
|
||||
MyLog::writeLog(
|
||||
'userabo',
|
||||
'error',
|
||||
'Error:RetryPaypal RetryFailedPaypalAbos / makePayment Error',
|
||||
$response
|
||||
);
|
||||
|
||||
$this->markAboError($userAbo, $shoppingOrder);
|
||||
|
||||
$shoppingPayment = $userOrder->getShoppingPayment();
|
||||
if ($shoppingPayment) {
|
||||
Payment::paymentStatusSendMail($shoppingOrder, $shoppingPayment, [
|
||||
'mode' => $shoppingPayment->mode,
|
||||
'txaction' => 'error',
|
||||
'send_link' => false,
|
||||
'payment_error' => $response,
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('RetryFailedPaypalAbos: Exception bei Abo-Verarbeitung', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
$this->error(" Exception: {$e->getMessage()}");
|
||||
|
||||
if ($shoppingOrder) {
|
||||
$this->markAboError($userAbo, $shoppingOrder);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function markAboSuccess(UserAbo $userAbo, $shoppingOrder): void
|
||||
{
|
||||
DB::transaction(function () use ($userAbo, $shoppingOrder) {
|
||||
$nextDate = AboHelper::setNextDate(now(), $userAbo->abo_interval);
|
||||
|
||||
$userAbo->update([
|
||||
'status' => 2,
|
||||
'next_date' => $nextDate,
|
||||
'last_date' => now(),
|
||||
]);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => 1,
|
||||
'paid' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
IncentiveTracker::trackAboActivated($shoppingOrder);
|
||||
|
||||
$nextDateFormatted = Carbon::parse($userAbo->getRawOriginal('next_date'))->format('d.m.Y');
|
||||
$this->info(" Status → 2 (abo_okay), nächstes Datum → {$nextDateFormatted}");
|
||||
|
||||
\Log::channel('abo_order')->info('RetryFailedPaypalAbos: Abo erfolgreich reaktiviert', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'next_date' => $userAbo->getRawOriginal('next_date'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function markAboError(UserAbo $userAbo, $shoppingOrder): void
|
||||
{
|
||||
DB::transaction(function () use ($userAbo, $shoppingOrder) {
|
||||
$userAbo->update(['last_date' => now()]);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => 3,
|
||||
'paid' => false,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
private function getExecutionTime(): string
|
||||
{
|
||||
$diff = microtime(true) - $this->timeStart;
|
||||
$sec = intval($diff);
|
||||
$micro = $diff - $sec;
|
||||
|
||||
return $sec.' Sekunden und '.round($micro * 1000, 2).' ms';
|
||||
}
|
||||
}
|
||||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\UserAbo;
|
||||
use App\Cron\UserMakeOrder;
|
||||
use App\Services\AboHelper;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Services\AboHelper;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TestUserMakeAboOrder extends Command
|
||||
|
|
@ -63,8 +62,9 @@ class TestUserMakeAboOrder extends Command
|
|||
if ($aboId) {
|
||||
// Test für spezifisches Abo
|
||||
$userAbo = UserAbo::find($aboId);
|
||||
if (!$userAbo) {
|
||||
if (! $userAbo) {
|
||||
$this->error("Abo mit ID {$aboId} nicht gefunden!");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -80,8 +80,9 @@ class TestUserMakeAboOrder extends Command
|
|||
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Fehler beim Testen: ' . $e->getMessage());
|
||||
$this->error('Fehler beim Testen: '.$e->getMessage());
|
||||
$this->error($e->getTraceAsString());
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -89,10 +90,10 @@ class TestUserMakeAboOrder extends Command
|
|||
/**
|
||||
* Testet ein einzelnes Abo
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param string $testDate
|
||||
* @param bool $dryRun
|
||||
* @param bool $force
|
||||
* @param UserAbo $userAbo
|
||||
* @param string $testDate
|
||||
* @param bool $dryRun
|
||||
* @param bool $force
|
||||
* @return void
|
||||
*/
|
||||
private function testSingleAbo($userAbo, $testDate, $dryRun, $force)
|
||||
|
|
@ -101,15 +102,15 @@ class TestUserMakeAboOrder extends Command
|
|||
$this->displayAboInfo($userAbo);
|
||||
|
||||
// Prüfe ob Abo für Test-Datum fällig ist
|
||||
if ($userAbo->next_date != $testDate && !$force) {
|
||||
if ($userAbo->next_date != $testDate && ! $force) {
|
||||
$this->warn("Abo ist nicht für {$testDate} fällig (next_date: {$userAbo->next_date})");
|
||||
if (!$this->confirm('Trotzdem fortfahren?', false)) {
|
||||
if (! $this->confirm('Trotzdem fortfahren?', false)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe auf Duplikate
|
||||
if (!$force) {
|
||||
if (! $force) {
|
||||
$existingOrder = UserAboOrder::where('user_abo_id', $userAbo->id)
|
||||
->whereDate('created_at', $testDate)
|
||||
->first();
|
||||
|
|
@ -117,7 +118,7 @@ class TestUserMakeAboOrder extends Command
|
|||
if ($existingOrder) {
|
||||
$this->warn("Es existiert bereits eine Bestellung für dieses Abo am {$testDate}");
|
||||
$this->info("Bestell-ID: {$existingOrder->shopping_order_id}");
|
||||
if (!$this->confirm('Trotzdem fortfahren?', false)) {
|
||||
if (! $this->confirm('Trotzdem fortfahren?', false)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -144,7 +145,7 @@ class TestUserMakeAboOrder extends Command
|
|||
if ($shoppingOrder) {
|
||||
$this->info("✓ Bestellung erfolgreich erstellt: ID {$shoppingOrder->id}");
|
||||
} else {
|
||||
$this->error("✗ Bestellung konnte nicht erstellt werden");
|
||||
$this->error('✗ Bestellung konnte nicht erstellt werden');
|
||||
}
|
||||
} finally {
|
||||
// next_date zurücksetzen falls geändert
|
||||
|
|
@ -160,9 +161,9 @@ class TestUserMakeAboOrder extends Command
|
|||
/**
|
||||
* Testet alle fälligen Abos
|
||||
*
|
||||
* @param string $testDate
|
||||
* @param bool $dryRun
|
||||
* @param bool $force
|
||||
* @param string $testDate
|
||||
* @param bool $dryRun
|
||||
* @param bool $force
|
||||
* @return void
|
||||
*/
|
||||
private function testAllAbos($testDate, $dryRun, $force)
|
||||
|
|
@ -170,7 +171,7 @@ class TestUserMakeAboOrder extends Command
|
|||
$query = UserAbo::where('next_date', '=', $testDate)
|
||||
->where('active', true);
|
||||
|
||||
if (!$force) {
|
||||
if (! $force) {
|
||||
$query->whereDoesntHave('user_abo_orders', function ($q) use ($testDate) {
|
||||
$q->whereDate('created_at', $testDate);
|
||||
});
|
||||
|
|
@ -184,10 +185,11 @@ class TestUserMakeAboOrder extends Command
|
|||
|
||||
if ($count === 0) {
|
||||
$this->warn('Keine fälligen Abos gefunden!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->confirm("Möchten Sie {$count} Abo(s) testen?", true)) {
|
||||
if (! $this->confirm("Möchten Sie {$count} Abo(s) testen?", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +205,7 @@ class TestUserMakeAboOrder extends Command
|
|||
/**
|
||||
* Zeigt Informationen über ein Abo an
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param UserAbo $userAbo
|
||||
* @return void
|
||||
*/
|
||||
private function displayAboInfo($userAbo)
|
||||
|
|
@ -215,11 +217,11 @@ class TestUserMakeAboOrder extends Command
|
|||
['User ID', $userAbo->user_id],
|
||||
['Payone UserID', $userAbo->payone_userid],
|
||||
['Aktiv', $userAbo->active ? 'Ja' : 'Nein'],
|
||||
['Status', $userAbo->status . ' (' . ($userAbo->getStatusType() ?? 'unbekannt') . ')'],
|
||||
['Status', $userAbo->status.' ('.($userAbo->getStatusType() ?? 'unbekannt').')'],
|
||||
['Intervall', $userAbo->abo_interval],
|
||||
['Next Date', $userAbo->next_date],
|
||||
['Last Date', $userAbo->last_date ?? 'Nie'],
|
||||
['Amount', number_format($userAbo->amount / 100, 2, ',', '.') . ' €'],
|
||||
['Amount', number_format($userAbo->amount / 100, 2, ',', '.').' €'],
|
||||
['is_for', $userAbo->is_for],
|
||||
['Clearing Type', $userAbo->clearingtype],
|
||||
['Items', $userAbo->user_abo_items->count()],
|
||||
|
|
@ -235,7 +237,7 @@ class TestUserMakeAboOrder extends Command
|
|||
'Product ID' => $item->product_id,
|
||||
'Qty' => $item->qty,
|
||||
'Comp' => $item->comp ?? '-',
|
||||
'Price' => number_format($item->price / 100, 2, ',', '.') . ' €',
|
||||
'Price' => number_format($item->price / 100, 2, ',', '.').' €',
|
||||
];
|
||||
}
|
||||
$this->table(['Product ID', 'Qty', 'Comp', 'Price'], $items);
|
||||
|
|
@ -245,7 +247,7 @@ class TestUserMakeAboOrder extends Command
|
|||
/**
|
||||
* Zeigt eine Vorschau der Bestellung an
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param UserAbo $userAbo
|
||||
* @return void
|
||||
*/
|
||||
private function displayOrderPreview($userAbo)
|
||||
|
|
@ -265,8 +267,8 @@ class TestUserMakeAboOrder extends Command
|
|||
/**
|
||||
* Erstellt eine Bestellung für ein Abo (vereinfachte Version für Test)
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param bool $dryRun
|
||||
* @param UserAbo $userAbo
|
||||
* @param bool $dryRun
|
||||
* @return mixed
|
||||
*/
|
||||
private function makeOrder($userAbo, $dryRun = false)
|
||||
|
|
@ -274,18 +276,20 @@ class TestUserMakeAboOrder extends Command
|
|||
$this->info('Erstelle Shopping-User...');
|
||||
$userOrder = new UserMakeOrder($userAbo);
|
||||
|
||||
if (!$userOrder->createShoppingUser()) {
|
||||
if (! $userOrder->createShoppingUser()) {
|
||||
$this->error('Konnte Shopping-User nicht erstellen');
|
||||
|
||||
return null;
|
||||
}
|
||||
$this->info('✓ Shopping-User erstellt');
|
||||
|
||||
$this->info('Erstelle Bestellung...');
|
||||
$shoppingOrder = $userOrder->makeShoppingOrder();
|
||||
$shoppingOrder->mode = 'test'; //immer im test mode testen
|
||||
$shoppingOrder->mode = 'test'; // immer im test mode testen
|
||||
$shoppingOrder->save();
|
||||
if (!$shoppingOrder) {
|
||||
if (! $shoppingOrder) {
|
||||
$this->error('Konnte Bestellung nicht erstellen');
|
||||
|
||||
return null;
|
||||
}
|
||||
$this->info("✓ Bestellung erstellt: ID {$shoppingOrder->id}");
|
||||
|
|
@ -293,6 +297,7 @@ class TestUserMakeAboOrder extends Command
|
|||
if ($dryRun) {
|
||||
$this->info('[DRY-RUN] Zahlung würde durchgeführt');
|
||||
$this->info('[DRY-RUN] Abo würde aktualisiert');
|
||||
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
|
|
@ -304,10 +309,11 @@ class TestUserMakeAboOrder extends Command
|
|||
$response = (array) $response;
|
||||
}
|
||||
|
||||
$this->info('Zahlungsantwort: ' . json_encode($response, JSON_PRETTY_PRINT));
|
||||
$this->info('Zahlungsantwort: '.json_encode($response, JSON_PRETTY_PRINT));
|
||||
|
||||
if (!isset($response['status'])) {
|
||||
if (! isset($response['status'])) {
|
||||
$this->warn('⚠ Kein Status in Zahlungsantwort');
|
||||
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +329,7 @@ class TestUserMakeAboOrder extends Command
|
|||
$this->warn("⚠ Zahlungsstatus: {$response['status']}");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Fehler bei Zahlung: ' . $e->getMessage());
|
||||
$this->error('Fehler bei Zahlung: '.$e->getMessage());
|
||||
}
|
||||
|
||||
return $shoppingOrder;
|
||||
|
|
@ -332,9 +338,9 @@ class TestUserMakeAboOrder extends Command
|
|||
/**
|
||||
* Aktualisiert das Abo nach erfolgreicher Bestellung (vereinfachte Version)
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
* @param int $status
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
* @param int $status
|
||||
* @return void
|
||||
*/
|
||||
private function updateAbo($userAbo, $shoppingOrder, $status = 1)
|
||||
|
|
@ -356,10 +362,11 @@ class TestUserMakeAboOrder extends Command
|
|||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => $status,
|
||||
'paid' => true,
|
||||
]);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Fehler beim Aktualisieren des Abos: ' . $e->getMessage());
|
||||
$this->error('Fehler beim Aktualisieren des Abos: '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
|
@ -375,6 +382,6 @@ class TestUserMakeAboOrder extends Command
|
|||
$sec = intval($diff);
|
||||
$micro = $diff - $sec;
|
||||
|
||||
return $sec . ' Sekunden und ' . round($micro * 1000, 2) . ' ms';
|
||||
return $sec.' Sekunden und '.round($micro * 1000, 2).' ms';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Setting;
|
||||
use App\Cron\UserMakeOrder;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\Services\MyLog;
|
||||
use App\Services\Payment;
|
||||
use App\Cron\UserMakeOrder;
|
||||
use App\Services\AboHelper;
|
||||
use App\Models\UserAboOrder;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserMakeAboOrder extends Command
|
||||
|
|
@ -33,10 +32,13 @@ class UserMakeAboOrder extends Command
|
|||
protected $description = 'Make Orders from Abos';
|
||||
|
||||
private $timeStart;
|
||||
|
||||
private $month;
|
||||
|
||||
private $year;
|
||||
|
||||
private $sendCreditMail = false;
|
||||
|
||||
private $sendUpdateMail = false;
|
||||
|
||||
/**
|
||||
|
|
@ -70,9 +72,10 @@ class UserMakeAboOrder extends Command
|
|||
} catch (\Exception $e) {
|
||||
\Log::channel('cron')->error('UserMakeAboOrder: Fehler beim Ausführen des Befehls', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
$this->error('Fehler beim Ausführen des Befehls: ' . $e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -91,7 +94,7 @@ class UserMakeAboOrder extends Command
|
|||
// Prüfe auf bereits verarbeitete Abos am heutigen Tag (Duplikatsprüfung)
|
||||
$userAbos = UserAbo::where('next_date', '=', $dateNow)
|
||||
->where('active', true)
|
||||
->where('status', '=', 2) //abo_okay
|
||||
->where('status', '=', 2) // abo_okay
|
||||
->whereDoesntHave('user_abo_orders', function ($query) use ($dateNow) {
|
||||
$query->whereDate('created_at', $dateNow);
|
||||
})
|
||||
|
|
@ -104,7 +107,7 @@ class UserMakeAboOrder extends Command
|
|||
foreach ($userAbos as $userAbo) {
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Verarbeite Abo', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'payone_userid' => $userAbo->payone_userid
|
||||
'payone_userid' => $userAbo->payone_userid,
|
||||
]);
|
||||
|
||||
$this->info("Verarbeite Abo: {$userAbo->id} (PayoneUserid: {$userAbo->payone_userid})");
|
||||
|
|
@ -116,14 +119,15 @@ class UserMakeAboOrder extends Command
|
|||
$lockedAbo = UserAbo::where('id', $userAbo->id)
|
||||
->where('next_date', '=', $dateNow)
|
||||
->where('active', true)
|
||||
->where('status', '=', 2) //abo_okay
|
||||
->where('status', '=', 2) // abo_okay
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$lockedAbo) {
|
||||
if (! $lockedAbo) {
|
||||
\Log::channel('abo_order')->warning('UserMakeAboOrder: Abo wurde bereits verarbeitet oder ist nicht mehr aktiv', [
|
||||
'abo_id' => $userAbo->id
|
||||
'abo_id' => $userAbo->id,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -135,8 +139,9 @@ class UserMakeAboOrder extends Command
|
|||
if ($existingOrder) {
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo wurde bereits heute verarbeitet', [
|
||||
'abo_id' => $lockedAbo->id,
|
||||
'existing_order_id' => $existingOrder->shopping_order_id
|
||||
'existing_order_id' => $existingOrder->shopping_order_id,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -146,18 +151,18 @@ class UserMakeAboOrder extends Command
|
|||
if ($shoppingOrder) {
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Bestellung erstellt', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id
|
||||
'order_id' => $shoppingOrder->id,
|
||||
]);
|
||||
$this->info("Bestellung erstellt: {$shoppingOrder->id}");
|
||||
} else {
|
||||
\Log::channel('abo_order')->warning('UserMakeAboOrder: Keine Bestellung erstellt für Abo', ['abo_id' => $userAbo->id]);
|
||||
$this->warn("Keine Bestellung erstellt für Abo: {$userAbo->id}");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler bei der Verarbeitung des Abos', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
$this->error("Fehler bei Abo {$userAbo->id}: " . $e->getMessage());
|
||||
}
|
||||
|
|
@ -167,7 +172,7 @@ class UserMakeAboOrder extends Command
|
|||
/**
|
||||
* Erstellt eine Bestellung für ein Abo
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param UserAbo $userAbo
|
||||
* @return mixed
|
||||
*/
|
||||
private function makeOrder($userAbo)
|
||||
|
|
@ -179,22 +184,24 @@ class UserMakeAboOrder extends Command
|
|||
$userOrder = new UserMakeOrder($userAbo);
|
||||
|
||||
try {
|
||||
if (!$userOrder->createShoppingUser()) {
|
||||
if (! $userOrder->createShoppingUser()) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Konnte Shopping-User nicht erstellen', ['abo_id' => $userAbo->id]);
|
||||
$this->error("Konnte Shopping-User für Abo {$userAbo->id} nicht erstellen");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$shoppingOrder = $userOrder->makeShoppingOrder();
|
||||
if (!$shoppingOrder) {
|
||||
if (! $shoppingOrder) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Konnte Bestellung nicht erstellen', ['abo_id' => $userAbo->id]);
|
||||
$this->error("Konnte Bestellung für Abo {$userAbo->id} nicht erstellen");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Bestellung erstellt, starte Zahlungsvorgang', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id
|
||||
'order_id' => $shoppingOrder->id,
|
||||
]);
|
||||
|
||||
$response = $userOrder->makePayment();
|
||||
|
|
@ -205,17 +212,18 @@ class UserMakeAboOrder extends Command
|
|||
$response = (array) $response;
|
||||
}
|
||||
|
||||
if (!isset($response['status'])) {
|
||||
if (! isset($response['status'])) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Ungültige Zahlungsantwort', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'response' => $response
|
||||
'response' => $response,
|
||||
]);
|
||||
$this->error("Ungültige Zahlungsantwort für Abo {$userAbo->id}");
|
||||
|
||||
// Bei fehlender Status-Information: Abo nicht aktualisieren, damit es beim nächsten Lauf erneut versucht wird
|
||||
// Aber Bestellung speichern für Nachverfolgung
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Ungültige Zahlungsantwort - kein Status');
|
||||
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +231,7 @@ class UserMakeAboOrder extends Command
|
|||
\Log::channel('abo_order')->info('UserMakeAboOrder: Zahlung erfolgreich', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'response' => $response
|
||||
'response' => $response,
|
||||
]);
|
||||
$this->info("Zahlung erfolgreich für Abo {$userAbo->id}");
|
||||
// Nur bei erfolgreicher Zahlung: next_date aktualisieren
|
||||
|
|
@ -232,7 +240,7 @@ class UserMakeAboOrder extends Command
|
|||
\Log::channel('abo_order')->error('UserMakeAboOrder: Zahlungsfehler', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'error' => $response
|
||||
'error' => $response,
|
||||
]);
|
||||
$this->error("Zahlungsfehler für Abo {$userAbo->id}");
|
||||
|
||||
|
|
@ -263,7 +271,7 @@ class UserMakeAboOrder extends Command
|
|||
\Log::channel('abo_order')->info('UserMakeAboOrder: Zahlung ausstehend/weiterleitung', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'status' => $response['status']
|
||||
'status' => $response['status'],
|
||||
]);
|
||||
$this->info("Zahlung ausstehend für Abo {$userAbo->id}: {$response['status']}");
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Zahlung ausstehend: ' . $response['status']);
|
||||
|
|
@ -272,23 +280,29 @@ class UserMakeAboOrder extends Command
|
|||
\Log::channel('abo_order')->warning('UserMakeAboOrder: Unbekannter Zahlungsstatus', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'status' => $response['status']
|
||||
'status' => $response['status'],
|
||||
]);
|
||||
$this->warn("Unbekannter Zahlungsstatus für Abo {$userAbo->id}: {$response['status']}");
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Unbekannter Status: ' . $response['status']);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Ausnahme bei der Bestellungserstellung', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
$this->error("Ausnahme bei Abo {$userAbo->id}: " . $e->getMessage());
|
||||
|
||||
// Bei Exception: Bestellung speichern falls vorhanden, aber Abo nicht aktualisieren
|
||||
// Bestellung existiert (z. B. Fehler bei Payone): Abo-Fehlerstatus, Bestellung bleibt nachvollziehbar
|
||||
if ($shoppingOrder) {
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Exception: ' . $e->getMessage());
|
||||
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
||||
// Noch keine ShoppingOrder (createShoppingUser / makeShoppingOrder): Exception durchreichen,
|
||||
// sonst ruft der Aufrufer nur "null" ohne Ursache (z. B. Testbench, fehlende country_id im Yard).
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $shoppingOrder;
|
||||
|
|
@ -298,9 +312,9 @@ class UserMakeAboOrder extends Command
|
|||
* Aktualisiert das Abo nach einer erfolgreichen Bestellung
|
||||
* Aktualisiert next_date für den nächsten Abo-Zyklus
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
* @param int $status
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
* @param int $status
|
||||
* @return void
|
||||
*/
|
||||
private function updateAbo($userAbo, $shoppingOrder, $status = 1)
|
||||
|
|
@ -308,7 +322,7 @@ class UserMakeAboOrder extends Command
|
|||
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo nach erfolgreicher Zahlung', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'status' => $status
|
||||
'status' => $status,
|
||||
]);
|
||||
|
||||
$this->info("Aktualisiere Abo: {$userAbo->id} mit Status {$status}");
|
||||
|
|
@ -330,18 +344,22 @@ class UserMakeAboOrder extends Command
|
|||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'status' => $status,
|
||||
'paid' => false,
|
||||
'paid' => true,
|
||||
]);
|
||||
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo erfolgreich aktualisiert', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'next_date' => $updateData['next_date']
|
||||
'next_date' => $updateData['next_date'],
|
||||
]);
|
||||
});
|
||||
|
||||
// Wie bei Payment::paymentStatusPaidAction: Incentive nur wenn Callback nicht lief
|
||||
// (firstOrCreate verhindert Doppelungen wenn Payone später noch trackt)
|
||||
IncentiveTracker::trackAboActivated($shoppingOrder);
|
||||
} catch (\Exception $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler beim Aktualisieren des Abos', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
|
||||
throw $e; // Re-throw für besseres Error-Handling
|
||||
|
|
@ -352,10 +370,10 @@ class UserMakeAboOrder extends Command
|
|||
* Aktualisiert das Abo bei Fehlern - OHNE next_date zu aktualisieren
|
||||
* Damit wird das Abo beim nächsten Cron-Lauf erneut versucht
|
||||
*
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
* @param int|string $status Status-Code oder Fehlermeldung
|
||||
* @param array|null $errorResponse Optionale Fehlerantwort von Payment
|
||||
* @param UserAbo $userAbo
|
||||
* @param mixed $shoppingOrder
|
||||
* @param int|string $status Status-Code oder Fehlermeldung
|
||||
* @param array|null $errorResponse Optionale Fehlerantwort von Payment
|
||||
* @return void
|
||||
*/
|
||||
private function updateAboOnError($userAbo, $shoppingOrder, $status, $errorResponse = null)
|
||||
|
|
@ -363,7 +381,7 @@ class UserMakeAboOrder extends Command
|
|||
\Log::channel('abo_order')->info('UserMakeAboOrder: Aktualisiere Abo bei Fehler (ohne next_date)', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $shoppingOrder->id,
|
||||
'status' => $status
|
||||
'status' => $status,
|
||||
]);
|
||||
|
||||
$this->info("Aktualisiere Abo bei Fehler: {$userAbo->id} (Status: {$status})");
|
||||
|
|
@ -395,13 +413,13 @@ class UserMakeAboOrder extends Command
|
|||
\Log::channel('abo_order')->info('UserMakeAboOrder: Abo bei Fehler aktualisiert (next_date unverändert)', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'next_date' => $userAbo->next_date,
|
||||
'status' => $status
|
||||
'status' => $status,
|
||||
]);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler beim Aktualisieren des Abos bei Fehler', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
|
||||
// Bei Fehler hier nicht re-throw, damit der Hauptprozess fortgesetzt werden kann
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ class Kernel extends ConsoleKernel
|
|||
$schedule->command('user:cleanup')->dailyAt('03:30');
|
||||
$schedule->command('user:make_abo_order')->dailyAt('04:00');
|
||||
|
||||
// Abo-Chart-Snapshots: vergangene Monate einfrieren (nach allen Abo-Jobs)
|
||||
$schedule->command('abo:store-chart-snapshots')->dailyAt('04:30');
|
||||
|
||||
// Incentive: Punkteberechnung täglich nach business:store-optimized
|
||||
$schedule->command('incentive:calculate')->dailyAt('05:00');
|
||||
|
||||
// Cleanup old log files weekly (keeps logs for 30 days)
|
||||
$schedule->command('logs:cleanup --days=30')->weekly()->sundays()->at('05:00');
|
||||
|
||||
|
|
|
|||
|
|
@ -2,41 +2,45 @@
|
|||
|
||||
namespace App\Cron;
|
||||
|
||||
use Yard;
|
||||
use App\Models\UserAbo;
|
||||
use App\Http\Controllers\Pay\PayoneController;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Http\Controllers\Pay\PayoneController;
|
||||
use App\Models\UserAbo;
|
||||
use App\Services\AboOrderCart;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yard;
|
||||
|
||||
class UserMakeOrder
|
||||
{
|
||||
private $userAbo;
|
||||
private $shopping_user;
|
||||
private $shopping_order;
|
||||
private $is_for;
|
||||
private $user;
|
||||
private $pay;
|
||||
|
||||
private $shopping_user;
|
||||
|
||||
private $shopping_order;
|
||||
|
||||
private $is_for;
|
||||
|
||||
private $user;
|
||||
|
||||
private $pay;
|
||||
|
||||
public function __construct(UserAbo $userAbo)
|
||||
{
|
||||
$this->userAbo = $userAbo;
|
||||
Log::info('UserMakeOrder initialisiert für UserAbo ID: ' . $userAbo->id);
|
||||
Log::info('UserMakeOrder initialisiert für UserAbo ID: '.$userAbo->id);
|
||||
}
|
||||
|
||||
|
||||
public function checkProducts()
|
||||
{
|
||||
Log::info('Überprüfe Produkte für UserAbo ID: ' . $this->userAbo->id);
|
||||
Log::info('Überprüfe Produkte für UserAbo ID: '.$this->userAbo->id);
|
||||
$ret = [];
|
||||
|
||||
if (!$this->userAbo->items || $this->userAbo->items->isEmpty()) {
|
||||
Log::warning('Keine Artikel für UserAbo ID: ' . $this->userAbo->id . ' gefunden');
|
||||
if (! $this->userAbo->items || $this->userAbo->items->isEmpty()) {
|
||||
Log::warning('Keine Artikel für UserAbo ID: '.$this->userAbo->id.' gefunden');
|
||||
|
||||
return $ret;
|
||||
}
|
||||
//preise prüfen, ob sie sich geändert haben?
|
||||
// preise prüfen, ob sie sich geändert haben?
|
||||
foreach ($this->userAbo->items as $item) {
|
||||
$ret[] = [
|
||||
'product_id' => $item->product_id,
|
||||
|
|
@ -52,75 +56,79 @@ class UserMakeOrder
|
|||
];
|
||||
}
|
||||
|
||||
Log::info('Produkte überprüft: ' . count($ret) . ' Artikel gefunden');
|
||||
Log::info('Produkte überprüft: '.count($ret).' Artikel gefunden');
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function makePayment($testmode = false)
|
||||
{
|
||||
Log::info('Starte Zahlungsvorgang für UserAbo ID: ' . $this->userAbo->id);
|
||||
Log::info('Starte Zahlungsvorgang für UserAbo ID: '.$this->userAbo->id);
|
||||
|
||||
try {
|
||||
$this->pay = new PayoneController();
|
||||
$this->pay = new PayoneController;
|
||||
$this->pay->init($this->shopping_user, $this->shopping_order);
|
||||
$amount = $this->shopping_order->subtotal_ws * 100;
|
||||
$amount = $this->shopping_order->total_shipping * 100;
|
||||
// $amount = Yard::instance($this->instance)->totalWithShipping(2, '.', '') * 100;
|
||||
|
||||
$this->pay->setAboPayment($this->userAbo, $amount, 'EUR');
|
||||
$this->pay->setPersonalData();
|
||||
$response = $this->pay->onlyPaymentResponse();
|
||||
\Log::info('Response: ' . json_encode($response));
|
||||
//$response = $this->pay->ResponseData(true);
|
||||
\Log::info('Response: '.json_encode($response));
|
||||
// $response = $this->pay->ResponseData(true);
|
||||
|
||||
Log::info('Zahlungsvorgang abgeschlossen für UserAbo ID: '.$this->userAbo->id.', Status: '.($response->status ?? 'unbekannt'));
|
||||
|
||||
Log::info('Zahlungsvorgang abgeschlossen für UserAbo ID: ' . $this->userAbo->id . ', Status: ' . ($response->status ?? 'unbekannt'));
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Fehler bei Zahlungsvorgang für UserAbo ID: ' . $this->userAbo->id . ': ' . $e->getMessage());
|
||||
Log::error('Fehler bei Zahlungsvorgang für UserAbo ID: '.$this->userAbo->id.': '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function getShoppingPayment()
|
||||
{
|
||||
Log::info('Rufe Zahlungsinformationen ab für UserAbo ID: ' . $this->userAbo->id);
|
||||
Log::info('Rufe Zahlungsinformationen ab für UserAbo ID: '.$this->userAbo->id);
|
||||
|
||||
if ($this->pay) {
|
||||
$payment = $this->pay->getShoppingPayment();
|
||||
Log::info('Zahlungsinformationen abgerufen: ' . ($payment ? 'erfolgreich' : 'nicht verfügbar'));
|
||||
Log::info('Zahlungsinformationen abgerufen: '.($payment ? 'erfolgreich' : 'nicht verfügbar'));
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
Log::warning('Keine Zahlungsinformationen verfügbar für UserAbo ID: ' . $this->userAbo->id);
|
||||
Log::warning('Keine Zahlungsinformationen verfügbar für UserAbo ID: '.$this->userAbo->id);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function createShoppingUser()
|
||||
{
|
||||
Log::info('Erstelle Shopping-User für UserAbo ID: ' . $this->userAbo->id);
|
||||
//hier muss der letzte shopping_user verwendet werden
|
||||
Log::info('Erstelle Shopping-User für UserAbo ID: '.$this->userAbo->id);
|
||||
// hier muss der letzte shopping_user verwendet werden
|
||||
try {
|
||||
$this->shopping_user = AboOrderCart::makeCustomerDetail($this->userAbo);
|
||||
$this->shopping_user->created_at = now();
|
||||
$this->shopping_user->updated_at = now();
|
||||
$this->shopping_user->save();
|
||||
|
||||
Log::info('Shopping-User erstellt für UserAbo ID: ' . $this->userAbo->id . ', Neue User-ID: ' . $this->shopping_user->id);
|
||||
Log::info('Shopping-User erstellt für UserAbo ID: '.$this->userAbo->id.', Neue User-ID: '.$this->shopping_user->id);
|
||||
|
||||
return $this->shopping_user;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Fehler beim Erstellen des Shopping-Users für UserAbo ID: ' . $this->userAbo->id . ': ' . $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Fehler beim Erstellen des Shopping-Users für UserAbo ID: '.$this->userAbo->id.': '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
|
||||
Log::warning('Kein Shopping-User verfügbar für UserAbo ID: ' . $this->userAbo->id);
|
||||
return false;
|
||||
}
|
||||
|
||||
public function makeShoppingOrder()
|
||||
{
|
||||
Log::info('Erstelle Bestellung für UserAbo ID: ' . $this->userAbo->id);
|
||||
Log::info('Erstelle Bestellung für UserAbo ID: '.$this->userAbo->id);
|
||||
|
||||
try {
|
||||
if (!$this->shopping_user) {
|
||||
Log::error('Kein Shopping-User verfügbar für Bestellerstellung, UserAbo ID: ' . $this->userAbo->id);
|
||||
if (! $this->shopping_user) {
|
||||
Log::error('Kein Shopping-User verfügbar für Bestellerstellung, UserAbo ID: '.$this->userAbo->id);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -135,18 +143,18 @@ class UserMakeOrder
|
|||
$yardBefore = Yard::instance('shopping');
|
||||
$itemsBefore = $yardBefore->content();
|
||||
if ($itemsBefore->count() > 0) {
|
||||
Log::warning('UserMakeOrder: Yard war nicht leer nach initYard für Abo ID: ' . $this->userAbo->id . ', Items: ' . $itemsBefore->count());
|
||||
Log::warning('UserMakeOrder: Yard war nicht leer nach initYard für Abo ID: '.$this->userAbo->id.', Items: '.$itemsBefore->count());
|
||||
$yardBefore->destroy(); // Erzwinge Leerung
|
||||
}
|
||||
|
||||
//hier wird die Bestellung erstellt inkl aktueller Preise
|
||||
// hier wird die Bestellung erstellt inkl aktueller Preise
|
||||
AboOrderCart::makeOrderYard($this->userAbo);
|
||||
|
||||
$yard = Yard::instance('shopping');
|
||||
|
||||
// Debug: Logge welche Produkte im Cart sind
|
||||
$items = $yard->content();
|
||||
Log::info('UserMakeOrder: Produkte im Cart nach makeOrderYard für Abo ID: ' . $this->userAbo->id, [
|
||||
Log::info('UserMakeOrder: Produkte im Cart nach makeOrderYard für Abo ID: '.$this->userAbo->id, [
|
||||
'abo_id' => $this->userAbo->id,
|
||||
'item_count' => $items->count(),
|
||||
'items' => $items->map(function ($item) {
|
||||
|
|
@ -154,26 +162,57 @@ class UserMakeOrder
|
|||
'product_id' => $item->id,
|
||||
'name' => $item->name,
|
||||
'qty' => $item->qty,
|
||||
'rowId' => $item->rowId
|
||||
'rowId' => $item->rowId,
|
||||
];
|
||||
})->toArray()
|
||||
})->toArray(),
|
||||
]);
|
||||
|
||||
if (!$this->userAbo->shopping_user || !$this->userAbo->shopping_user->shopping_order || !$this->userAbo->shopping_user->shopping_order->user_shop) {
|
||||
Log::error('Fehlende Beziehungsdaten für Bestellerstellung, UserAbo ID: ' . $this->userAbo->id);
|
||||
$shoppingUserStamm = $this->userAbo->shopping_user;
|
||||
if (! $shoppingUserStamm) {
|
||||
Log::error('UserAbo ohne shopping_user (Stammdaten-Kunde), UserAbo ID: '.$this->userAbo->id);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Referenz fuer Shop/Mode: neueste Bestellung (hasOne shopping_order kann null sein z. B. wenn die
|
||||
// aelteste Order soft-deleted ist oder mehrere Orders existieren).
|
||||
$referenceOrder = $shoppingUserStamm->shopping_orders()
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if (! $referenceOrder || ! $referenceOrder->user_shop_id) {
|
||||
Log::error('Fehlende Beziehungsdaten fuer Bestellerstellung (Referenz-Bestellung ohne user_shop_id), UserAbo ID: '.$this->userAbo->id, [
|
||||
'shopping_user_id' => $shoppingUserStamm->id,
|
||||
'reference_order_id' => $referenceOrder?->id,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$countryId = $yard->getShippingCountryId() ?? $referenceOrder->country_id;
|
||||
if (! $countryId) {
|
||||
Log::error('Kein country_id (Yard shipping_country_id und Referenz-Bestellung leer), UserAbo ID: '.$this->userAbo->id, [
|
||||
'yard_shipping_country_id' => $yard->getShippingCountryId(),
|
||||
'reference_order_id' => $referenceOrder->id,
|
||||
'reference_country_id' => $referenceOrder->country_id,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->shopping_order = ShoppingOrder::create([
|
||||
'shopping_user_id' => $this->shopping_user->id,
|
||||
'auth_user_id' => $this->shopping_user->auth_user_id,
|
||||
'country_id' => $yard->getShippingCountryId(),
|
||||
'member_id' => $this->userAbo->member_id ?? $referenceOrder->member_id,
|
||||
'auth_user_id' => $this->userAbo->is_for === 'me'
|
||||
? ($this->userAbo->user_id ?? $referenceOrder->auth_user_id ?? $this->shopping_user->auth_user_id)
|
||||
: ($this->shopping_user->auth_user_id ?? $referenceOrder->auth_user_id),
|
||||
'country_id' => $countryId,
|
||||
'language' => \App::getLocale(),
|
||||
'user_shop_id' => $this->userAbo->shopping_user->shopping_order->user_shop->id,
|
||||
'user_shop_id' => (int) $referenceOrder->user_shop_id,
|
||||
'payment_for' => $this->shopping_user->getOrderPaymentFor(),
|
||||
'total' => $yard->total(2, '.', ''),
|
||||
'subtotal' => $yard->subtotal(2, '.', ''),
|
||||
'shipping' => $yard->shipping(2, '.', ','),
|
||||
'shipping' => $yard->shipping(2, '.', ''),
|
||||
'shipping_net' => $yard->shippingNet(2, '.', ''),
|
||||
'subtotal_ws' => $yard->subtotalWithShipping(2, '.', ''),
|
||||
'tax' => $yard->taxWithShipping(2, '.', ''),
|
||||
|
|
@ -183,21 +222,21 @@ class UserMakeOrder
|
|||
'is_abo' => 1,
|
||||
'abo_interval' => $this->userAbo->abo_interval ?? 0,
|
||||
'txaction' => 'prev',
|
||||
'mode' => $this->userAbo->shopping_user->shopping_order->mode,
|
||||
'mode' => $referenceOrder->mode,
|
||||
]);
|
||||
|
||||
Log::info('Bestellung erstellt für UserAbo ID: ' . $this->userAbo->id . ', Bestellnummer: ' . $this->shopping_order->id);
|
||||
Log::info('Bestellung erstellt für UserAbo ID: '.$this->userAbo->id.', Bestellnummer: '.$this->shopping_order->id);
|
||||
|
||||
$items = $yard->getContentByOrder();
|
||||
$itemCount = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (!ShoppingOrderItem::where('shopping_order_id', $this->shopping_order->id)->where('row_id', $item->rowId)->count()) {
|
||||
if (! ShoppingOrderItem::where('shopping_order_id', $this->shopping_order->id)->where('row_id', $item->rowId)->count()) {
|
||||
$price_net = $yard->rowPriceNet($item, 2, '.', '');
|
||||
$tax = $item->price - $price_net;
|
||||
$data = [
|
||||
'shopping_order_id' => $this->shopping_order->id,
|
||||
'row_id' => $item->rowId,
|
||||
'row_id' => $item->rowId,
|
||||
'product_id' => $item->id,
|
||||
'comp' => $item->options->comp,
|
||||
'qty' => $item->qty,
|
||||
|
|
@ -208,21 +247,21 @@ class UserMakeOrder
|
|||
'price_vk_net' => $this->shopping_order->getPriceVkNetBy($item->id),
|
||||
'discount' => $item->options->no_commission ? 0 : $this->shopping_order->getUserDiscount(),
|
||||
'points' => $item->options->points,
|
||||
'slug' => $item->options->slug
|
||||
'slug' => $item->options->slug,
|
||||
];
|
||||
ShoppingOrderItem::create($data);
|
||||
$itemCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Bestellpositionen hinzugefügt für UserAbo ID: ' . $this->userAbo->id . ', Anzahl: ' . $itemCount);
|
||||
Log::info('Bestellpositionen hinzugefügt für UserAbo ID: '.$this->userAbo->id.', Anzahl: '.$itemCount);
|
||||
|
||||
$this->shopping_order->makeTaxSplit();
|
||||
Log::info('Steueraufteilung für Bestellung abgeschlossen, UserAbo ID: ' . $this->userAbo->id);
|
||||
Log::info('Steueraufteilung für Bestellung abgeschlossen, UserAbo ID: '.$this->userAbo->id);
|
||||
|
||||
return $this->shopping_order;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Fehler bei Bestellerstellung für UserAbo ID: ' . $this->userAbo->id . ': ' . $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Fehler bei Bestellerstellung für UserAbo ID: '.$this->userAbo->id.': '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
411
app/Http/Controllers/Admin/IncentiveController.php
Normal file
411
app/Http/Controllers/Admin/IncentiveController.php
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveNewAbo;
|
||||
use App\Models\IncentiveNewPartner;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\IncentivePointsLog;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Incentive\IncentiveCalculationService;
|
||||
use App\User;
|
||||
use Request;
|
||||
|
||||
class IncentiveController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('admin');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('admin.incentive.index');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.incentive.create', [
|
||||
'languages' => config('localization.supportedLocales'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$data = Request::validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'subtitle' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'image' => 'nullable|string|max:255',
|
||||
'terms' => 'nullable|string',
|
||||
'qualification_start' => 'required|date',
|
||||
'qualification_end' => 'required|date|after_or_equal:qualification_start',
|
||||
'calculation_end' => 'required|date|after_or_equal:qualification_end',
|
||||
'points_partner_onetime' => 'required|integer|min:0',
|
||||
'points_abo_onetime' => 'required|integer|min:0',
|
||||
'min_direct_partners' => 'required|integer|min:0',
|
||||
'min_customer_abos' => 'required|integer|min:0',
|
||||
'max_winners' => 'required|integer|min:1',
|
||||
'status' => 'required|integer|in:0,1,2',
|
||||
]);
|
||||
|
||||
$data = array_merge($data, $this->extractTranslations());
|
||||
|
||||
Incentive::create($data);
|
||||
|
||||
\Session()->flash('alert-success', __('incentive.created'));
|
||||
|
||||
return redirect(route('admin_incentives'));
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$incentive = Incentive::findOrFail($id);
|
||||
$participants = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->with('user', 'user.account')
|
||||
->orderByIncentiveLeaderboard()
|
||||
->get();
|
||||
|
||||
return view('admin.incentive.show', [
|
||||
'incentive' => $incentive,
|
||||
'participants' => $participants,
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$incentive = Incentive::findOrFail($id);
|
||||
|
||||
return view('admin.incentive.edit', [
|
||||
'incentive' => $incentive,
|
||||
'languages' => config('localization.supportedLocales'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$data = Request::validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'subtitle' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'image' => 'nullable|string|max:255',
|
||||
'terms' => 'nullable|string',
|
||||
'qualification_start' => 'required|date',
|
||||
'qualification_end' => 'required|date|after_or_equal:qualification_start',
|
||||
'calculation_end' => 'required|date|after_or_equal:qualification_end',
|
||||
'points_partner_onetime' => 'required|integer|min:0',
|
||||
'points_abo_onetime' => 'required|integer|min:0',
|
||||
'min_direct_partners' => 'required|integer|min:0',
|
||||
'min_customer_abos' => 'required|integer|min:0',
|
||||
'max_winners' => 'required|integer|min:1',
|
||||
'status' => 'required|integer|in:0,1,2',
|
||||
]);
|
||||
|
||||
$data = array_merge($data, $this->extractTranslations());
|
||||
|
||||
$incentive = Incentive::findOrFail($id);
|
||||
$incentive->update($data);
|
||||
|
||||
\Session()->flash('alert-success', __('incentive.updated'));
|
||||
|
||||
return redirect(route('admin_incentive_show', [$id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{trans_name: array<string, string>, trans_description: array<string, string>, trans_terms: array<string, string>}
|
||||
*/
|
||||
private function extractTranslations(): array
|
||||
{
|
||||
$transName = [];
|
||||
$transDescription = [];
|
||||
$transTerms = [];
|
||||
|
||||
$transSubtitle = [];
|
||||
|
||||
foreach (config('localization.supportedLocales') as $locale => $localeData) {
|
||||
if ($locale !== 'de') {
|
||||
$transName[$locale] = Request::get('trans_name_'.$locale, '');
|
||||
$transSubtitle[$locale] = Request::get('trans_subtitle_'.$locale, '');
|
||||
$transDescription[$locale] = Request::get('trans_description_'.$locale, '');
|
||||
$transTerms[$locale] = Request::get('trans_terms_'.$locale, '');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'trans_name' => $transName,
|
||||
'trans_subtitle' => $transSubtitle,
|
||||
'trans_description' => $transDescription,
|
||||
'trans_terms' => $transTerms,
|
||||
];
|
||||
}
|
||||
|
||||
public function recalculate($id)
|
||||
{
|
||||
$incentive = Incentive::findOrFail($id);
|
||||
$service = new IncentiveCalculationService;
|
||||
$stats = $service->recalculate($incentive, Request::has('force'));
|
||||
|
||||
\Session()->flash('alert-success', __('incentive.recalculated', [
|
||||
'participants' => $stats['participants'],
|
||||
'errors' => $stats['errors'],
|
||||
]));
|
||||
|
||||
return redirect(route('admin_incentive_show', [$id]));
|
||||
}
|
||||
|
||||
public function participantDetails($participant_id)
|
||||
{
|
||||
$participant = IncentiveParticipant::with('incentive', 'user', 'user.account')
|
||||
->findOrFail($participant_id);
|
||||
|
||||
$data = self::buildParticipantDetailData($participant);
|
||||
|
||||
return view('admin.incentive._participant_details', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die Detail-Daten fuer einen Teilnehmer auf.
|
||||
* Wird von Admin und User Controller genutzt.
|
||||
*/
|
||||
public static function buildParticipantDetailData(IncentiveParticipant $participant): array
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
$calculation_months = $incentive->getCalculationMonths();
|
||||
|
||||
// Alle Logs dieses Teilnehmers (ohne Stornos)
|
||||
$all_logs = IncentivePointsLog::where('participant_id', $participant->id)
|
||||
->where('is_storno', false)
|
||||
->with('salesVolume')
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
|
||||
// UserSalesVolume-IDs -> User-ID Mapping aufbauen (fuer akkumulierte Partner-Punkte)
|
||||
$sv_ids = $all_logs->whereNotNull('user_sales_volume_id')->pluck('user_sales_volume_id')->unique()->toArray();
|
||||
$sv_user_map = ! empty($sv_ids)
|
||||
? UserSalesVolume::whereIn('id', $sv_ids)->pluck('user_id', 'id')->toArray()
|
||||
: [];
|
||||
|
||||
// Partner aus Tracking-Tabelle
|
||||
$new_partners = IncentiveNewPartner::where('participant_id', $participant->id)
|
||||
->with('user', 'user.account')
|
||||
->orderBy('registered_at')
|
||||
->get();
|
||||
|
||||
$partner_logs = $all_logs->where('type', 'partner');
|
||||
|
||||
$partner_sources = $new_partners->map(function ($np) use ($partner_logs, $sv_user_map, $calculation_months, $incentive) {
|
||||
$monthly = [];
|
||||
$transactions = [];
|
||||
|
||||
foreach ($calculation_months as $period) {
|
||||
// Akkumulierte Logs: source_type=UserSalesVolume, deren USV.user_id == partner user_id
|
||||
$month_logs = $partner_logs
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->filter(function ($log) use ($np, $sv_user_map) {
|
||||
if ($log->source_type === User::class) {
|
||||
return false; // Einmal-Punkte nicht in Monatsspalte
|
||||
}
|
||||
|
||||
if ($log->incentive_new_partner_id) {
|
||||
return (int) $log->incentive_new_partner_id === (int) $np->id;
|
||||
}
|
||||
|
||||
// Legacy: USV.user_id muss zum Partner gehoeren
|
||||
return isset($sv_user_map[$log->user_sales_volume_id])
|
||||
&& $sv_user_map[$log->user_sales_volume_id] === $np->user_id;
|
||||
});
|
||||
|
||||
$month_points = (int) $month_logs->sum('points_accumulated');
|
||||
$monthly[] = $month_points;
|
||||
|
||||
foreach ($month_logs as $log) {
|
||||
$transactions[] = [
|
||||
'date' => $log->created_at->format('d.m.Y'),
|
||||
'month' => $period['month'],
|
||||
'year' => $period['year'],
|
||||
'label' => $log->source_label ?: 'KP #'.($log->user_sales_volume_id ?? $log->source_id),
|
||||
'points' => $log->points_accumulated,
|
||||
'type' => 'accumulated',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Einmal-Punkte als Transaktion hinzufuegen
|
||||
$onetime_log = $partner_logs
|
||||
->where('source_type', User::class)
|
||||
->first(function ($log) use ($np) {
|
||||
if ($log->incentive_new_partner_id) {
|
||||
return (int) $log->incentive_new_partner_id === (int) $np->id;
|
||||
}
|
||||
|
||||
return (int) $log->source_id === (int) $np->user_id;
|
||||
});
|
||||
|
||||
if ($onetime_log) {
|
||||
array_unshift($transactions, [
|
||||
'date' => $onetime_log->created_at->format('d.m.Y'),
|
||||
'month' => $onetime_log->month,
|
||||
'year' => $onetime_log->year,
|
||||
'label' => __('incentive.onetime_registration'),
|
||||
'points' => $onetime_log->points_onetime,
|
||||
'type' => 'onetime',
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $np->id,
|
||||
'label' => $np->user ? ($np->user->getFullName() ?: $np->user->email ?: 'User #'.$np->user_id) : 'User #'.$np->user_id,
|
||||
'month' => $np->registered_at->month,
|
||||
'year' => $np->registered_at->year,
|
||||
'onetime' => $incentive->points_partner_onetime,
|
||||
'monthly' => $monthly,
|
||||
'total' => $incentive->points_partner_onetime + array_sum($monthly),
|
||||
'transactions' => $transactions,
|
||||
];
|
||||
});
|
||||
|
||||
// Abos aus Tracking-Tabelle
|
||||
$new_abos = IncentiveNewAbo::where('participant_id', $participant->id)
|
||||
->with('userAbo')
|
||||
->orderBy('activated_at')
|
||||
->get();
|
||||
|
||||
$abo_logs = $all_logs->where('type', 'abo');
|
||||
|
||||
// Legacy-Fallback: USV -> user_abo_id (Logs ohne incentive_new_abo_id)
|
||||
$sv_user_abo_map = [];
|
||||
$needs_legacy_abo_map = $abo_logs->whereNull('incentive_new_abo_id')->whereNotNull('user_sales_volume_id')->isNotEmpty();
|
||||
if ($needs_legacy_abo_map && ! empty($sv_ids)) {
|
||||
$sv_rows = UserSalesVolume::query()
|
||||
->whereIn('id', $sv_ids)
|
||||
->whereNotNull('shopping_order_id')
|
||||
->get(['id', 'shopping_order_id']);
|
||||
$order_ids = $sv_rows->pluck('shopping_order_id')->unique()->filter()->values();
|
||||
if ($order_ids->isNotEmpty()) {
|
||||
$user_abo_id_by_order_id = UserAboOrder::query()
|
||||
->whereIn('shopping_order_id', $order_ids)
|
||||
->get(['shopping_order_id', 'user_abo_id'])
|
||||
->keyBy('shopping_order_id');
|
||||
foreach ($sv_rows as $sv) {
|
||||
$link = $user_abo_id_by_order_id->get($sv->shopping_order_id);
|
||||
if ($link) {
|
||||
$sv_user_abo_map[$sv->id] = (int) $link->user_abo_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$abo_sources = $new_abos->map(function ($na) use ($abo_logs, $sv_user_abo_map, $calculation_months, $incentive) {
|
||||
$monthly = [];
|
||||
$transactions = [];
|
||||
$tracked_user_abo_id = (int) $na->user_abo_id;
|
||||
|
||||
foreach ($calculation_months as $period) {
|
||||
$month_logs = $abo_logs
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->filter(function ($log) use ($na, $tracked_user_abo_id, $sv_user_abo_map) {
|
||||
if ($log->source_type !== UserSalesVolume::class) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($log->incentive_new_abo_id) {
|
||||
return (int) $log->incentive_new_abo_id === (int) $na->id;
|
||||
}
|
||||
|
||||
$usv_id = $log->user_sales_volume_id;
|
||||
if (! $usv_id || ! isset($sv_user_abo_map[$usv_id])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $sv_user_abo_map[$usv_id] === $tracked_user_abo_id;
|
||||
});
|
||||
|
||||
$month_points = (int) $month_logs->sum('points_accumulated');
|
||||
$monthly[] = $month_points;
|
||||
|
||||
foreach ($month_logs as $log) {
|
||||
$transactions[] = [
|
||||
'date' => $log->created_at->format('d.m.Y'),
|
||||
'month' => $period['month'],
|
||||
'year' => $period['year'],
|
||||
'label' => $log->source_label ?: 'SV #'.($log->user_sales_volume_id ?? $log->source_id),
|
||||
'points' => $log->points_accumulated,
|
||||
'type' => 'accumulated',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Einmal-Punkte als Transaktion
|
||||
$onetime_log = $abo_logs
|
||||
->where('source_type', '!=', UserSalesVolume::class)
|
||||
->first(function ($log) use ($na) {
|
||||
if ($log->incentive_new_abo_id) {
|
||||
return (int) $log->incentive_new_abo_id === (int) $na->id;
|
||||
}
|
||||
|
||||
return (int) $log->source_id === (int) $na->user_abo_id;
|
||||
});
|
||||
|
||||
if ($onetime_log) {
|
||||
array_unshift($transactions, [
|
||||
'date' => $onetime_log->created_at->format('d.m.Y'),
|
||||
'month' => $onetime_log->month,
|
||||
'year' => $onetime_log->year,
|
||||
'label' => __('incentive.onetime_abo_activation'),
|
||||
'points' => $onetime_log->points_onetime,
|
||||
'type' => 'onetime',
|
||||
]);
|
||||
}
|
||||
|
||||
$label = $na->userAbo?->email ?: ('Abo #'.$na->user_abo_id);
|
||||
|
||||
return [
|
||||
'id' => $na->id,
|
||||
'label' => $label,
|
||||
'month' => $na->activated_at->month,
|
||||
'year' => $na->activated_at->year,
|
||||
'onetime' => $incentive->points_abo_onetime,
|
||||
'monthly' => $monthly,
|
||||
'total' => $incentive->points_abo_onetime + array_sum($monthly),
|
||||
'transactions' => $transactions,
|
||||
];
|
||||
});
|
||||
|
||||
return [
|
||||
'incentive' => $incentive,
|
||||
'participant' => $participant,
|
||||
'calculation_months' => $calculation_months,
|
||||
'partner_sources' => $partner_sources,
|
||||
'abo_sources' => $abo_sources,
|
||||
];
|
||||
}
|
||||
|
||||
public function datatable()
|
||||
{
|
||||
$query = Incentive::query()->select('incentives.*');
|
||||
|
||||
return \DataTables::eloquent($query)
|
||||
->addColumn('action', function (Incentive $incentive) {
|
||||
return '<a href="'.route('admin_incentive_show', [$incentive->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-eye"></span></a>
|
||||
<a href="'.route('admin_incentive_edit', [$incentive->id]).'" class="btn icon-btn btn-sm btn-warning"><span class="fa fa-edit"></span></a>';
|
||||
})
|
||||
->addColumn('status_label', function (Incentive $incentive) {
|
||||
return '<span class="badge badge-'.$incentive->getStatusColor().'">'.$incentive->getStatusType().'</span>';
|
||||
})
|
||||
->addColumn('period', function (Incentive $incentive) {
|
||||
return $incentive->qualification_start->format('d.m.Y').' - '.$incentive->qualification_end->format('d.m.Y');
|
||||
})
|
||||
->addColumn('participants_count', function (Incentive $incentive) {
|
||||
return $incentive->participants()->count();
|
||||
})
|
||||
->orderColumn('name', 'name $1')
|
||||
->orderColumn('status_label', 'status $1')
|
||||
->rawColumns(['action', 'status_label'])
|
||||
->make(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -105,29 +105,39 @@ class PayoneController extends Controller
|
|||
echo 'TSOK';
|
||||
exit;
|
||||
}
|
||||
|
||||
/* TODO -- need this? */
|
||||
/*
|
||||
* Payone sendet dieselbe txaction oft mehrfach (v. a. "appointed"). War der Status
|
||||
* bereits auf ShoppingPayment gespeichert, ist das ein Duplikat: TSOK, keine Doppel-Verarbeitung.
|
||||
* Ausnahme: erneutes "paid", obwohl die Bestellung noch nicht als bezahlt gefuehrt wird (Recovery).
|
||||
*/
|
||||
if ($shopping_payment->txaction == $data['txaction']) {
|
||||
|
||||
if ($data['txaction'] === 'paid' && $shopping_order->txaction === 'paid') {
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error:2007 App\Http\Controllers\Api\PayoneController::paymentStatus same txaction - was already paid',
|
||||
'notice',
|
||||
'App\Http\Controllers\Api\PayoneController::paymentStatus duplicate callback ignored (already paid)',
|
||||
$data,
|
||||
false
|
||||
);
|
||||
// was already paid
|
||||
echo 'TSOK';
|
||||
exit;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (in_array($data['txaction'], ['appointed', 'failed', 'pending'], true)) {
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'error',
|
||||
'Error:2007 App\Http\Controllers\Api\PayoneController::paymentStatus same txaction - show',
|
||||
$data,
|
||||
'info',
|
||||
'App\Http\Controllers\Api\PayoneController::paymentStatus duplicate callback ignored (same txaction)',
|
||||
[
|
||||
'reference' => $data['reference'] ?? null,
|
||||
'param' => $data['param'] ?? null,
|
||||
'txaction' => $data['txaction'],
|
||||
'txid' => $data['txid'] ?? null,
|
||||
],
|
||||
false
|
||||
);
|
||||
echo 'TSOK';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +201,6 @@ class PayoneController extends Controller
|
|||
$locked_order = ShoppingOrder::where('id', $shopping_order->id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
// Double-check if payment was already processed
|
||||
if (! $locked_order->paid) {
|
||||
$send_link = Payment::paymentStatusPaidAction($locked_order, true, $shopping_payment);
|
||||
|
|
@ -211,6 +220,7 @@ class PayoneController extends Controller
|
|||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$data['send_link'] = $send_link;
|
||||
if ($send_mail) {
|
||||
Payment::paymentStatusSendMail($shopping_order, $shopping_payment, $data);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DashboardNews;
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
|
|
@ -20,6 +23,7 @@ class HomeController extends Controller
|
|||
|
||||
public function index()
|
||||
{
|
||||
|
||||
if (! Auth::check()) {
|
||||
return redirect('login');
|
||||
}
|
||||
|
|
@ -43,10 +47,26 @@ class HomeController extends Controller
|
|||
return redirect('login');
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
$activeIncentive = null;
|
||||
$incentiveParticipant = null;
|
||||
|
||||
if ($user->isActiveAccount()) {
|
||||
$activeIncentive = Incentive::active()->first();
|
||||
if ($activeIncentive) {
|
||||
$incentiveParticipant = IncentiveParticipant::where('incentive_id', $activeIncentive->id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'user' => Auth::user(),
|
||||
'user' => $user,
|
||||
'now' => Carbon::now(),
|
||||
'dashboardNews' => \App\Models\DashboardNews::getActiveNews(),
|
||||
'dashboardNews' => DashboardNews::getActiveNews(),
|
||||
'activeIncentive' => $activeIncentive,
|
||||
'incentiveParticipant' => $incentiveParticipant,
|
||||
];
|
||||
|
||||
return view('home', $data);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use App\Services\Payment;
|
|||
use App\Services\Util;
|
||||
use App\User;
|
||||
use Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Request;
|
||||
|
||||
class PaymentCreditController extends Controller
|
||||
|
|
@ -183,6 +184,36 @@ class PaymentCreditController extends Controller
|
|||
return $query;
|
||||
}
|
||||
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
$this->setFilterVars();
|
||||
|
||||
$month = Request::get('credit_filter_month', session('credit_filter_month'));
|
||||
$year = Request::get('credit_filter_year', session('credit_filter_year'));
|
||||
$name = Request::get('credit_filter_name', '');
|
||||
|
||||
$dateStart = Carbon::parse('01.'.$month.'.'.$year)->format('Y-m-d');
|
||||
$dateEnd = Carbon::parse('01.'.$month.'.'.$year)->endOfMonth()->format('Y-m-d');
|
||||
|
||||
$baseQuery = UserCredit::query()
|
||||
->whereBetween('date', [$dateStart, $dateEnd]);
|
||||
|
||||
if ($name) {
|
||||
$baseQuery->whereHas('user.account', function ($query) use ($name) {
|
||||
$query->where('first_name', 'LIKE', '%'.$name.'%')
|
||||
->orWhere('last_name', 'LIKE', '%'.$name.'%');
|
||||
});
|
||||
}
|
||||
|
||||
$count = (clone $baseQuery)->count();
|
||||
$total = (clone $baseQuery)->sum('total');
|
||||
|
||||
return response()->json([
|
||||
'count' => $count,
|
||||
'total' => number_format((float) $total, 2, ',', '.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function datatable()
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Carbon;
|
||||
use Request;
|
||||
use App\Services\Payment;
|
||||
use App\Models\UserInvoice;
|
||||
use App\Services\HTMLHelper;
|
||||
use App\Services\Payment;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Request;
|
||||
|
||||
class PaymentInvoiceController extends Controller
|
||||
{
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('admin');
|
||||
|
|
@ -26,16 +23,17 @@ class PaymentInvoiceController extends Controller
|
|||
'filter_months' => HTMLHelper::getTransMonths(),
|
||||
'filter_years' => HTMLHelper::getYearRange(),
|
||||
];
|
||||
|
||||
return view('admin.payment.invoice', $data);
|
||||
}
|
||||
|
||||
private function setFilterVars()
|
||||
{
|
||||
|
||||
if (!session('invoice_filter_month')) {
|
||||
if (! session('invoice_filter_month')) {
|
||||
session(['invoice_filter_month' => intval(date('m'))]);
|
||||
}
|
||||
if (!session('invoice_filter_year')) {
|
||||
if (! session('invoice_filter_year')) {
|
||||
session(['invoice_filter_year' => intval(date('Y'))]);
|
||||
}
|
||||
if (Request::get('invoice_filter_name')) {
|
||||
|
|
@ -61,12 +59,44 @@ class PaymentInvoiceController extends Controller
|
|||
|
||||
if (Request::get('invoice_filter_name')) {
|
||||
$query->whereHas('shopping_order.shopping_user', function ($query) {
|
||||
return $query->where('billing_firstname', 'LIKE', '%' . Request::get('invoice_filter_name') . '%')->orWhere('billing_lastname', 'LIKE', '%' . Request::get('invoice_filter_name') . '%')->orWhere('billing_email', 'LIKE', '%' . Request::get('invoice_filter_name') . '%');
|
||||
return $query->where('billing_firstname', 'LIKE', '%'.Request::get('invoice_filter_name').'%')->orWhere('billing_lastname', 'LIKE', '%'.Request::get('invoice_filter_name').'%')->orWhere('billing_email', 'LIKE', '%'.Request::get('invoice_filter_name').'%');
|
||||
})->get();
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
$this->setFilterVars();
|
||||
|
||||
$month = Request::get('invoice_filter_month', session('invoice_filter_month'));
|
||||
$year = Request::get('invoice_filter_year', session('invoice_filter_year'));
|
||||
$name = Request::get('invoice_filter_name', '');
|
||||
|
||||
$baseQuery = UserInvoice::query()
|
||||
->where('user_invoices.month', $month)
|
||||
->where('user_invoices.year', $year);
|
||||
|
||||
if ($name) {
|
||||
$baseQuery->whereHas('shopping_order.shopping_user', function ($query) use ($name) {
|
||||
$query->where('billing_firstname', 'LIKE', '%'.$name.'%')
|
||||
->orWhere('billing_lastname', 'LIKE', '%'.$name.'%')
|
||||
->orWhere('billing_email', 'LIKE', '%'.$name.'%');
|
||||
});
|
||||
}
|
||||
|
||||
$count = (clone $baseQuery)->count();
|
||||
$total = (clone $baseQuery)
|
||||
->join('shopping_orders', 'shopping_orders.id', '=', 'user_invoices.shopping_order_id')
|
||||
->sum('shopping_orders.total_shipping');
|
||||
|
||||
return response()->json([
|
||||
'count' => $count,
|
||||
'total' => number_format((float) $total, 2, ',', '.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function datatable()
|
||||
{
|
||||
|
||||
|
|
@ -75,32 +105,35 @@ class PaymentInvoiceController extends Controller
|
|||
return \DataTables::eloquent($query)
|
||||
->addColumn('id', function (UserInvoice $UserInvoice) {
|
||||
if ($UserInvoice->shopping_order->auth_user_id) {
|
||||
return '<a href="' . route('admin_sales_users_detail', [$UserInvoice->shopping_order->id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
||||
return '<a href="'.route('admin_sales_users_detail', [$UserInvoice->shopping_order->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
||||
}
|
||||
return '<a href="' . route('admin_sales_customers_detail', [$UserInvoice->shopping_order->id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
||||
|
||||
return '<a href="'.route('admin_sales_customers_detail', [$UserInvoice->shopping_order->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
||||
})
|
||||
->addColumn('total_shipping', function (UserInvoice $UserInvoice) {
|
||||
return '<span class="no-line-break">' . $UserInvoice->shopping_order->getFormattedTotalShipping() . " €</span>";
|
||||
return '<span class="no-line-break">'.$UserInvoice->shopping_order->getFormattedTotalShipping().' €</span>';
|
||||
})
|
||||
->addColumn('created_at', function (UserInvoice $UserInvoice) {
|
||||
return $UserInvoice->created_at->format("d.m.Y");
|
||||
return $UserInvoice->created_at->format('d.m.Y');
|
||||
})
|
||||
->addColumn('txaction', function (UserInvoice $UserInvoice) {
|
||||
if ($UserInvoice->shopping_order) {
|
||||
return Payment::getShoppingOrderBadge($UserInvoice->shopping_order);
|
||||
}
|
||||
return "-";
|
||||
|
||||
return '-';
|
||||
})
|
||||
->addColumn('status', function (UserInvoice $UserInvoice) {
|
||||
return '<a href="#" data-toggle="modal" data-target="#modals-load-content" data-modal="modal-lg"
|
||||
data-id="' . $UserInvoice->id . '" data-route="' . route('modal_load') . '" data-action="user-credit-status" data-view="">
|
||||
<span class="badge badge-pill badge-' . $UserInvoice->getStatusColor() . '">' . $UserInvoice->getStatusType() . '</span>
|
||||
data-id="'.$UserInvoice->id.'" data-route="'.route('modal_load').'" data-action="user-credit-status" data-view="">
|
||||
<span class="badge badge-pill badge-'.$UserInvoice->getStatusColor().'">'.$UserInvoice->getStatusType().'</span>
|
||||
</a>';
|
||||
})
|
||||
->addColumn('invoice', function (UserInvoice $UserInvoice) {
|
||||
$ret = "";
|
||||
$ret .= '<a href="' . route('storage_file', [$UserInvoice->shopping_order->id, 'invoice', 'download']) . '" class="btn btn-primary btn-xs"><i class="fa fa-download"></i></a> ';
|
||||
$ret .= '<a href="' . route('storage_file', [$UserInvoice->shopping_order->id, 'invoice', 'stream']) . '" target="_blank" class="btn btn-warning btn-xs"><i class="fa fa-eye"></i></a>';
|
||||
$ret = '';
|
||||
$ret .= '<a href="'.route('storage_file', [$UserInvoice->shopping_order->id, 'invoice', 'download']).'" class="btn btn-primary btn-xs"><i class="fa fa-download"></i></a> ';
|
||||
$ret .= '<a href="'.route('storage_file', [$UserInvoice->shopping_order->id, 'invoice', 'stream']).'" target="_blank" class="btn btn-warning btn-xs"><i class="fa fa-eye"></i></a>';
|
||||
|
||||
return $ret;
|
||||
})
|
||||
->orderColumn('id', 'id $1')
|
||||
|
|
|
|||
|
|
@ -403,16 +403,20 @@ class AboController extends Controller
|
|||
$data['step'] = 4;
|
||||
break;
|
||||
case 5:
|
||||
// chekout verarbeiten
|
||||
UserService::setInstance($this->instance);
|
||||
UserService::initCustomerYard($shopping_user, 'abo-ot-customer');
|
||||
if (Request::get('action') == 'checkout') {
|
||||
// checkout verarbeiten
|
||||
if (! $this->preCheckCheckout()) {
|
||||
if (! Request::boolean('abo_order_info_checkbox')) {
|
||||
$data['error'] = __('abo.abo_order_info_checkbox_required');
|
||||
$data['step'] = 4;
|
||||
} elseif (! in_array((int) Request::input('abo_interval'), UserAbo::$aboDeliveryDays, true)) {
|
||||
$data['error'] = __('abo.error_abo_interval');
|
||||
$data['step'] = 4;
|
||||
} elseif (! $this->preCheckCheckout()) {
|
||||
$data['error'] = __('abo.abo_error_basis_product');
|
||||
$data['step'] = 4;
|
||||
} else {
|
||||
$data['checkout_url'] = $this->processCheckout();
|
||||
$data['checkout_url'] = $this->processCheckout($shopping_user);
|
||||
}
|
||||
}
|
||||
$data['step'] = 4;
|
||||
|
|
@ -439,18 +443,9 @@ class AboController extends Controller
|
|||
Shop::initUserShopLang($delivery_country, $this->instance);
|
||||
}
|
||||
|
||||
private function preCheckCheckout()
|
||||
private function preCheckCheckout(): bool
|
||||
{
|
||||
$result = false;
|
||||
// alle inhlate des warenkorb
|
||||
$cartItems = $this->yard->content();
|
||||
foreach ($cartItems as $item) {
|
||||
if (in_array(12, $item->options->show_on)) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
return AboHelper::aboHasBaseProduct($this->yard->getContentByOrder());
|
||||
}
|
||||
|
||||
private function checkBasisProduct()
|
||||
|
|
@ -550,7 +545,7 @@ class AboController extends Controller
|
|||
$this->yard->reCalculateShippingPrice();
|
||||
}
|
||||
|
||||
private function processCheckout()
|
||||
private function processCheckout(ShoppingUser $shoppingUser): string
|
||||
{
|
||||
$user_shop = Util::getUserShop();
|
||||
if (! $user_shop) {
|
||||
|
|
@ -560,24 +555,38 @@ class AboController extends Controller
|
|||
$identifier = Util::getToken();
|
||||
} while (ShoppingInstance::where('identifier', $identifier)->count());
|
||||
|
||||
$data = [];
|
||||
$data['is_from'] = 'shopping';
|
||||
$data['user_price_infos'] = $this->yard->getUserPriceInfos();
|
||||
$aboInterval = (int) Request::input('abo_interval', 0);
|
||||
|
||||
$fillable = (new ShoppingUser)->getFillable();
|
||||
$shoppingData = array_merge(
|
||||
array_intersect_key($shoppingUser->getAttributes(), array_flip($fillable)),
|
||||
[
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'is_from' => 'shopping',
|
||||
'is_for' => 'abo-ot-customer',
|
||||
'is_abo' => true,
|
||||
'abo_interval' => $aboInterval,
|
||||
'shipping_is_for' => 'abo-ot-customer',
|
||||
'user_price_infos' => $this->yard->getUserPriceInfos(),
|
||||
'mode' => config('app.mode') === 'test' ? 'test' : 'live',
|
||||
]
|
||||
);
|
||||
|
||||
ShoppingInstance::create([
|
||||
'identifier' => $identifier,
|
||||
'user_shop_id' => $user_shop->id,
|
||||
'payment' => 1, // Customer Shop Payment
|
||||
'payment' => 1,
|
||||
'subdomain' => url('/'),
|
||||
'country_id' => $this->yard->getShippingCountryId(),
|
||||
'language' => \App::getLocale(),
|
||||
'shopping_data' => $data,
|
||||
'language' => $shoppingUser->getLocale(),
|
||||
'amount' => (float) $this->yard->totalWithShipping(2, '.', ''),
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'shopping_data' => $shoppingData,
|
||||
'back' => url()->previous(),
|
||||
|
||||
]);
|
||||
|
||||
$this->yard->store($identifier);
|
||||
// add to DB
|
||||
|
||||
$path = route('checkout.checkout_card', ['identifier' => $identifier]);
|
||||
if (strpos($path, 'https') === false) {
|
||||
$path = str_replace('http', 'https', $path);
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ use App\Models\ShoppingOrder;
|
|||
use App\Models\ShoppingPayment;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\Payment;
|
||||
use App\Services\ProductOrderContext;
|
||||
use App\Services\Shop;
|
||||
use App\Services\Util;
|
||||
use Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Yard;
|
||||
|
||||
class OrderController extends Controller
|
||||
|
|
@ -192,7 +194,7 @@ class OrderController extends Controller
|
|||
public function myOrderCreate(int $id)
|
||||
{
|
||||
$user = Auth::guard('customers')->user();
|
||||
$shopping_order = ShoppingOrder::findOrFail($id);
|
||||
$shopping_order = ShoppingOrder::with('member.shop')->findOrFail($id);
|
||||
|
||||
if ($shopping_order->shopping_user_id != $user->shopping_user_id) {
|
||||
$shopping_user = ShoppingUser::findOrFail($user->shopping_user_id);
|
||||
|
|
@ -202,6 +204,13 @@ class OrderController extends Controller
|
|||
}
|
||||
|
||||
$shopping_user = ShoppingUser::findOrFail($user->shopping_user_id);
|
||||
|
||||
if ($shopping_order->is_abo) {
|
||||
Session::flash('alert-error', __('order.reorder_abo_not_allowed'));
|
||||
|
||||
return redirect()->route('portal.my_orders.show', $shopping_order->id);
|
||||
}
|
||||
|
||||
$delivery_country = $shopping_user->getDeliveryCountry(true);
|
||||
|
||||
\Session::put('user_init_country', strtolower($delivery_country->code));
|
||||
|
|
@ -211,18 +220,18 @@ class OrderController extends Controller
|
|||
Shop::initUserShopLang($delivery_country, $this->instance);
|
||||
|
||||
foreach ($shopping_order->shopping_order_items as $item) {
|
||||
if ($item->product) {
|
||||
if ($item->product && ProductOrderContext::isProductAllowedInCustomerWebshop($item->product)) {
|
||||
$this->addToCart($item->product_id, $item->qty);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect(Util::getMyMivitaShopUrl('/user/card/show'));
|
||||
return redirect(Util::getCustomerReorderCartUrl($shopping_order));
|
||||
}
|
||||
|
||||
private function addToCart(int $productId, int $quantity = 1): void
|
||||
{
|
||||
$product = Product::find($productId);
|
||||
if (! $product) {
|
||||
if (! $product || ! ProductOrderContext::isProductAllowedInCustomerWebshop($product)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
42
app/Http/Controllers/SAdmin/SAdminController.php
Normal file
42
app/Http/Controllers/SAdmin/SAdminController.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\SAdmin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\SyS\AboOrdersOverview;
|
||||
|
||||
class SAdminController extends Controller
|
||||
{
|
||||
protected $userRepo;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('superadmin');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('sys.index');
|
||||
}
|
||||
|
||||
public function tool($serve)
|
||||
{
|
||||
switch ($serve) {
|
||||
|
||||
case 'abo_orders_overview':
|
||||
return AboOrdersOverview::show();
|
||||
break;
|
||||
}
|
||||
abort(403, 'not found tool');
|
||||
}
|
||||
|
||||
public function store($serve)
|
||||
{
|
||||
switch ($serve) {
|
||||
case 'abo_orders_overview':
|
||||
// return AboOrdersOverview::store();
|
||||
break;
|
||||
}
|
||||
abort(403, 'not found tool');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,23 +2,23 @@
|
|||
|
||||
namespace App\Http\Controllers\SyS;
|
||||
|
||||
use Carbon;
|
||||
use Request;
|
||||
use App\Services\SyS\Sales;
|
||||
use App\Services\SyS\Import;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\SyS\AboOrdersOverview;
|
||||
use App\Services\SyS\BusinessStructur;
|
||||
use App\Services\SyS\BuyingsProducts;
|
||||
use App\Services\SyS\ChangeUserBusinesses;
|
||||
use App\Services\SyS\CleanHTMLProductDescription;
|
||||
use App\Services\SyS\Correction;
|
||||
use App\Services\SyS\Cronjobs;
|
||||
use App\Services\SyS\Customers;
|
||||
use App\Services\SyS\DomainSSL;
|
||||
use App\Services\SyS\Correction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\SyS\ShoppingOrders;
|
||||
use App\Services\SyS\BuyingsProducts;
|
||||
use App\Services\SyS\BusinessStructur;
|
||||
use App\Services\SyS\Import;
|
||||
use App\Services\SyS\ImportDbipCountry;
|
||||
use App\Services\SyS\ChangeUserBusinesses;
|
||||
use App\Services\SyS\UserCreditItemsAddFrom;
|
||||
use App\Services\SyS\PayoneCallbackTestbench;
|
||||
use App\Services\SyS\RepairSalesVolumeInvoice;
|
||||
use App\Services\SyS\CleanHTMLProductDescription;
|
||||
use App\Services\SyS\Sales;
|
||||
use App\Services\SyS\ShoppingOrders;
|
||||
use App\Services\SyS\UserCreditItemsAddFrom;
|
||||
use App\Services\SyS\UserCreditItemsChangeMessage;
|
||||
|
||||
class SysController extends Controller
|
||||
|
|
@ -28,18 +28,17 @@ class SysController extends Controller
|
|||
public function __construct()
|
||||
{
|
||||
$this->middleware('sysadmin');
|
||||
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
{
|
||||
return view('sys.index');
|
||||
}
|
||||
|
||||
public function tool($serve)
|
||||
{
|
||||
{
|
||||
switch ($serve) {
|
||||
|
||||
|
||||
case 'user_credit_items_add_from':
|
||||
return UserCreditItemsAddFrom::show();
|
||||
break;
|
||||
|
|
@ -54,19 +53,19 @@ class SysController extends Controller
|
|||
break;
|
||||
case 'customers':
|
||||
return Customers::show();
|
||||
break;
|
||||
break;
|
||||
case 'cronjobs':
|
||||
return Cronjobs::show();
|
||||
break;
|
||||
break;
|
||||
case 'domainssl':
|
||||
return DomainSSL::show();
|
||||
break;
|
||||
case 'shopping_orders':
|
||||
return ShoppingOrders::show();
|
||||
break;
|
||||
break;
|
||||
case 'import':
|
||||
return Import::show();
|
||||
break;
|
||||
break;
|
||||
case 'corrections':
|
||||
return Correction::show();
|
||||
break;
|
||||
|
|
@ -75,27 +74,28 @@ class SysController extends Controller
|
|||
break;
|
||||
case 'repair_sales_volume_invoice':
|
||||
return RepairSalesVolumeInvoice::show();
|
||||
break;
|
||||
break;
|
||||
case 'user_credit_items_change_message':
|
||||
return UserCreditItemsChangeMessage::show();
|
||||
break;
|
||||
case 'clean_html_product_description':
|
||||
break;
|
||||
case 'clean_html_product_description':
|
||||
return CleanHTMLProductDescription::show();
|
||||
break;
|
||||
break;
|
||||
case 'import_dbip_country_lite':
|
||||
return ImportDbipCountry::show();
|
||||
break;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
break;
|
||||
case 'abo_orders_overview':
|
||||
return AboOrdersOverview::show();
|
||||
break;
|
||||
case 'payone_callback_testbench':
|
||||
return PayoneCallbackTestbench::show();
|
||||
break;
|
||||
}
|
||||
abort(403, 'not found tool');
|
||||
abort(403, 'not found tool');
|
||||
}
|
||||
|
||||
|
||||
public function store($serve)
|
||||
{
|
||||
{
|
||||
switch ($serve) {
|
||||
case 'user_credit_items_add_from':
|
||||
return UserCreditItemsAddFrom::show();
|
||||
|
|
@ -111,38 +111,41 @@ class SysController extends Controller
|
|||
break;
|
||||
case 'customers':
|
||||
return Customers::store();
|
||||
break;
|
||||
break;
|
||||
case 'cronjobs':
|
||||
return Cronjobs::store();
|
||||
break;
|
||||
break;
|
||||
case 'domainssl':
|
||||
return DomainSSL::store();
|
||||
break;
|
||||
case 'shopping_orders':
|
||||
return ShoppingOrders::store();
|
||||
break;
|
||||
break;
|
||||
case 'import':
|
||||
return Import::store();
|
||||
break;
|
||||
break;
|
||||
case 'corrections':
|
||||
return Correction::store();
|
||||
break;
|
||||
case 'change_user_businesses':
|
||||
return ChangeUserBusinesses::store();
|
||||
break;
|
||||
break;
|
||||
case 'repair_sales_volume_invoice':
|
||||
return RepairSalesVolumeInvoice::store();
|
||||
break;
|
||||
break;
|
||||
case 'user_credit_items_change_message':
|
||||
return UserCreditItemsChangeMessage::store();
|
||||
break;
|
||||
break;
|
||||
case 'clean_html_product_description':
|
||||
return CleanHTMLProductDescription::store();
|
||||
break;
|
||||
break;
|
||||
case 'import_dbip_country_lite':
|
||||
return ImportDbipCountry::store();
|
||||
break;
|
||||
break;
|
||||
case 'payone_callback_testbench':
|
||||
return PayoneCallbackTestbench::store();
|
||||
break;
|
||||
}
|
||||
abort(403, 'not found tool');
|
||||
abort(403, 'not found tool');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,9 +45,13 @@ class AboController extends Controller
|
|||
}
|
||||
|
||||
if ($view === 'ot') {
|
||||
$user_abos = UserAbo::where('member_id', \Auth::user()->id)
|
||||
$selectedYear = (int) \Request::get('year', now()->year);
|
||||
$baseQuery = UserAbo::where('member_id', \Auth::user()->id)
|
||||
->where('status', '>', 1)
|
||||
->where('is_for', 'ot')
|
||||
->where('is_for', 'ot');
|
||||
|
||||
$user_abos = (clone $baseQuery)
|
||||
->with(['user_abo_items', 'user_abo_items.product', 'shopping_user'])
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
|
|
@ -55,6 +59,10 @@ class AboController extends Controller
|
|||
'user_abos' => $user_abos,
|
||||
'view' => 'ot',
|
||||
'isAdmin' => false,
|
||||
'chartData' => AboHelper::getMonthlyAboCounts($baseQuery, $selectedYear, 'ot', \Auth::user()->id),
|
||||
'chartYear' => $selectedYear,
|
||||
'chartYears' => \App\Services\HTMLHelper::getYearRange(2026),
|
||||
'chartMonths' => \App\Services\HTMLHelper::getTransMonths(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
200
app/Http/Controllers/User/IncentiveController.php
Normal file
200
app/Http/Controllers/User/IncentiveController.php
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Admin\IncentiveController as AdminIncentiveController;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\UserAbo;
|
||||
use App\Services\Incentive\IncentivePointsLogRepairService;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Request;
|
||||
|
||||
class IncentiveController extends Controller
|
||||
{
|
||||
/**
|
||||
* Anzahl Plaetze in der User-Live-Rangliste (Gewinner-Highlight bleibt ueber max_winners, typ. Top 20).
|
||||
*/
|
||||
public const USER_RANKING_DISPLAY_LIMIT = 30;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('active.account');
|
||||
}
|
||||
|
||||
public function teaser($slug)
|
||||
{
|
||||
$incentive = Incentive::where('slug', $slug)
|
||||
->where('status', '!=', 0)
|
||||
->firstOrFail();
|
||||
|
||||
$user = Auth::user();
|
||||
$participant = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
|
||||
$galleryImages = $this->collectGalleryImages($incentive);
|
||||
|
||||
return view('user.incentive.teaser', [
|
||||
'incentive' => $incentive,
|
||||
'participant' => $participant,
|
||||
'galleryImages' => $galleryImages,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($slug)
|
||||
{
|
||||
$incentive = Incentive::where('slug', $slug)
|
||||
->where('status', '!=', 0) // not draft
|
||||
->firstOrFail();
|
||||
|
||||
$user = Auth::user();
|
||||
$participant = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
|
||||
$ranking = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->withRankingActivity()
|
||||
->with('user', 'user.account')
|
||||
->orderByIncentiveLeaderboard()
|
||||
->limit(self::USER_RANKING_DISPLAY_LIMIT)
|
||||
->get();
|
||||
|
||||
$participateHasTrackableAbos = false;
|
||||
if (! $participant?->accepted_terms_at) {
|
||||
$participateHasTrackableAbos = $this->userHasTrackableAbosForIncentive($user, $incentive);
|
||||
}
|
||||
|
||||
return view('user.incentive.show', [
|
||||
'incentive' => $incentive,
|
||||
'participant' => $participant,
|
||||
'hasConfirmedParticipation' => $participant && $participant->accepted_terms_at !== null,
|
||||
'ranking' => $ranking,
|
||||
'rankingDisplayLimit' => self::USER_RANKING_DISPLAY_LIMIT,
|
||||
'participateHasTrackableAbos' => $participateHasTrackableAbos,
|
||||
]);
|
||||
}
|
||||
|
||||
public function participate($slug)
|
||||
{
|
||||
$incentive = Incentive::where('slug', $slug)
|
||||
->active()
|
||||
->firstOrFail();
|
||||
|
||||
if (! Request::has('accept_terms')) {
|
||||
\Session()->flash('alert-error', __('incentive.terms_required'));
|
||||
|
||||
return redirect(route('user_incentive_show', [$slug]));
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
$participant = IncentiveParticipant::firstOrCreate(
|
||||
[
|
||||
'incentive_id' => $incentive->id,
|
||||
'user_id' => $user->id,
|
||||
],
|
||||
[
|
||||
'accepted_terms_at' => null,
|
||||
]
|
||||
);
|
||||
|
||||
if ($participant->accepted_terms_at !== null) {
|
||||
\Session()->flash('alert-info', __('incentive.already_participating'));
|
||||
|
||||
return redirect(route('user_incentive_show', [$slug]));
|
||||
}
|
||||
|
||||
$participant->accepted_terms_at = Carbon::now();
|
||||
$participant->save();
|
||||
|
||||
$repair = app(IncentivePointsLogRepairService::class);
|
||||
$repair->syncMissingTrackingAbos($participant);
|
||||
$repair->syncMissingSalesVolumeLogs($participant);
|
||||
$participant->refresh()->recalculateFromTrackingTables()->save();
|
||||
IncentiveTracker::updateRanking($incentive);
|
||||
|
||||
\Session()->flash('alert-success', __('incentive.participation_confirmed'));
|
||||
|
||||
return redirect(route('user_incentive_show', [$slug]));
|
||||
}
|
||||
|
||||
public function details($slug)
|
||||
{
|
||||
$incentive = Incentive::where('slug', $slug)
|
||||
->where('status', '!=', 0)
|
||||
->firstOrFail();
|
||||
|
||||
$user = Auth::user();
|
||||
$participant = IncentiveParticipant::with('incentive', 'user', 'user.account')
|
||||
->where('incentive_id', $incentive->id)
|
||||
->where('user_id', $user->id)
|
||||
->firstOrFail();
|
||||
|
||||
if ($participant->accepted_terms_at === null) {
|
||||
\Session()->flash('alert-info', __('incentive.details_requires_confirmation'));
|
||||
|
||||
return redirect(route('user_incentive_show', [$slug]));
|
||||
}
|
||||
|
||||
$data = AdminIncentiveController::buildParticipantDetailData($participant);
|
||||
|
||||
return view('user.incentive.details', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sammelt alle verfuegbaren Galerie-Bilder aus public/img/incentive/
|
||||
* (ohne das Hauptbild, das bereits als Hero verwendet wird).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function collectGalleryImages(Incentive $incentive): array
|
||||
{
|
||||
$dir = public_path('img/incentive');
|
||||
|
||||
if (! is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = glob($dir . '/*.{jpg,jpeg,png,webp}', GLOB_BRACE) ?: [];
|
||||
|
||||
$images = [];
|
||||
foreach ($files as $file) {
|
||||
$basename = basename($file);
|
||||
$images[] = 'img/incentive/' . $basename;
|
||||
}
|
||||
sort($images);
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hinweis auf der Teilnehmen-Karte: Es gibt bereits ein Eigenabo oder ein Kundenabo im Qualifikationszeitraum.
|
||||
*/
|
||||
private function userHasTrackableAbosForIncentive(User $user, Incentive $incentive): bool
|
||||
{
|
||||
$qualEnd = $incentive->qualification_end->copy()->endOfDay();
|
||||
|
||||
$hasOwnActiveAbo = UserAbo::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('is_for', 'me')
|
||||
->where('status', 2)
|
||||
->exists();
|
||||
|
||||
$hasCustomerAboInQualification = UserAbo::query()
|
||||
->where('member_id', $user->id)
|
||||
->where('is_for', 'ot')
|
||||
->where('status', 2)
|
||||
->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$qualEnd,
|
||||
])
|
||||
->exists();
|
||||
|
||||
return $hasOwnActiveAbo || $hasCustomerAboInQualification;
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +63,6 @@ class MembershipController extends Controller
|
|||
if ($user->isActiveAccount() && ! $user->isActiveShop()) {
|
||||
$payment_greaterThan = Carbon::parse($user->payment_account)->modify('-'.(config('mivita.renewal_days') + 1).' days');
|
||||
$userHistoryUpgradeOrder = UserHistory::whereUserId($user->id)->whereAction('upgrade_order')->where('created_at', '>=', $payment_greaterThan)->get()->last();
|
||||
|
||||
}
|
||||
$userHistoryDeleteMembership = UserHistory::whereUserId($user->id)->whereAction('delete_membership')->whereStatus(50)->get()->last();
|
||||
|
||||
|
|
@ -87,7 +86,6 @@ class MembershipController extends Controller
|
|||
];
|
||||
|
||||
return view('user.membership.index', $data);
|
||||
|
||||
}
|
||||
|
||||
private function checkShoppingCountry($user)
|
||||
|
|
@ -158,8 +156,11 @@ class MembershipController extends Controller
|
|||
if ($product->images->count()) {
|
||||
$image = $product->images->first()->slug;
|
||||
}
|
||||
$qty = Request::get('qty') ? Request::get('qty') : 1;
|
||||
$qty = $product->is_membership_only ? 1 : (Request::get('qty') ? Request::get('qty') : 1);
|
||||
$cartItem = Yard::instance('shopping')->add($product->id, $product->getLang('name'), $qty, $product->getPriceWith(\App\Services\UserService::getTaxFree(), false, \App\Services\UserService::$user_country), false, false, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]);
|
||||
if ($cartItem->qty > 1) {
|
||||
Yard::instance('shopping')->update($cartItem->rowId, 1);
|
||||
}
|
||||
if (\App\Services\UserService::getTaxFree()) {
|
||||
Yard::setTax($cartItem->rowId, 0);
|
||||
} else {
|
||||
|
|
@ -214,7 +215,6 @@ class MembershipController extends Controller
|
|||
\Session()->flash('alert-success', __('msg.booked_package_has_been_changed'));
|
||||
|
||||
return back();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -236,11 +236,9 @@ class MembershipController extends Controller
|
|||
\Session()->flash('alert-error', __('msg.error_checkbox_not_confirm'));
|
||||
|
||||
return back();
|
||||
|
||||
}
|
||||
\Session()->flash('alert-error', __('msg.error_checkbox_not_confirm'));
|
||||
|
||||
return back();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use App\Services\AboHelper;
|
|||
use App\Services\MyLog;
|
||||
use App\Services\OrderPaymentService;
|
||||
use App\Services\Payment;
|
||||
use App\Services\ProductOrderContext;
|
||||
use App\Services\Shop;
|
||||
use App\Services\UserService;
|
||||
use App\Services\Util;
|
||||
|
|
@ -182,6 +183,16 @@ class OrderController extends Controller
|
|||
$delivery_id = $shopping_user->id;
|
||||
}
|
||||
|
||||
$isAbo = str_contains($for, 'abo');
|
||||
$previousFor = session('user_order_flow_for');
|
||||
if ($previousFor !== null && $previousFor !== $for) {
|
||||
$previousAbo = str_contains($previousFor, 'abo');
|
||||
if (ProductOrderContext::allowedShowOnIds($previousAbo, $previousFor) !== ProductOrderContext::allowedShowOnIds($isAbo, $for)) {
|
||||
Yard::instance('shopping')->destroy();
|
||||
}
|
||||
}
|
||||
session(['user_order_flow_for' => $for]);
|
||||
|
||||
if ($for === 'ot-customer' || $for === 'abo-ot-customer') {
|
||||
UserService::initCustomerYard($shopping_user, $for);
|
||||
} else {
|
||||
|
|
@ -262,7 +273,7 @@ class OrderController extends Controller
|
|||
// Prepare common data
|
||||
$data['is_from'] = 'user_order';
|
||||
$data['is_for'] = $for;
|
||||
$data['is_abo'] = $data['is_abo'] ?? 0;
|
||||
$data['is_abo'] = str_contains($for, 'abo');
|
||||
$data['abo_interval'] = $data['abo_interval'] ?? 0;
|
||||
$data['shopping_user_id'] = $id;
|
||||
$data['user_price_infos'] = Yard::instance('shopping')->getUserPriceInfos();
|
||||
|
|
@ -406,6 +417,17 @@ class OrderController extends Controller
|
|||
throw new \Exception(__('msg.shipping_country_was_not_correctly'));
|
||||
}
|
||||
|
||||
$isAbo = str_contains($data['shipping_is_for'], 'abo');
|
||||
foreach (Yard::instance('shopping')->content() as $row) {
|
||||
$product = Product::find($row->id);
|
||||
if (! $product) {
|
||||
continue;
|
||||
}
|
||||
if (! ProductOrderContext::isProductAllowedInContext($product, $isAbo, $data['shipping_is_for'])) {
|
||||
throw new \Exception(__('msg.cart_product_not_allowed_for_order_type'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($data['shipping_is_for'] !== 'ot-customer') {
|
||||
if (Yard::instance('shopping')->shipping_free) {
|
||||
$identifier = 'error-'.time().mt_rand(1000000, 9999999);
|
||||
|
|
@ -748,6 +770,15 @@ class OrderController extends Controller
|
|||
return response()->json(['response' => false, 'message' => 'Product not found']);
|
||||
}
|
||||
|
||||
$isAbo = str_contains($is_for, 'abo');
|
||||
$qty = isset($data['qty']) ? (int) $data['qty'] : 0;
|
||||
if ($qty > 0 && ! ProductOrderContext::isProductAllowedInContext($product, $isAbo, $is_for)) {
|
||||
return response()->json([
|
||||
'response' => false,
|
||||
'message' => __('msg.cart_product_not_allowed_for_order_type'),
|
||||
]);
|
||||
}
|
||||
|
||||
$image = '';
|
||||
if ($product->images->count()) {
|
||||
$image = $product->images->first()->slug;
|
||||
|
|
|
|||
|
|
@ -713,9 +713,13 @@ class TeamController extends Controller
|
|||
// Hole Team-Mitglieder-IDs effizient via Sponsor-Hierarchie
|
||||
$teamUserIds = AboHelper::getTeamUserIds($user->id);
|
||||
|
||||
// Hole Abos der Team-Mitglieder
|
||||
$abos = \App\Models\UserAbo::whereIn('user_id', $teamUserIds)
|
||||
$selectedYear = (int) Request::get('year', now()->year);
|
||||
$baseQuery = \App\Models\UserAbo::whereIn('user_id', $teamUserIds)
|
||||
->where('is_for', 'me')
|
||||
->where('status', '>', 1);
|
||||
|
||||
// Hole Abos der Team-Mitglieder
|
||||
$abos = (clone $baseQuery)
|
||||
->with(['user', 'user.account', 'user_abo_items', 'user_abo_items.product'])
|
||||
->orderBy('next_date', 'asc')
|
||||
->get();
|
||||
|
|
@ -724,11 +728,45 @@ class TeamController extends Controller
|
|||
'filter_months' => HTMLHelper::getTransMonths(),
|
||||
'filter_years' => HTMLHelper::getYearRange(2022),
|
||||
'abos' => $abos,
|
||||
'chartData' => AboHelper::getMonthlyAboCounts($baseQuery, $selectedYear, 'team_abos', $user->id),
|
||||
'chartYear' => $selectedYear,
|
||||
'chartYears' => HTMLHelper::getYearRange(2026),
|
||||
'chartMonths' => HTMLHelper::getTransMonths(),
|
||||
];
|
||||
|
||||
return view('user.team.abos', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Übersicht der Kunden-Abos aller Team-Mitglieder (anonymisiert)
|
||||
*/
|
||||
public function showTeamCustomerAbos(): \Illuminate\View\View
|
||||
{
|
||||
$user = User::find(\Auth::user()->id);
|
||||
$teamUserIds = AboHelper::getTeamUserIds($user->id);
|
||||
|
||||
$selectedYear = (int) Request::get('year', now()->year);
|
||||
$baseQuery = \App\Models\UserAbo::whereIn('member_id', $teamUserIds)
|
||||
->where('is_for', 'ot')
|
||||
->where('status', '>', 1);
|
||||
|
||||
$abos = (clone $baseQuery)
|
||||
->with(['member', 'member.account', 'user_abo_items', 'user_abo_items.product'])
|
||||
->orderBy('member_id')
|
||||
->orderBy('next_date', 'asc')
|
||||
->get();
|
||||
|
||||
$groupedByMember = $abos->groupBy('member_id');
|
||||
|
||||
return view('user.team.customer_abos', [
|
||||
'groupedByMember' => $groupedByMember,
|
||||
'chartData' => AboHelper::getMonthlyAboCounts($baseQuery, $selectedYear, 'team_cust_abos', $user->id),
|
||||
'chartYear' => $selectedYear,
|
||||
'chartYears' => HTMLHelper::getYearRange(2026),
|
||||
'chartMonths' => HTMLHelper::getTransMonths(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt die Detail-Ansicht eines Team-Abos an
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,46 +2,49 @@
|
|||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
|
||||
use Yard;
|
||||
use Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Product;
|
||||
use App\Models\ShoppingInstance;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\ProductOrderContext;
|
||||
use App\Services\Shop;
|
||||
use App\Services\Util;
|
||||
use App\Models\Product;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\ShoppingInstance;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Request;
|
||||
use Yard;
|
||||
|
||||
class CardController extends Controller
|
||||
{
|
||||
private $instance = 'webshop';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
public function __construct() {}
|
||||
|
||||
|
||||
|
||||
//Cart::instance('wishlist')->add('sdjk922', 'Product 2', 1, 19.95, ['size' => 'medium']);
|
||||
// Cart::instance('wishlist')->add('sdjk922', 'Product 2', 1, 19.95, ['size' => 'medium']);
|
||||
public function addToCardGet($id, $quantity = 1, $product_slug = false)
|
||||
{
|
||||
$product = Product::find($id);
|
||||
if($product){
|
||||
$image = "";
|
||||
if($product->images->count()){
|
||||
if ($product && ProductOrderContext::isProductAllowedInCustomerWebshop($product)) {
|
||||
$image = '';
|
||||
if ($product->images->count()) {
|
||||
$image = $product->images->first()->slug;
|
||||
}
|
||||
$cartItem = Yard::instance($this->instance)
|
||||
->add($product->id, $product->getLang('name'), $quantity,
|
||||
$product->getPriceWith(Yard::instance($this->instance)->getUserTaxFree(), false, Yard::instance($this->instance)->getUserCountry()), false, false,
|
||||
['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]);
|
||||
if(Yard::instance($this->instance)->getUserTaxFree()){
|
||||
->add(
|
||||
$product->id,
|
||||
$product->getLang('name'),
|
||||
$quantity,
|
||||
$product->getPriceWith(Yard::instance($this->instance)->getUserTaxFree(), false, Yard::instance($this->instance)->getUserCountry()),
|
||||
false,
|
||||
false,
|
||||
['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]
|
||||
);
|
||||
if (Yard::instance($this->instance)->getUserTaxFree()) {
|
||||
Yard::setTax($cartItem->rowId, 0);
|
||||
}else{
|
||||
} else {
|
||||
Yard::setTax($cartItem->rowId, $product->getTaxWith(Yard::instance($this->instance)->getUserCountry()));
|
||||
}
|
||||
Yard::instance($this->instance)->reCalculateShippingPrice();
|
||||
|
|
@ -50,7 +53,6 @@ class CardController extends Controller
|
|||
}
|
||||
|
||||
return back();
|
||||
|
||||
}
|
||||
|
||||
public function addToCardPost($id)
|
||||
|
|
@ -58,39 +60,45 @@ class CardController extends Controller
|
|||
|
||||
$product = Product::find($id);
|
||||
|
||||
if($product){
|
||||
$image = "";
|
||||
if($product->images->count()){
|
||||
if ($product && ProductOrderContext::isProductAllowedInCustomerWebshop($product)) {
|
||||
$image = '';
|
||||
if ($product->images->count()) {
|
||||
$image = $product->images->first()->slug;
|
||||
}
|
||||
$quantity = Request::get('quantity') ? Request::get('quantity') : 1;
|
||||
$cartItem = Yard::instance($this->instance)
|
||||
->add($product->id, $product->getLang('name'), $quantity,
|
||||
$product->getPriceWith(Yard::instance($this->instance)->getUserTaxFree(), false, Yard::instance($this->instance)->getUserCountry()), false, false,
|
||||
['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]);
|
||||
if(Yard::instance($this->instance)->getUserTaxFree()){
|
||||
->add(
|
||||
$product->id,
|
||||
$product->getLang('name'),
|
||||
$quantity,
|
||||
$product->getPriceWith(Yard::instance($this->instance)->getUserTaxFree(), false, Yard::instance($this->instance)->getUserCountry()),
|
||||
false,
|
||||
false,
|
||||
['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]
|
||||
);
|
||||
if (Yard::instance($this->instance)->getUserTaxFree()) {
|
||||
Yard::setTax($cartItem->rowId, 0);
|
||||
}else{
|
||||
} else {
|
||||
Yard::setTax($cartItem->rowId, $product->getTaxWith(Yard::instance($this->instance)->getUserCountry()));
|
||||
}
|
||||
Yard::instance($this->instance)->reCalculateShippingPrice();
|
||||
|
||||
\Session()->flash('show-card-after-add', true);
|
||||
}
|
||||
|
||||
return back();
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function showCard(){
|
||||
public function showCard()
|
||||
{
|
||||
|
||||
if(Request::get('selected_country')){
|
||||
if (Request::get('selected_country')) {
|
||||
Yard::instance($this->instance)->setShippingCountryWithPrice(Request::get('selected_country'));
|
||||
}else{
|
||||
} else {
|
||||
Yard::instance($this->instance)->reCalculateShippingPrice();
|
||||
}
|
||||
|
||||
//show konflikt wenn user eingeloggt ist und country nicht gesetzt ist
|
||||
// show konflikt wenn user eingeloggt ist und country nicht gesetzt ist
|
||||
$shipping_error = $this->checkShippingError();
|
||||
$data = [
|
||||
'user_shop' => Util::getUserShop(),
|
||||
|
|
@ -98,30 +106,49 @@ class CardController extends Controller
|
|||
'yard_instance' => $this->instance,
|
||||
'shipping_error' => $shipping_error ?? false,
|
||||
];
|
||||
|
||||
return view('web.templates.card', $data);
|
||||
}
|
||||
|
||||
public function updateCard(){
|
||||
public function updateCard()
|
||||
{
|
||||
|
||||
$data = Request::all();
|
||||
if(isset($data['quantity'])){
|
||||
foreach ($data['quantity'] as $rowId => $qty){
|
||||
if (isset($data['quantity'])) {
|
||||
foreach ($data['quantity'] as $rowId => $qty) {
|
||||
$cartItem = Yard::instance($this->instance)->get($rowId);
|
||||
if ($cartItem) {
|
||||
$product = Product::find($cartItem->id);
|
||||
if ($product && $product->is_membership_only) {
|
||||
$qty = 1;
|
||||
}
|
||||
}
|
||||
Yard::instance($this->instance)->update($rowId, $qty);
|
||||
Yard::instance($this->instance)->reCalculateShippingPrice();
|
||||
}
|
||||
}else{
|
||||
$this->deleteCard();
|
||||
} else {
|
||||
$this->deleteCard();
|
||||
}
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function checkoutServer(){
|
||||
|
||||
public function checkoutServer()
|
||||
{
|
||||
foreach (Yard::instance($this->instance)->content() as $row) {
|
||||
$product = Product::find($row->id);
|
||||
if (! $product || ! ProductOrderContext::isProductAllowedInCustomerWebshop($product)) {
|
||||
\Session::flash('alert-error', __('msg.cart_product_not_allowed_for_order_type'));
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
$user_shop = Util::getUserShop();
|
||||
|
||||
|
||||
do {
|
||||
$identifier = Util::getToken();
|
||||
} while( ShoppingInstance::where('identifier', $identifier)->count() );
|
||||
} while (ShoppingInstance::where('identifier', $identifier)->count());
|
||||
|
||||
$data = [];
|
||||
$data['is_from'] = 'shopping';
|
||||
|
|
@ -130,7 +157,7 @@ class CardController extends Controller
|
|||
ShoppingInstance::create([
|
||||
'identifier' => $identifier,
|
||||
'user_shop_id' => $user_shop->id,
|
||||
'payment' => 1, //Customer Shop Payment
|
||||
'payment' => 1, // Customer Shop Payment
|
||||
'subdomain' => url('/'),
|
||||
'country_id' => Yard::instance($this->instance)->getShippingCountryId(),
|
||||
'language' => \App::getLocale(),
|
||||
|
|
@ -138,55 +165,63 @@ class CardController extends Controller
|
|||
'back' => url()->previous(),
|
||||
|
||||
]);
|
||||
|
||||
|
||||
Yard::instance($this->instance)->store($identifier);
|
||||
//add to DB
|
||||
$path = route('checkout.checkout_card', ['identifier'=>$identifier]);
|
||||
if(strpos($path, 'https') === false){
|
||||
// add to DB
|
||||
$path = route('checkout.checkout_card', ['identifier' => $identifier]);
|
||||
if (strpos($path, 'https') === false) {
|
||||
$path = str_replace('http', 'https', $path);
|
||||
}
|
||||
|
||||
return redirect()->secure($path);
|
||||
}
|
||||
|
||||
public function backToShop(){
|
||||
public function backToShop()
|
||||
{
|
||||
$this->deleteCard();
|
||||
return redirect(url('/'));
|
||||
|
||||
return redirect(url('/'));
|
||||
}
|
||||
public function removeCard($rowId){
|
||||
|
||||
public function removeCard($rowId)
|
||||
{
|
||||
|
||||
Yard::instance($this->instance)->remove($rowId);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function deleteCard(){
|
||||
public function deleteCard()
|
||||
{
|
||||
|
||||
$setCode = Shop::getUserShopLang(null, $this->instance);
|
||||
$mylangs = Shop::getLangChange($this->instance);
|
||||
foreach($mylangs as $code => $country){
|
||||
if(strtolower($setCode) === strtolower($code)){
|
||||
foreach ($mylangs as $code => $country) {
|
||||
if (strtolower($setCode) === strtolower($code)) {
|
||||
Shop::initUserShopLang($country, $this->instance);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkShippingError(){
|
||||
private function checkShippingError()
|
||||
{
|
||||
$shipping_error = false;
|
||||
if(\Auth::guard('customers')->check()){
|
||||
if (\Auth::guard('customers')->check()) {
|
||||
$user = \Auth::guard('customers')->user();
|
||||
if($user->shopping_user_id){
|
||||
if ($user->shopping_user_id) {
|
||||
$shopping_user = ShoppingUser::find($user->shopping_user_id);
|
||||
if($shopping_user->same_as_billing){
|
||||
if($shopping_user->billing_country_id != Yard::instance($this->instance)->getUserCountryId()){
|
||||
if ($shopping_user->same_as_billing) {
|
||||
if ($shopping_user->billing_country_id != Yard::instance($this->instance)->getUserCountryId()) {
|
||||
$user_country = Yard::instance($this->instance)->getUserCountry();
|
||||
$user_country_name = $user_country ? $user_country->getLocated() : '';
|
||||
$billing_country = $shopping_user->billing_country;
|
||||
$country_name = $billing_country ? $billing_country->getLocated() : '';
|
||||
$shipping_error = __('website.shipping_error_billing', ['shipping_country' => $user_country_name, 'billing_country' => $country_name]);
|
||||
}
|
||||
}else{
|
||||
if($shopping_user->shipping_country_id != Yard::instance($this->instance)->getUserCountryId()){
|
||||
} else {
|
||||
if ($shopping_user->shipping_country_id != Yard::instance($this->instance)->getUserCountryId()) {
|
||||
$user_country = Yard::instance($this->instance)->getUserCountry();
|
||||
$user_country_name = $user_country ? $user_country->getLocated() : '';
|
||||
$shipping_country = $shopping_user->shipping_country;
|
||||
|
|
@ -194,9 +229,9 @@ class CardController extends Controller
|
|||
$shipping_error = __('website.shipping_error_delivery', ['shipping_country' => $user_country_name, 'billing_country' => $country_name]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return $shipping_error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,26 +2,24 @@
|
|||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
|
||||
use Yard;
|
||||
use Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\IqSite;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductCategory;
|
||||
use App\Services\LocaleGuard;
|
||||
use App\Services\Shop;
|
||||
use App\Services\Util;
|
||||
use App\Models\Product;
|
||||
use App\Models\Category;
|
||||
use App\Models\ProductCategory;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Request;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->setIPInfo();
|
||||
$products = ['aloe-vera-gel-99', 'aloe-vera-saft-500-ml', 'aloe-vera-lippenbalsam'];
|
||||
// $set_products = ['aloe-vera-cleaner-set', 'aloe-vera-koerper-set', 'aloe-vera-repair-set'];
|
||||
$set_products = ['aloe-vera-koerper-set', 'baby-set', 'aloe-vera-gel-set'];
|
||||
$set_products = ['aloe-vera-koerper-set', 'baby-set', 'aloe-vera-gel-set'];
|
||||
$data = [
|
||||
'products' => Product::whereIn('slug', $products)->where('active', true)->whereJsonContains('show_on', '1')->get(),
|
||||
'set_products' => Product::whereIn('slug', $set_products)->where('active', true)->whereJsonContains('show_on', '1')->get(),
|
||||
|
|
@ -34,9 +32,9 @@ class SiteController extends Controller
|
|||
return view('web.index', $data);
|
||||
}
|
||||
|
||||
public function domainCheck()
|
||||
public function domainCheck()
|
||||
{
|
||||
die("checked");
|
||||
exit('checked');
|
||||
}
|
||||
|
||||
public function changeLang()
|
||||
|
|
@ -48,8 +46,12 @@ class SiteController extends Controller
|
|||
if (strtolower($data['change_country_id']) === strtolower($code)) {
|
||||
\Session::put('user_init_country', strtolower($code));
|
||||
\Session::forget('user_init_country_options');
|
||||
\Session::put('locale', strtolower($data['change_locale_id']));
|
||||
$locale = LocaleGuard::normalize($data['change_locale_id'] ?? null);
|
||||
if ($locale !== null) {
|
||||
\Session::put('locale', $locale);
|
||||
}
|
||||
Shop::initUserShopLang($country, 'webshop');
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
|
|
@ -58,40 +60,41 @@ class SiteController extends Controller
|
|||
|
||||
private function setIPinfo()
|
||||
{
|
||||
//wurde schon gesetzt //cache
|
||||
// wurde schon gesetzt //cache
|
||||
$country = strtolower(Shop::getIPDatabaseInfo());
|
||||
if (\Session::has('user_init_country')) {
|
||||
return;
|
||||
}
|
||||
if (config('app.ipinfo')) {
|
||||
$country = strtolower(Shop::getIPDatabaseInfo());
|
||||
if ($country === 'de') { //$locale de - init AT
|
||||
if ($country === 'de') { // $locale de - init AT
|
||||
\Session::put('user_init_country', $country);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($country === 'error') { //$locale at - init AT
|
||||
if ($country === 'error') { // $locale at - init AT
|
||||
$country = 'de';
|
||||
}
|
||||
} else {
|
||||
$country = 'de';
|
||||
}
|
||||
|
||||
//$locale = strtolower(\App::getLocale());
|
||||
//ist default
|
||||
// $locale = strtolower(\App::getLocale());
|
||||
// ist default
|
||||
|
||||
//sprache
|
||||
// sprache
|
||||
if (array_key_exists($country, \App\Services\UserService::getTransChange())) {
|
||||
\Session::put('user_init_country', $country);
|
||||
\Session::put('locale', $country);
|
||||
\App::setLocale($country);
|
||||
} else {
|
||||
//default EN
|
||||
// default EN
|
||||
\Session::put('user_init_country', 'de');
|
||||
\Session::put('locale', 'de');
|
||||
\App::setLocale('de');
|
||||
}
|
||||
|
||||
//bestelland / versandland
|
||||
// bestelland / versandland
|
||||
if (array_key_exists($country, Shop::getLangChange('webshop'))) {
|
||||
\Session::put('user_init_country_options', $country);
|
||||
} else {
|
||||
|
|
@ -119,6 +122,7 @@ class SiteController extends Controller
|
|||
'p_count' => Product::where('active', true)->whereJsonContains('show_on', '1')->count(),
|
||||
'yard_instance' => 'webshop',
|
||||
];
|
||||
|
||||
return view('web.templates.produkte-show', $data);
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +135,7 @@ class SiteController extends Controller
|
|||
$headline_image = $category->iq_image;
|
||||
}
|
||||
|
||||
$product_categories = ProductCategory::where('category_id', $category->id)->whereHas('product', function ($query) use ($category) {
|
||||
$product_categories = ProductCategory::where('category_id', $category->id)->whereHas('product', function ($query) {
|
||||
$query->where('active', true)->whereJsonContains('show_on', '1');
|
||||
})->orderBy('pos', 'DESC')->get();
|
||||
|
||||
|
|
@ -147,7 +151,8 @@ class SiteController extends Controller
|
|||
'headline_image' => $headline_image,
|
||||
'yard_instance' => 'webshop',
|
||||
];
|
||||
return view('web.templates.' . $site, $data);
|
||||
|
||||
return view('web.templates.'.$site, $data);
|
||||
}
|
||||
}
|
||||
dd($subsite);
|
||||
|
|
@ -163,7 +168,8 @@ class SiteController extends Controller
|
|||
'headline_image' => false,
|
||||
'yard_instance' => 'webshop',
|
||||
];
|
||||
return view('web.templates.' . $site, $data);
|
||||
|
||||
return view('web.templates.'.$site, $data);
|
||||
}
|
||||
$data = [
|
||||
'user_shop' => Util::getUserShop(),
|
||||
|
|
@ -171,14 +177,16 @@ class SiteController extends Controller
|
|||
'yard_instance' => 'webshop',
|
||||
];
|
||||
if ($subsite) {
|
||||
if (!view()->exists('web.templates.' . $subsite)) {
|
||||
if (! view()->exists('web.templates.'.$subsite)) {
|
||||
abort(404);
|
||||
}
|
||||
return view('web.templates.' . $subsite, $data);
|
||||
|
||||
return view('web.templates.'.$subsite, $data);
|
||||
}
|
||||
if (!view()->exists('web.templates.' . $site)) {
|
||||
if (! view()->exists('web.templates.'.$site)) {
|
||||
abort(404);
|
||||
}
|
||||
return view('web.templates.' . $site, $data);
|
||||
|
||||
return view('web.templates.'.$site, $data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -605,6 +605,9 @@ class WizardController extends Controller
|
|||
$image = $product->images->first()->slug;
|
||||
}
|
||||
$cartItem = Yard::instance('shopping')->add($product->id, $product->getLang('name'), 1, $product->getPriceWith(\App\Services\UserService::getTaxFree(), false, \App\Services\UserService::$user_country), false, false, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'free_shipping_consultant' => $product->free_shipping_consultant, 'show_on' => $product->show_on]);
|
||||
if ($cartItem->qty > 1) {
|
||||
Yard::instance('shopping')->update($cartItem->rowId, 1);
|
||||
}
|
||||
if (\App\Services\UserService::getTaxFree()) {
|
||||
Yard::setTax($cartItem->rowId, 0);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,29 +2,37 @@
|
|||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Carbon;
|
||||
use App\Services\LocaleGuard;
|
||||
use Closure;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class Localization
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
* Session locale must be validated: arbitrary strings break Symfony/Carbon
|
||||
* (e.g. scanner payloads stored as "locale").
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
|
||||
public function handle($request, Closure $next)
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if (\Session::has('locale')) {
|
||||
\App::setLocale(\Session::get('locale'));
|
||||
// Carbon::setLocale('\Session::get('locale')');
|
||||
//Carbon::setLocale('de');
|
||||
if (! Session::has('locale')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$raw = Session::get('locale');
|
||||
$normalized = LocaleGuard::normalize(is_string($raw) ? $raw : null);
|
||||
|
||||
if ($normalized !== null) {
|
||||
App::setLocale($normalized);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
Session::forget('locale');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
30
app/Models/AboChartSnapshot.php
Normal file
30
app/Models/AboChartSnapshot.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AboChartSnapshot extends Model
|
||||
{
|
||||
protected $table = 'abo_chart_snapshots';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'scope',
|
||||
'year',
|
||||
'month',
|
||||
'count',
|
||||
'calculated_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'int',
|
||||
'year' => 'int',
|
||||
'month' => 'int',
|
||||
'count' => 'int',
|
||||
'calculated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
240
app/Models/Incentive.php
Normal file
240
app/Models/Incentive.php
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Cviebrock\EloquentSluggable\Sluggable;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* Class Incentive
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property array|null $trans_name
|
||||
* @property string|null $subtitle
|
||||
* @property array|null $trans_subtitle
|
||||
* @property string $slug
|
||||
* @property string|null $description
|
||||
* @property array|null $trans_description
|
||||
* @property string|null $image
|
||||
* @property string|null $terms
|
||||
* @property array|null $trans_terms
|
||||
* @property Carbon $qualification_start
|
||||
* @property Carbon $qualification_end
|
||||
* @property Carbon $calculation_end
|
||||
* @property int $points_partner_onetime
|
||||
* @property int $points_abo_onetime
|
||||
* @property int $min_direct_partners
|
||||
* @property int $min_customer_abos
|
||||
* @property int $max_winners
|
||||
* @property int $status
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Carbon|null $deleted_at
|
||||
* @property-read Collection<int, IncentiveParticipant> $participants
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Incentive newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Incentive newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Incentive query()
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Incentive extends Model
|
||||
{
|
||||
use HasFactory, Sluggable, SoftDeletes;
|
||||
|
||||
protected $table = 'incentives';
|
||||
|
||||
protected $casts = [
|
||||
'trans_name' => 'array',
|
||||
'trans_subtitle' => 'array',
|
||||
'trans_description' => 'array',
|
||||
'trans_terms' => 'array',
|
||||
'points_partner_onetime' => 'int',
|
||||
'points_abo_onetime' => 'int',
|
||||
'min_direct_partners' => 'int',
|
||||
'min_customer_abos' => 'int',
|
||||
'max_winners' => 'int',
|
||||
'status' => 'int',
|
||||
'qualification_start' => 'date',
|
||||
'qualification_end' => 'date',
|
||||
'calculation_end' => 'date',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'trans_name',
|
||||
'subtitle',
|
||||
'trans_subtitle',
|
||||
'description',
|
||||
'trans_description',
|
||||
'image',
|
||||
'terms',
|
||||
'trans_terms',
|
||||
'qualification_start',
|
||||
'qualification_end',
|
||||
'calculation_end',
|
||||
'points_partner_onetime',
|
||||
'points_abo_onetime',
|
||||
'min_direct_partners',
|
||||
'min_customer_abos',
|
||||
'max_winners',
|
||||
'status',
|
||||
];
|
||||
|
||||
public static $statusTypes = [
|
||||
0 => 'draft',
|
||||
1 => 'active',
|
||||
2 => 'closed',
|
||||
];
|
||||
|
||||
public static $statusColors = [
|
||||
0 => 'warning',
|
||||
1 => 'success',
|
||||
2 => 'secondary',
|
||||
];
|
||||
|
||||
public function sluggable(): array
|
||||
{
|
||||
return [
|
||||
'slug' => [
|
||||
'source' => 'name',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Relationships
|
||||
|
||||
public function participants()
|
||||
{
|
||||
return $this->hasMany(IncentiveParticipant::class);
|
||||
}
|
||||
|
||||
// Scopes
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 1);
|
||||
}
|
||||
|
||||
public function scopeInQualificationPeriod($query, ?Carbon $date = null)
|
||||
{
|
||||
$date = $date ?: Carbon::now();
|
||||
|
||||
return $query->where('qualification_start', '<=', $date)
|
||||
->where('qualification_end', '>=', $date);
|
||||
}
|
||||
|
||||
public function scopeInCalculationPeriod($query, ?Carbon $date = null)
|
||||
{
|
||||
$date = $date ?: Carbon::now();
|
||||
|
||||
return $query->where('qualification_start', '<=', $date)
|
||||
->where('calculation_end', '>=', $date);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 1;
|
||||
}
|
||||
|
||||
public function isDraft(): bool
|
||||
{
|
||||
return $this->status === 0;
|
||||
}
|
||||
|
||||
public function isClosed(): bool
|
||||
{
|
||||
return $this->status === 2;
|
||||
}
|
||||
|
||||
public function isInQualificationPeriod(?Carbon $date = null): bool
|
||||
{
|
||||
$date = $date ?: Carbon::now();
|
||||
|
||||
return $date->between($this->qualification_start, $this->qualification_end);
|
||||
}
|
||||
|
||||
public function isInCalculationPeriod(?Carbon $date = null): bool
|
||||
{
|
||||
$date = $date ?: Carbon::now();
|
||||
|
||||
return $date->between($this->qualification_start, $this->calculation_end);
|
||||
}
|
||||
|
||||
public function isDateInScope(int $month, int $year): bool
|
||||
{
|
||||
$date = Carbon::createFromDate($year, $month, 1);
|
||||
|
||||
return $date->between(
|
||||
$this->qualification_start->copy()->startOfMonth(),
|
||||
$this->calculation_end->copy()->endOfMonth()
|
||||
);
|
||||
}
|
||||
|
||||
public function getStatusType(): string
|
||||
{
|
||||
return isset(self::$statusTypes[$this->status]) ? __('incentive.status_'.self::$statusTypes[$this->status]) : '';
|
||||
}
|
||||
|
||||
public function getStatusColor(): string
|
||||
{
|
||||
return self::$statusColors[$this->status] ?? 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific translation for a field and locale.
|
||||
*/
|
||||
public function getTrans(string $key, string $lang): string
|
||||
{
|
||||
$transKey = 'trans_'.$key;
|
||||
if (! empty($this->{$transKey}[$lang])) {
|
||||
return $this->{$transKey}[$lang];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translated value for the current locale, falling back to German (default).
|
||||
*/
|
||||
public function getLang(string $key): string
|
||||
{
|
||||
$lang = \App::getLocale();
|
||||
if ($lang === 'de') {
|
||||
return (string) ($this->{$key} ?? '');
|
||||
}
|
||||
$trans = $this->getTrans($key, $lang);
|
||||
if ($trans !== '') {
|
||||
return $trans;
|
||||
}
|
||||
|
||||
return (string) ($this->{$key} ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{month: int, year: int}>
|
||||
*/
|
||||
public function getCalculationMonths(): array
|
||||
{
|
||||
$months = [];
|
||||
$current = $this->qualification_start->copy()->startOfMonth();
|
||||
$end = $this->calculation_end->copy()->startOfMonth();
|
||||
|
||||
while ($current->lte($end)) {
|
||||
$months[] = [
|
||||
'month' => $current->month,
|
||||
'year' => $current->year,
|
||||
];
|
||||
$current->addMonth();
|
||||
}
|
||||
|
||||
return $months;
|
||||
}
|
||||
}
|
||||
56
app/Models/IncentiveNewAbo.php
Normal file
56
app/Models/IncentiveNewAbo.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class IncentiveNewAbo
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $participant_id
|
||||
* @property int $user_abo_id
|
||||
* @property Carbon $activated_at
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read IncentiveParticipant $participant
|
||||
* @property-read UserAbo $userAbo
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, IncentivePointsLog> $pointsLogs
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class IncentiveNewAbo extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'incentive_new_abos';
|
||||
|
||||
protected $casts = [
|
||||
'participant_id' => 'int',
|
||||
'user_abo_id' => 'int',
|
||||
'activated_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'participant_id',
|
||||
'user_abo_id',
|
||||
'activated_at',
|
||||
];
|
||||
|
||||
public function participant()
|
||||
{
|
||||
return $this->belongsTo(IncentiveParticipant::class, 'participant_id');
|
||||
}
|
||||
|
||||
public function userAbo()
|
||||
{
|
||||
return $this->belongsTo(UserAbo::class, 'user_abo_id');
|
||||
}
|
||||
|
||||
public function pointsLogs()
|
||||
{
|
||||
return $this->hasMany(IncentivePointsLog::class, 'incentive_new_abo_id');
|
||||
}
|
||||
}
|
||||
57
app/Models/IncentiveNewPartner.php
Normal file
57
app/Models/IncentiveNewPartner.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class IncentiveNewPartner
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $participant_id
|
||||
* @property int $user_id
|
||||
* @property Carbon $registered_at
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read IncentiveParticipant $participant
|
||||
* @property-read User $user
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, IncentivePointsLog> $pointsLogs
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class IncentiveNewPartner extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'incentive_new_partners';
|
||||
|
||||
protected $casts = [
|
||||
'participant_id' => 'int',
|
||||
'user_id' => 'int',
|
||||
'registered_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'participant_id',
|
||||
'user_id',
|
||||
'registered_at',
|
||||
];
|
||||
|
||||
public function participant()
|
||||
{
|
||||
return $this->belongsTo(IncentiveParticipant::class, 'participant_id');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function pointsLogs()
|
||||
{
|
||||
return $this->hasMany(IncentivePointsLog::class, 'incentive_new_partner_id');
|
||||
}
|
||||
}
|
||||
536
app/Models/IncentiveParticipant.php
Normal file
536
app/Models/IncentiveParticipant.php
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class IncentiveParticipant
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $incentive_id
|
||||
* @property int $user_id
|
||||
* @property Carbon|null $accepted_terms_at
|
||||
* @property int $total_points
|
||||
* @property int $qualified_partners
|
||||
* @property int $qualified_abos
|
||||
* @property bool $is_qualified
|
||||
* @property int|null $rank
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read Incentive $incentive
|
||||
* @property-read User $user
|
||||
* @property-read Collection<int, IncentivePointsLog> $pointsLog
|
||||
* @property-read Collection<int, IncentiveNewPartner> $newPartners
|
||||
* @property-read Collection<int, IncentiveNewAbo> $newAbos
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentiveParticipant newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentiveParticipant newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentiveParticipant orderByIncentiveLeaderboard()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentiveParticipant orderByRankNullsLast()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentiveParticipant query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentiveParticipant withRankingActivity()
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class IncentiveParticipant extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'incentive_participants';
|
||||
|
||||
protected $casts = [
|
||||
'incentive_id' => 'int',
|
||||
'user_id' => 'int',
|
||||
'total_points' => 'int',
|
||||
'qualified_partners' => 'int',
|
||||
'qualified_abos' => 'int',
|
||||
'is_qualified' => 'bool',
|
||||
'rank' => 'int',
|
||||
'accepted_terms_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'incentive_id',
|
||||
'user_id',
|
||||
'accepted_terms_at',
|
||||
'total_points',
|
||||
'qualified_partners',
|
||||
'qualified_abos',
|
||||
'is_qualified',
|
||||
'rank',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
|
||||
public function incentive()
|
||||
{
|
||||
return $this->belongsTo(Incentive::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function pointsLog()
|
||||
{
|
||||
return $this->hasMany(IncentivePointsLog::class, 'participant_id');
|
||||
}
|
||||
|
||||
public function newPartners()
|
||||
{
|
||||
return $this->hasMany(IncentiveNewPartner::class, 'participant_id');
|
||||
}
|
||||
|
||||
public function newAbos()
|
||||
{
|
||||
return $this->hasMany(IncentiveNewAbo::class, 'participant_id');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
|
||||
public function scopeQualified($query)
|
||||
{
|
||||
return $query->where('is_qualified', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teilnehmer mit nachweisbarer Aktivität: mindestens ein qualifizierter Partner, ein Kunden-Abo oder
|
||||
* Gesamtpunkte größer null. Reine Nullstände erscheinen in der User-Rangliste nicht.
|
||||
*/
|
||||
public function scopeWithRankingActivity(Builder $query): Builder
|
||||
{
|
||||
$model = $query->getModel();
|
||||
$qualifiedPartners = $model->qualifyColumn('qualified_partners');
|
||||
$qualifiedAbos = $model->qualifyColumn('qualified_abos');
|
||||
$totalPoints = $model->qualifyColumn('total_points');
|
||||
|
||||
return $query->where(function (Builder $q) use ($qualifiedPartners, $qualifiedAbos, $totalPoints) {
|
||||
$q->where($qualifiedPartners, '>', 0)
|
||||
->orWhere($qualifiedAbos, '>', 0)
|
||||
->orWhere($totalPoints, '>', 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaderboard: qualifizierte Teilnehmer (Mindest-Partner/Abos) oben, unabhängig von den Punkten;
|
||||
* darunter nach Rang (1, 2, …), ohne Rang zuletzt; gleiche Stufe nach Gesamtpunkten absteigend.
|
||||
* Bei Punktgleichstand: Teilnehmer mit bestätigter Teilnahme (Klarnamen) vor anonymen.
|
||||
*/
|
||||
public function scopeOrderByIncentiveLeaderboard(Builder $query): Builder
|
||||
{
|
||||
$model = $query->getModel();
|
||||
$grammar = $model->getConnection()->getQueryGrammar();
|
||||
$qualifiedRank = $grammar->wrap(
|
||||
$model->qualifyColumn('rank')
|
||||
);
|
||||
$acceptedTerms = $grammar->wrap(
|
||||
$model->qualifyColumn('accepted_terms_at')
|
||||
);
|
||||
|
||||
return $query
|
||||
->orderBy($model->qualifyColumn('is_qualified'), 'desc')
|
||||
->orderByRaw($qualifiedRank.' IS NULL')
|
||||
->orderBy($model->qualifyColumn('rank'), 'asc')
|
||||
->orderBy($model->qualifyColumn('total_points'), 'desc')
|
||||
->orderByRaw($acceptedTerms.' IS NOT NULL DESC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortierung nach Rang (1, 2, …); Teilnehmer ohne gesetzten Rang stehen unten.
|
||||
*/
|
||||
public function scopeOrderByRankNullsLast(Builder $query): Builder
|
||||
{
|
||||
$model = $query->getModel();
|
||||
$qualifiedRank = $model->getConnection()->getQueryGrammar()->wrap(
|
||||
$model->qualifyColumn('rank')
|
||||
);
|
||||
|
||||
return $query
|
||||
->orderByRaw($qualifiedRank.' IS NULL')
|
||||
->orderBy($model->qualifyColumn('rank'), 'asc');
|
||||
}
|
||||
|
||||
public function scopeWinners($query, int $maxWinners)
|
||||
{
|
||||
return $query->qualified()
|
||||
->whereNotNull('rank')
|
||||
->where('rank', '<=', $maxWinners)
|
||||
->orderBy('rank');
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
public function hasAcceptedTerms(): bool
|
||||
{
|
||||
return $this->accepted_terms_at !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Teilnehmerzeile fuer einen Berater anlegen, falls noch nicht vorhanden (ohne Teilnahme-Bestaetigung).
|
||||
*/
|
||||
public static function ensureForIncentiveUser(Incentive $incentive, int $userId): self
|
||||
{
|
||||
return self::firstOrCreate(
|
||||
[
|
||||
'incentive_id' => $incentive->id,
|
||||
'user_id' => $userId,
|
||||
],
|
||||
[
|
||||
'accepted_terms_at' => null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Berater (User mit m_level) als Teilnehmer anlegen, damit Punkte im Qualifikationszeitraum ohne Checkbox mitlaufen.
|
||||
*
|
||||
* @return int Anzahl neu angelegter Zeilen
|
||||
*/
|
||||
public static function ensureConsultantsForIncentive(Incentive $incentive): int
|
||||
{
|
||||
$added = 0;
|
||||
|
||||
User::query()
|
||||
->where('id', '!=', 1)
|
||||
->whereNull('deleted_at')
|
||||
->where('admin', '<', 4)
|
||||
->whereNotNull('m_level')
|
||||
->whereNotExists(function ($q) use ($incentive) {
|
||||
$q->selectRaw('1')
|
||||
->from('incentive_participants')
|
||||
->whereColumn('incentive_participants.user_id', 'users.id')
|
||||
->where('incentive_participants.incentive_id', $incentive->id);
|
||||
})
|
||||
->orderBy('id')
|
||||
->chunkById(500, function ($users) use ($incentive, &$added) {
|
||||
foreach ($users as $user) {
|
||||
self::create([
|
||||
'incentive_id' => $incentive->id,
|
||||
'user_id' => $user->id,
|
||||
'accepted_terms_at' => null,
|
||||
]);
|
||||
$added++;
|
||||
}
|
||||
});
|
||||
|
||||
return $added;
|
||||
}
|
||||
|
||||
public function checkQualification(): bool
|
||||
{
|
||||
$incentive = $this->incentive;
|
||||
|
||||
$this->is_qualified = $this->qualified_partners >= $incentive->min_direct_partners
|
||||
&& $this->qualified_abos >= $incentive->min_customer_abos;
|
||||
|
||||
return $this->is_qualified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnung aus Tracking-Tabellen und Points-Log.
|
||||
* Zaehlt Partner/Abos aus eigenen Tabellen, Punkte aus Log.
|
||||
*/
|
||||
public function recalculateFromTrackingTables(): self
|
||||
{
|
||||
$this->qualified_partners = $this->newPartners()->count();
|
||||
$this->qualified_abos = $this->newAbos()->count();
|
||||
|
||||
$this->total_points = (int) $this->pointsLog()
|
||||
->selectRaw('COALESCE(SUM(points_onetime + points_accumulated), 0) as total')
|
||||
->value('total');
|
||||
|
||||
$this->checkQualification();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kompletter Neuaufbau aus Quelldaten (Users, UserAbos, UserSalesVolumes).
|
||||
* Loescht Tracking-Tabellen + Log und baut alles neu auf.
|
||||
* Nur fuer Batch/Cron/Force-Rebuild.
|
||||
*/
|
||||
public function rebuildFromSourceTables(): self
|
||||
{
|
||||
$incentive = $this->incentive;
|
||||
|
||||
// Tracking-Tabellen + Log leeren
|
||||
$this->newPartners()->delete();
|
||||
$this->newAbos()->delete();
|
||||
$this->pointsLog()->delete();
|
||||
|
||||
// A. Neupartner: direkt gesponserte User im Qualifikationszeitraum mit bezahltem Starterpaket
|
||||
$new_partners = User::where('m_sponsor', $this->user_id)
|
||||
->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$incentive->qualification_end->copy()->endOfDay(),
|
||||
])
|
||||
->whereHas('shopping_orders', function ($q) {
|
||||
$q->wherePaidRegistrationIncludesStarterKit();
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($new_partners as $partner) {
|
||||
$newPartner = IncentiveNewPartner::create([
|
||||
'participant_id' => $this->id,
|
||||
'user_id' => $partner->id,
|
||||
'registered_at' => $partner->created_at,
|
||||
]);
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $this->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,
|
||||
]);
|
||||
}
|
||||
|
||||
// B. Kundenabos (ot) + Berater-Eigenabos (me): status=2
|
||||
$qualStart = $incentive->qualification_start->copy()->startOfDay();
|
||||
$qualEnd = $incentive->qualification_end->copy()->endOfDay();
|
||||
|
||||
// Kundenabos: Berater steht in member_id (nicht user_id)
|
||||
$customerAbosInPeriod = UserAbo::where('member_id', $this->user_id)
|
||||
->where('is_for', 'ot')
|
||||
->where('status', 2)
|
||||
->whereBetween('created_at', [$qualStart, $qualEnd])
|
||||
->get();
|
||||
|
||||
// Eigenabo: im Qualifikationszeitraum neu abgeschlossen
|
||||
$ownAbosInPeriod = UserAbo::where('user_id', $this->user_id)
|
||||
->where('is_for', 'me')
|
||||
->where('status', 2)
|
||||
->whereBetween('created_at', [$qualStart, $qualEnd])
|
||||
->get();
|
||||
|
||||
// Eigenabo: bereits vor Qualifikationsbeginn aktiv → einmalig mit Wirkung ab Qualifikationsstart
|
||||
$ownAbosPreExisting = UserAbo::where('user_id', $this->user_id)
|
||||
->where('is_for', 'me')
|
||||
->where('status', 2)
|
||||
->where('created_at', '<', $qualStart)
|
||||
->get();
|
||||
|
||||
foreach ($customerAbosInPeriod->concat($ownAbosInPeriod)->concat($ownAbosPreExisting) as $abo) {
|
||||
$activatedAt = $abo->created_at;
|
||||
$logMonth = (int) $abo->created_at->month;
|
||||
$logYear = (int) $abo->created_at->year;
|
||||
|
||||
if ($abo->is_for === 'me' && $abo->created_at->lt($qualStart)) {
|
||||
$activatedAt = $qualStart->copy();
|
||||
$logMonth = (int) $qualStart->month;
|
||||
$logYear = (int) $qualStart->year;
|
||||
}
|
||||
|
||||
$newAbo = IncentiveNewAbo::create([
|
||||
'participant_id' => $this->id,
|
||||
'user_abo_id' => $abo->id,
|
||||
'activated_at' => $activatedAt,
|
||||
]);
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $this->id,
|
||||
'type' => 'abo',
|
||||
'source_type' => UserAbo::class,
|
||||
'source_id' => $abo->id,
|
||||
'source_label' => $abo->email ?: ('Abo #'.$abo->id),
|
||||
'month' => $logMonth,
|
||||
'year' => $logYear,
|
||||
'points_onetime' => $incentive->points_abo_onetime,
|
||||
'points_accumulated' => 0,
|
||||
'incentive_new_abo_id' => $newAbo->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// C. Akkumulierte Punkte NUR von Neupartnern und Neuabos
|
||||
$calculation_months = $incentive->getCalculationMonths();
|
||||
$new_partner_user_ids = $this->newPartners()->pluck('user_id')->toArray();
|
||||
$abo_shopping_user_ids = UserAbo::whereIn('id', $this->newAbos()->pluck('user_abo_id'))
|
||||
->whereNotNull('shopping_user_id')
|
||||
->pluck('shopping_user_id')
|
||||
->toArray();
|
||||
|
||||
$newPartnerByUserId = $this->newPartners()->get()->keyBy('user_id');
|
||||
$newAboByUserAboId = $this->newAbos()->get()->keyBy('user_abo_id');
|
||||
|
||||
foreach ($calculation_months as $period) {
|
||||
// C1. Neupartner-Umsaetze: Sales Volumes der Neupartner selbst
|
||||
if (! empty($new_partner_user_ids)) {
|
||||
$partner_svs = UserSalesVolume::whereIn('user_id', $new_partner_user_ids)
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->where('status', '!=', 6)
|
||||
->get();
|
||||
|
||||
foreach ($partner_svs as $sv) {
|
||||
$points = (int) abs($sv->points ?? 0);
|
||||
if ($points <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $this->id,
|
||||
'type' => 'partner',
|
||||
'source_type' => UserSalesVolume::class,
|
||||
'source_id' => $sv->id,
|
||||
'source_label' => $sv->message ?? ('SV '.$period['month'].'/'.$period['year']),
|
||||
'month' => $period['month'],
|
||||
'year' => $period['year'],
|
||||
'points_onetime' => 0,
|
||||
'points_accumulated' => $points,
|
||||
'user_sales_volume_id' => $sv->id,
|
||||
'incentive_new_partner_id' => $newPartnerByUserId->get($sv->user_id)?->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Stornos von Neupartnern
|
||||
$partner_stornos = UserSalesVolume::whereIn('user_id', $new_partner_user_ids)
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->where('status', 6)
|
||||
->get();
|
||||
|
||||
foreach ($partner_stornos as $storno_sv) {
|
||||
$points = (int) abs($storno_sv->points ?? 0);
|
||||
if ($points <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $this->id,
|
||||
'type' => 'partner',
|
||||
'source_type' => UserSalesVolume::class,
|
||||
'source_id' => $storno_sv->id,
|
||||
'source_label' => 'Storno: '.($storno_sv->message ?? 'SV #'.$storno_sv->id),
|
||||
'month' => $period['month'],
|
||||
'year' => $period['year'],
|
||||
'points_onetime' => 0,
|
||||
'points_accumulated' => -$points,
|
||||
'is_storno' => true,
|
||||
'user_sales_volume_id' => $storno_sv->id,
|
||||
'incentive_new_partner_id' => $newPartnerByUserId->get($storno_sv->user_id)?->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// C2. Neuabo-Umsaetze: Sales Volumes von Bestellungen der Abo-Kunden
|
||||
if (! empty($abo_shopping_user_ids)) {
|
||||
$abo_svs = UserSalesVolume::where('user_id', $this->user_id)
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->where('status', '!=', 6)
|
||||
->whereHas('shopping_order', fn ($q) => $q->whereIn('shopping_user_id', $abo_shopping_user_ids))
|
||||
->get();
|
||||
|
||||
foreach ($abo_svs as $sv) {
|
||||
$points = (int) abs($sv->points ?? 0);
|
||||
if ($points <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$incentiveNewAboId = null;
|
||||
if ($sv->shopping_order_id) {
|
||||
$userAboId = UserAboOrder::where('shopping_order_id', $sv->shopping_order_id)->value('user_abo_id');
|
||||
if ($userAboId) {
|
||||
$incentiveNewAboId = $newAboByUserAboId->get($userAboId)?->id;
|
||||
}
|
||||
}
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $this->id,
|
||||
'type' => 'abo',
|
||||
'source_type' => UserSalesVolume::class,
|
||||
'source_id' => $sv->id,
|
||||
'source_label' => $sv->message ?? ('SV '.$period['month'].'/'.$period['year']),
|
||||
'month' => $period['month'],
|
||||
'year' => $period['year'],
|
||||
'points_onetime' => 0,
|
||||
'points_accumulated' => $points,
|
||||
'user_sales_volume_id' => $sv->id,
|
||||
'incentive_new_abo_id' => $incentiveNewAboId,
|
||||
]);
|
||||
}
|
||||
|
||||
// Stornos von Abo-Kunden
|
||||
$abo_stornos = UserSalesVolume::where('user_id', $this->user_id)
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->where('status', 6)
|
||||
->whereHas('shopping_order', fn ($q) => $q->whereIn('shopping_user_id', $abo_shopping_user_ids))
|
||||
->get();
|
||||
|
||||
foreach ($abo_stornos as $storno_sv) {
|
||||
$points = (int) abs($storno_sv->points ?? 0);
|
||||
if ($points <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$incentiveNewAboId = null;
|
||||
if ($storno_sv->shopping_order_id) {
|
||||
$userAboId = UserAboOrder::where('shopping_order_id', $storno_sv->shopping_order_id)->value('user_abo_id');
|
||||
if ($userAboId) {
|
||||
$incentiveNewAboId = $newAboByUserAboId->get($userAboId)?->id;
|
||||
}
|
||||
}
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $this->id,
|
||||
'type' => 'abo',
|
||||
'source_type' => UserSalesVolume::class,
|
||||
'source_id' => $storno_sv->id,
|
||||
'source_label' => 'Storno: '.($storno_sv->message ?? 'SV #'.$storno_sv->id),
|
||||
'month' => $period['month'],
|
||||
'year' => $period['year'],
|
||||
'points_onetime' => 0,
|
||||
'points_accumulated' => -$points,
|
||||
'is_storno' => true,
|
||||
'user_sales_volume_id' => $storno_sv->id,
|
||||
'incentive_new_abo_id' => $incentiveNewAboId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Totals aus den neu aufgebauten Tracking-Tabellen berechnen
|
||||
return $this->recalculateFromTrackingTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Verwende recalculateFromTrackingTables() stattdessen
|
||||
*/
|
||||
public function recalculatePoints(): int
|
||||
{
|
||||
$this->recalculateFromTrackingTables();
|
||||
|
||||
return $this->total_points;
|
||||
}
|
||||
|
||||
public function isWinner(): bool
|
||||
{
|
||||
if (! $this->is_qualified || $this->rank === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->rank <= $this->incentive->max_winners;
|
||||
}
|
||||
|
||||
private static function determineLogType(UserSalesVolume $usv): string
|
||||
{
|
||||
if ($usv->status_turnover == 2 || $usv->status_points == 2) {
|
||||
return 'abo';
|
||||
}
|
||||
|
||||
return 'partner';
|
||||
}
|
||||
}
|
||||
148
app/Models/IncentivePointsLog.php
Normal file
148
app/Models/IncentivePointsLog.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class IncentivePointsLog
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $participant_id
|
||||
* @property string $type
|
||||
* @property string $source_type
|
||||
* @property int $source_id
|
||||
* @property string $source_label
|
||||
* @property int $month
|
||||
* @property int $year
|
||||
* @property int $points_onetime
|
||||
* @property int $points_accumulated
|
||||
* @property bool $is_storno
|
||||
* @property int|null $storno_of_id
|
||||
* @property int|null $user_sales_volume_id
|
||||
* @property int|null $incentive_new_partner_id
|
||||
* @property int|null $incentive_new_abo_id
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read IncentiveParticipant $participant
|
||||
* @property-read UserSalesVolume|null $salesVolume
|
||||
* @property-read IncentiveNewPartner|null $incentiveNewPartner
|
||||
* @property-read IncentiveNewAbo|null $incentiveNewAbo
|
||||
* @property-read IncentivePointsLog|null $stornoOf
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentivePointsLog newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentivePointsLog newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|IncentivePointsLog query()
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class IncentivePointsLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'incentive_points_log';
|
||||
|
||||
protected $casts = [
|
||||
'participant_id' => 'int',
|
||||
'source_id' => 'int',
|
||||
'month' => 'int',
|
||||
'year' => 'int',
|
||||
'points_onetime' => 'int',
|
||||
'points_accumulated' => 'int',
|
||||
'is_storno' => 'bool',
|
||||
'storno_of_id' => 'int',
|
||||
'user_sales_volume_id' => 'int',
|
||||
'incentive_new_partner_id' => 'int',
|
||||
'incentive_new_abo_id' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'participant_id',
|
||||
'type',
|
||||
'source_type',
|
||||
'source_id',
|
||||
'source_label',
|
||||
'month',
|
||||
'year',
|
||||
'points_onetime',
|
||||
'points_accumulated',
|
||||
'is_storno',
|
||||
'storno_of_id',
|
||||
'user_sales_volume_id',
|
||||
'incentive_new_partner_id',
|
||||
'incentive_new_abo_id',
|
||||
];
|
||||
|
||||
public static $types = [
|
||||
'partner' => 'partner',
|
||||
'abo' => 'abo',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
|
||||
public function participant()
|
||||
{
|
||||
return $this->belongsTo(IncentiveParticipant::class, 'participant_id');
|
||||
}
|
||||
|
||||
public function salesVolume()
|
||||
{
|
||||
return $this->belongsTo(UserSalesVolume::class, 'user_sales_volume_id');
|
||||
}
|
||||
|
||||
public function incentiveNewPartner()
|
||||
{
|
||||
return $this->belongsTo(IncentiveNewPartner::class, 'incentive_new_partner_id');
|
||||
}
|
||||
|
||||
public function incentiveNewAbo()
|
||||
{
|
||||
return $this->belongsTo(IncentiveNewAbo::class, 'incentive_new_abo_id');
|
||||
}
|
||||
|
||||
public function stornoOf()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'storno_of_id');
|
||||
}
|
||||
|
||||
public function stornoEntries()
|
||||
{
|
||||
return $this->hasMany(self::class, 'storno_of_id');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
|
||||
public function scopePartner($query)
|
||||
{
|
||||
return $query->where('type', 'partner');
|
||||
}
|
||||
|
||||
public function scopeAbo($query)
|
||||
{
|
||||
return $query->where('type', 'abo');
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_storno', false);
|
||||
}
|
||||
|
||||
public function scopeForMonth($query, int $month, int $year)
|
||||
{
|
||||
return $query->where('month', $month)->where('year', $year);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
public function getTotalPoints(): int
|
||||
{
|
||||
return $this->points_onetime + $this->points_accumulated;
|
||||
}
|
||||
|
||||
public function getFormattedMonthYear(): string
|
||||
{
|
||||
return str_pad($this->month, 2, '0', STR_PAD_LEFT).'/'.$this->year;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
|
|
@ -339,6 +340,36 @@ class ShoppingOrder extends Model
|
|||
return $this->hasMany('App\Models\ShoppingOrderItem', 'shopping_order_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bezahlte Berater-Registrierung (payment_for = 1) mit mindestens einem Produkt, das keine
|
||||
* reine Mitgliedschaft ohne Starterpaket ist ({@see Product::$is_membership_only}).
|
||||
*/
|
||||
public function scopeWherePaidRegistrationIncludesStarterKit(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->where('payment_for', 1)
|
||||
->where(function (Builder $q) {
|
||||
$q->where('paid', true)
|
||||
->orWhereIn('txaction', ['paid', 'extern_paid']);
|
||||
})
|
||||
->whereHas('shopping_order_items', function (Builder $q) {
|
||||
$q->whereHas('product', function (Builder $p) {
|
||||
$p->where('is_membership_only', false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Erfüllt diese Bestellung die Voraussetzung für Incentive-Neupartner-Tracking (Starterpaket)?
|
||||
*/
|
||||
public function qualifiesForIncentiveTrackedPartner(): bool
|
||||
{
|
||||
return static::query()
|
||||
->whereKey($this->getKey())
|
||||
->wherePaidRegistrationIncludesStarterKit()
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function shopping_payments()
|
||||
{
|
||||
return $this->hasMany('App\Models\ShoppingPayment', 'shopping_order_id');
|
||||
|
|
|
|||
|
|
@ -256,11 +256,23 @@ class UserAbo extends Model
|
|||
return $this->attributes['cancel_date'] ? Carbon::parse($this->attributes['cancel_date'])->format(\Util::formatDateDB()) : '';
|
||||
}
|
||||
|
||||
public function getFormattedAmount()
|
||||
public function getFormattedAmount(): string
|
||||
{
|
||||
return isset($this->attributes['amount']) ? Util::formatNumber($this->attributes['amount'] / 100) : '';
|
||||
}
|
||||
|
||||
public function getTotalPoints(): float
|
||||
{
|
||||
return $this->user_abo_items
|
||||
->where('comp', 0)
|
||||
->sum(fn ($item) => ($item->product?->points ?? 0) * $item->qty);
|
||||
}
|
||||
|
||||
public function getFormattedTotalPoints(): string
|
||||
{
|
||||
return Util::formatNumber($this->getTotalPoints());
|
||||
}
|
||||
|
||||
public function getIsForFormated()
|
||||
{
|
||||
return $this->attributes['is_for'] === 'me' ? '<span class="badge badge-outline-warning-dark">'.__('tables.adviser').'</span>' : '<span class="badge badge-outline-info">'.__('tables.customer').'</span>';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ use App\Services\AboHelper;
|
|||
|
||||
class AboRepository extends BaseRepository
|
||||
{
|
||||
private const LOCK_DAYS_CHANGE = 10;
|
||||
|
||||
private const LOCK_DAYS_PAUSE_CANCEL = 3;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// $this->model = $model;
|
||||
|
|
@ -24,9 +28,12 @@ class AboRepository extends BaseRepository
|
|||
if ($this->validate($data)) {
|
||||
$this->updateStatus($data);
|
||||
$this->model->abo_interval = $data['abo_interval'];
|
||||
$this->model->next_date = AboHelper::setNextDate(now(), $data['abo_interval']);
|
||||
$nextDate = $this->calculateNewNextDate($data['abo_interval']);
|
||||
$this->model->next_date = $nextDate;
|
||||
$this->model->save();
|
||||
\Session()->flash('alert-success', 'Einstellungen gespeichert');
|
||||
|
||||
$daysUntilNext = AboHelper::calendarDaysUntil(now(), $nextDate);
|
||||
\Session()->flash('alert-warning', __('abo.warning_next_date_info', ['days' => $daysUntilNext, 'date' => $nextDate->format('d.m.Y')]));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -44,6 +51,16 @@ class AboRepository extends BaseRepository
|
|||
{
|
||||
// Handle cancellation
|
||||
if (isset($data['abo_cancel']) && $data['abo_cancel'] == 'true') {
|
||||
// Sperre: 3 Tage vor Ausführung kann nicht mehr pausiert/gekündigt werden
|
||||
if ($this->model->next_date) {
|
||||
$daysUntil = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
if ($daysUntil >= 0 && $daysUntil < self::LOCK_DAYS_PAUSE_CANCEL) {
|
||||
\Session()->flash('alert-error', __('abo.error_cancel_locked', ['days' => $daysUntil]));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Status 4 = abo_cancel (storniert/gekündigt)
|
||||
$this->model->status = 4;
|
||||
$this->model->active = false;
|
||||
|
|
@ -54,6 +71,15 @@ class AboRepository extends BaseRepository
|
|||
}
|
||||
|
||||
$active = (isset($data['abo_is_active']) && $data['abo_is_active']) ? true : false;
|
||||
// Sperre: 3 Tage vor Ausführung kann nicht mehr pausiert werden
|
||||
if ($this->model->active && ! $active && $this->model->next_date) {
|
||||
$daysUntil = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
if ($daysUntil >= 0 && $daysUntil < self::LOCK_DAYS_PAUSE_CANCEL) {
|
||||
\Session()->flash('alert-error', __('abo.error_pause_locked', ['days' => $daysUntil]));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
// if status is active and active is false, set status to inactive
|
||||
if ($this->model->active && ! $active) {
|
||||
if ($this->model->status == 2) { // okay
|
||||
|
|
@ -63,7 +89,7 @@ class AboRepository extends BaseRepository
|
|||
}
|
||||
}
|
||||
if (! $this->model->active && $active) {
|
||||
if ($this->model->status = 6) { // inactive
|
||||
if ($this->model->status == 6) { // inactive
|
||||
$this->model->status = 2; // okay
|
||||
$this->model->active = true;
|
||||
$this->model->save();
|
||||
|
|
@ -97,23 +123,51 @@ class AboRepository extends BaseRepository
|
|||
}
|
||||
}
|
||||
if (! in_array($data['abo_interval'], \App\Models\UserAbo::$aboDeliveryDays)) {
|
||||
// to check if user is not admin
|
||||
\Session()->flash('alert-error', __('abo.error_abo_interval'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfung: Wenn das Abo diesen Monat noch nicht ausgeführt wurde (oder noch nie),
|
||||
// darf das Abo-Intervall nicht auf einen Tag gesetzt werden, der bereits vergangen ist (oder heute ist),
|
||||
// da setNextDate das nächste Ausführungsdatum sonst auf den nächsten Monat setzt und dieser Monat übersprungen wird.
|
||||
$executedThisMonth = $this->model->last_date && \Carbon\Carbon::parse($this->model->last_date)->isCurrentMonth();
|
||||
// Sperre: 10 Tage vor nächster Ausführung keine Änderungen mehr (Pakete werden vorgepackt)
|
||||
if ($this->model->next_date) {
|
||||
$daysUntilExecution = (int) now()->diffInDays(\Carbon\Carbon::parse($this->model->next_date), false);
|
||||
if ($daysUntilExecution >= 0 && $daysUntilExecution < self::LOCK_DAYS_CHANGE) {
|
||||
\Session()->flash('alert-error', __('abo.error_change_locked', ['days' => $daysUntilExecution]));
|
||||
|
||||
if (! $executedThisMonth && $data['abo_interval'] <= now()->day) {
|
||||
\Session()->flash('alert-error', __('abo.error_abo_interval_in_the_past'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfung: Das neue berechnete Ausführungsdatum muss mindestens LOCK_DAYS_CHANGE Tage entfernt sein.
|
||||
// Falls next_date bereits in einem zukünftigen Monat liegt, wird das neue Datum in diesem Monat berechnet.
|
||||
$newNextDate = $this->calculateNewNextDate($data['abo_interval']);
|
||||
$daysUntilNewDate = (int) now()->diffInDays($newNextDate, false);
|
||||
if ($daysUntilNewDate < self::LOCK_DAYS_CHANGE) {
|
||||
\Session()->flash('alert-error', __('abo.error_abo_interval_too_soon', ['days' => $daysUntilNewDate]));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet das neue Ausführungsdatum unter Berücksichtigung des aktuellen next_date.
|
||||
* Falls next_date bereits in einem zukünftigen Monat liegt, wird der Monatsanfang
|
||||
* dieses Monats als Referenz verwendet, sodass der neue Tag im selben Monat landet.
|
||||
*/
|
||||
private function calculateNewNextDate(int $aboInterval): \Carbon\Carbon
|
||||
{
|
||||
$referenceDate = now();
|
||||
|
||||
if ($this->model->next_date) {
|
||||
$currentNextDate = \Carbon\Carbon::parse($this->model->next_date);
|
||||
|
||||
if ($currentNextDate->format('Y-m') > now()->format('Y-m')) {
|
||||
$referenceDate = $currentNextDate->startOfMonth();
|
||||
}
|
||||
}
|
||||
|
||||
return AboHelper::setNextDate($referenceDate, $aboInterval);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ use App\Models\ShoppingOrder;
|
|||
use App\Models\UserInvoice;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\Services\Invoice;
|
||||
use App\Services\UserService;
|
||||
use App\Services\Util;
|
||||
use Storage;
|
||||
|
||||
class InvoiceRepository extends BaseRepository
|
||||
|
|
@ -217,10 +219,15 @@ class InvoiceRepository extends BaseRepository
|
|||
|
||||
public function createAndSalesVolume($request = [])
|
||||
{
|
||||
$this->user_sales_volume = SalesPointsVolume::addSalesPointsVolumeUser($this->model);
|
||||
$user_invoice = $this->create($request);
|
||||
$this->user_sales_volume->user_invoice_id = $user_invoice->id;
|
||||
$this->user_sales_volume->save();
|
||||
$this->user_sales_volume = SalesPointsVolume::User($this->model);
|
||||
if (! Util::isTestSystem(true)) { // rechnung erstellen nur in production
|
||||
$user_invoice = $this->create($request);
|
||||
$this->user_sales_volume->user_invoice_id = $user_invoice->id;
|
||||
$this->user_sales_volume->save();
|
||||
}
|
||||
|
||||
// Incentive: Track sales volume points
|
||||
IncentiveTracker::trackSalesVolume($this->user_sales_volume);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
162
app/User.php
162
app/User.php
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use App\Models\PaymentMethod;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Mail\MailResetPassword;
|
||||
use App\Models\PaymentMethod;
|
||||
use App\Models\UserSalesVolume;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
|
||||
/**
|
||||
|
|
@ -24,6 +24,7 @@ use Laravel\Passport\HasApiTokens;
|
|||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereId($value)
|
||||
|
|
@ -32,6 +33,7 @@ use Laravel\Passport\HasApiTokens;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereRememberToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereUpdatedAt($value)
|
||||
*
|
||||
* @property int $confirmed
|
||||
* @property string|null $confirmation_code
|
||||
* @property string|null $confirmation_date
|
||||
|
|
@ -47,6 +49,7 @@ use Laravel\Passport\HasApiTokens;
|
|||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read \App\Models\Account $account
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UserUpdateEmail[] $user_update_email
|
||||
*
|
||||
* @method static bool|null forceDelete()
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\User onlyTrashed()
|
||||
* @method static bool|null restore()
|
||||
|
|
@ -65,11 +68,14 @@ use Laravel\Passport\HasApiTokens;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereNotes($value)
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\User withTrashed()
|
||||
* @method static \Illuminate\Database\Query\Builder|\App\User withoutTrashed()
|
||||
*
|
||||
* @property int|null $account_id
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereAccountId($value)
|
||||
*
|
||||
* @property int|null $wizard
|
||||
* @property int|null $blocked
|
||||
* @property string|null $payment_account
|
||||
|
|
@ -77,16 +83,20 @@ use Laravel\Passport\HasApiTokens;
|
|||
* @property-read int|null $notifications_count
|
||||
* @property-read \App\Models\UserShop $shop
|
||||
* @property-read int|null $user_update_email_count
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereBlocked($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User wherePaymentAccount($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User wherePaymentShop($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereWizard($value)
|
||||
*
|
||||
* @property int|null $m_level
|
||||
* @property int|null $m_sponsor
|
||||
* @property-read \App\Models\UserLevel|null $user_level
|
||||
* @property-read \App\User|null $user_sponsor
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereMLevel($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereMSponsor($value)
|
||||
*
|
||||
* @property string|null $release_account
|
||||
* @property int|null $payment_order_id
|
||||
* @property int|null $abo_options
|
||||
|
|
@ -95,9 +105,11 @@ use Laravel\Passport\HasApiTokens;
|
|||
* @property-read \App\Models\Product|null $payment_order
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ShoppingOrder[] $shopping_orders
|
||||
* @property-read int|null $shopping_orders_count
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereAboOptions($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User wherePaymentOrderId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereReleaseAccount($value)
|
||||
*
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UserHistory[] $user_histories
|
||||
* @property-read int|null $user_histories_count
|
||||
* @property int|null $test_mode
|
||||
|
|
@ -107,28 +119,37 @@ use Laravel\Passport\HasApiTokens;
|
|||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ShoppingUser[] $member_shopping_users
|
||||
* @property-read int|null $member_shopping_users_count
|
||||
* @property-read \App\Models\Product|null $payment_order_product
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereSettings($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereTestMode($value)
|
||||
*
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Laravel\Passport\Client[] $clients
|
||||
* @property-read int|null $clients_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Laravel\Passport\Token[] $tokens
|
||||
* @property-read int|null $tokens_count
|
||||
* @property array|null $payment_methods
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\User wherePaymentMethods($value)
|
||||
*
|
||||
* @property int|null $pre_sponsor
|
||||
* @property-read User|null $user_pre_sponsor
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User wherePreSponsor($value)
|
||||
*
|
||||
* @property \Illuminate\Support\Carbon|null $pre_deleted_at
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePreDeletedAt($value)
|
||||
*
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\UserBusiness> $userBusiness
|
||||
* @property-read int|null $user_business_count
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use Notifiable, HasApiTokens;
|
||||
|
||||
use HasApiTokens, Notifiable;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
protected $table = 'users';
|
||||
|
|
@ -138,7 +159,6 @@ class User extends Authenticatable
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
|
||||
protected $fillable = [
|
||||
'email',
|
||||
'password',
|
||||
|
|
@ -195,7 +215,6 @@ class User extends Authenticatable
|
|||
return $this->hasMany('App\Models\File', 'user_id', '');
|
||||
}
|
||||
|
||||
|
||||
public function shopping_orders()
|
||||
{
|
||||
return $this->hasMany('App\Models\ShoppingOrder', 'auth_user_id', '');
|
||||
|
|
@ -236,24 +255,26 @@ class User extends Authenticatable
|
|||
return $this->lang ? $this->lang : \App::getLocale();
|
||||
}
|
||||
|
||||
|
||||
public function getMUserSponsor()
|
||||
{
|
||||
if ($this->user_sponsor && $this->user_sponsor->account) {
|
||||
return $this->user_sponsor->account->first_name . " " . $this->user_sponsor->account->last_name . " | " . $this->user_sponsor->email;
|
||||
return $this->user_sponsor->account->first_name.' '.$this->user_sponsor->account->last_name.' | '.$this->user_sponsor->email;
|
||||
}
|
||||
}
|
||||
|
||||
public function getFullName($email = true)
|
||||
{
|
||||
$ret = "";
|
||||
$ret = '';
|
||||
if ($this->account) {
|
||||
$ret = $this->account->first_name . " " . $this->account->last_name;
|
||||
$ret = $this->account->first_name.' '.$this->account->last_name;
|
||||
}
|
||||
if ($email && $this->id > 1) {
|
||||
$ret .= " | " . $this->email;
|
||||
$ret .= ' | '.$this->email;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
|
@ -262,10 +283,10 @@ class User extends Authenticatable
|
|||
if ($this->password == env('APP_KEY')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
|
@ -274,10 +295,10 @@ class User extends Authenticatable
|
|||
if ($this->admin >= 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
|
@ -286,6 +307,7 @@ class User extends Authenticatable
|
|||
if ($this->admin >= 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -297,6 +319,7 @@ class User extends Authenticatable
|
|||
if ($this->admin >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -308,15 +331,16 @@ class User extends Authenticatable
|
|||
if ($this->admin >= 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function isUserHasApi()
|
||||
{
|
||||
if ($this->id === 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +352,7 @@ class User extends Authenticatable
|
|||
if ($this->admin >= 5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -339,15 +364,25 @@ class User extends Authenticatable
|
|||
return $this->test_mode ? true : false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function showSideNav()
|
||||
{
|
||||
if ($this->active == 1 && $this->blocked == 0 && $this->wizard >= 10) {
|
||||
if ($this->blocked != 0 || $this->wizard < 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->active == 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Nach Account-Ablauf setzt u. a. cleanUpInActiveUser active=0; Zahlung/Mitgliedschaft
|
||||
// muss im CRM weiterhin erreichbar sein (Navigation „Mein Konto“).
|
||||
if ($this->payment_account && ! $this->isActiveAccount()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -361,6 +396,7 @@ class User extends Authenticatable
|
|||
{
|
||||
return ($this->active == 1 && $this->blocked == 0) ? true : false;
|
||||
}
|
||||
|
||||
public function isActiveAccount()
|
||||
{
|
||||
return $this->payment_account ? Carbon::parse($this->payment_account)->gt(Carbon::now()) : false;
|
||||
|
|
@ -374,14 +410,15 @@ class User extends Authenticatable
|
|||
public function isRenewalAccount()
|
||||
{
|
||||
if ($this->payment_account) {
|
||||
return Carbon::parse($this->payment_account)->modify('-' . (config('mivita.renewal_days') + 1) . ' days')->lt(Carbon::now());
|
||||
return Carbon::parse($this->payment_account)->modify('-'.(config('mivita.renewal_days') + 1).' days')->lt(Carbon::now());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function nextRenewalAccount()
|
||||
{
|
||||
return $this->payment_account ? Carbon::parse($this->payment_account)->modify('-' . config('mivita.renewal_days') . ' days')->format(\Util::formatDateTimeDB()) : false;
|
||||
return $this->payment_account ? Carbon::parse($this->payment_account)->modify('-'.config('mivita.renewal_days').' days')->format(\Util::formatDateTimeDB()) : false;
|
||||
}
|
||||
|
||||
public function daysActiveAccount()
|
||||
|
|
@ -389,7 +426,7 @@ class User extends Authenticatable
|
|||
return Carbon::now()->diffInDays(Carbon::parse($this->payment_account), false);
|
||||
}
|
||||
|
||||
public function modifyActiveAccount($add = "1 year")
|
||||
public function modifyActiveAccount($add = '1 year')
|
||||
{
|
||||
return Carbon::parse($this->payment_account)->modify($add)->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
|
@ -404,7 +441,7 @@ class User extends Authenticatable
|
|||
return Carbon::now()->diffInDays(Carbon::parse($this->payment_shop), false);
|
||||
}
|
||||
|
||||
public function modifyActiveShop($add = "1 year")
|
||||
public function modifyActiveShop($add = '1 year')
|
||||
{
|
||||
return Carbon::parse($this->payment_shop)->modify($add)->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
|
@ -417,12 +454,13 @@ class User extends Authenticatable
|
|||
public function isAcountAboPayDate()
|
||||
{
|
||||
if ($this->isAboOption()) {
|
||||
$pay_days = Carbon::parse($this->payment_account)->modify('- ' . config('mivita.abo_booking_days') . ' days');
|
||||
$pay_days = Carbon::parse($this->payment_account)->modify('- '.config('mivita.abo_booking_days').' days');
|
||||
$diff_days = Carbon::now()->diffInDays($pay_days, false);
|
||||
if ($diff_days <= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -431,9 +469,10 @@ class User extends Authenticatable
|
|||
*/
|
||||
public function getConfirmationDateFormat()
|
||||
{
|
||||
if (!$this->attributes['confirmation_date']) {
|
||||
return "";
|
||||
if (! $this->attributes['confirmation_date']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Carbon::parse($this->attributes['confirmation_date'])->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
||||
|
|
@ -442,12 +481,13 @@ class User extends Authenticatable
|
|||
*/
|
||||
public function getActiveDateFormat($time = true)
|
||||
{
|
||||
if (!$this->attributes['active_date']) {
|
||||
return "";
|
||||
if (! $this->attributes['active_date']) {
|
||||
return '';
|
||||
}
|
||||
if (!$time) {
|
||||
if (! $time) {
|
||||
return Carbon::parse($this->attributes['active_date'])->format(\Util::formatDateDB());
|
||||
}
|
||||
|
||||
return Carbon::parse($this->attributes['active_date'])->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
||||
|
|
@ -456,55 +496,59 @@ class User extends Authenticatable
|
|||
*/
|
||||
public function getAgreementFormat()
|
||||
{
|
||||
if (!$this->attributes['agreement']) {
|
||||
return "";
|
||||
if (! $this->attributes['agreement']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Carbon::parse($this->attributes['agreement'])->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
||||
public function getPaymentAccountDateFormat($time = true)
|
||||
{
|
||||
if (!$this->attributes['payment_account']) {
|
||||
return "";
|
||||
if (! $this->attributes['payment_account']) {
|
||||
return '';
|
||||
}
|
||||
if (!$time) {
|
||||
if (! $time) {
|
||||
return Carbon::parse($this->attributes['payment_account'])->format(\Util::formatDateDB());
|
||||
}
|
||||
|
||||
return Carbon::parse($this->attributes['payment_account'])->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
||||
public function getPaymentShopDateFormat($time = true)
|
||||
{
|
||||
if (!$this->attributes['payment_shop']) {
|
||||
return "";
|
||||
if (! $this->attributes['payment_shop']) {
|
||||
return '';
|
||||
}
|
||||
if (!$time) {
|
||||
if (! $time) {
|
||||
return Carbon::parse($this->attributes['payment_shop'])->format(\Util::formatDateDB());
|
||||
}
|
||||
|
||||
return Carbon::parse($this->attributes['payment_shop'])->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
||||
public function getReleaseAccountFormat($time = true)
|
||||
{
|
||||
if (!$this->attributes['release_account']) {
|
||||
return "";
|
||||
if (! $this->attributes['release_account']) {
|
||||
return '';
|
||||
}
|
||||
if (!$time) {
|
||||
if (! $time) {
|
||||
return Carbon::parse($this->attributes['release_account'])->format(\Util::formatDateDB());
|
||||
}
|
||||
|
||||
return Carbon::parse($this->attributes['release_account'])->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
|
||||
|
||||
public function setSetting(array $revisions, bool $save = true)
|
||||
{
|
||||
if (!$this->settings) {
|
||||
if (! $this->settings) {
|
||||
$this->settings = [];
|
||||
}
|
||||
$this->settings = array_merge($this->settings, $revisions);
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -515,38 +559,41 @@ class User extends Authenticatable
|
|||
|
||||
public function getPaymentMethodsShort()
|
||||
{
|
||||
$ret = "";
|
||||
$ret = '';
|
||||
if ($this->payment_methods !== null) {
|
||||
foreach ($this->payment_methods as $payment_method) {
|
||||
if ($find = PaymentMethod::find($payment_method)) {
|
||||
$ret .= $find->short . " | ";
|
||||
$ret .= $find->short.' | ';
|
||||
}
|
||||
}
|
||||
$ret = rtrim($ret, " | ");
|
||||
$ret = rtrim($ret, ' | ');
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLandByCountry()
|
||||
{
|
||||
if ($this->account && $this->account->country_id) {
|
||||
if ($this->account && $this->account->country_id) {
|
||||
$code = $this->account->country->code;
|
||||
if ($code == "FR") {
|
||||
if ($code == 'FR') {
|
||||
return 'fr';
|
||||
}
|
||||
if ($code == "CH") {
|
||||
if ($code == 'CH') {
|
||||
return 'de';
|
||||
}
|
||||
if ($code == "NL") {
|
||||
if ($code == 'NL') {
|
||||
return 'nl';
|
||||
}
|
||||
if ($code == "DE") {
|
||||
if ($code == 'DE') {
|
||||
return 'de';
|
||||
}
|
||||
}
|
||||
return "de";
|
||||
|
||||
return 'de';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -557,16 +604,16 @@ class User extends Authenticatable
|
|||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
//$bcc[] = "kevin.adametz@me.com"; //config('app.checkout_mail');
|
||||
//Mail::to($this->email)->bcc($bcc)->locale(\App::getLocale())->send(new MailResetPassword($token, $this));
|
||||
// $bcc[] = "kevin.adametz@me.com"; //config('app.checkout_mail');
|
||||
// Mail::to($this->email)->bcc($bcc)->locale(\App::getLocale())->send(new MailResetPassword($token, $this));
|
||||
Mail::to($this->email)->locale(\App::getLocale())->send(new MailResetPassword($token, $this));
|
||||
//$this->notify(new ResetPasswordNotification($token));
|
||||
// $this->notify(new ResetPasswordNotification($token));
|
||||
}
|
||||
|
||||
public function getUserSalesVolumeBy($month, $year, $key)
|
||||
{
|
||||
|
||||
//NOTE check ist, cant change month year !
|
||||
// NOTE check ist, cant change month year !
|
||||
if ($this->userSalesVolume === false) {
|
||||
$this->userSalesVolume = $this->getUserSalesVolume($month, $year, 'first');
|
||||
}
|
||||
|
|
@ -588,7 +635,7 @@ class User extends Authenticatable
|
|||
case 'sales_volume_points_TP_sum':
|
||||
return $this->userSalesVolume->getPointsTPSum();
|
||||
break;
|
||||
//price net
|
||||
// price net
|
||||
case 'sales_volume_total':
|
||||
return $this->userSalesVolume->month_total_net;
|
||||
break;
|
||||
|
|
@ -602,10 +649,11 @@ class User extends Authenticatable
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
//with = ['shopping_order.shopping_user'] <- optional wenn es noch weitere relations gibt
|
||||
// with = ['shopping_order.shopping_user'] <- optional wenn es noch weitere relations gibt
|
||||
public function getUserSalesVolume($month, $year, $record = 'get', $with = [])
|
||||
{
|
||||
$relations = array_merge(['shopping_order'], $with);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue