mivita/app/Console/Commands/RetryFailedPaypalAbos.php
2026-04-10 17:15:27 +02:00

297 lines
9.6 KiB
PHP

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