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');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue