Abo Einmalprodukte: Phase 4 - Ausfuehrung, Purge & User-Retry

- UserMakeOrder: bestaetigte Einmal-Artikel in den Yard, is_abo_addon
  auf ShoppingOrderItem; amount bleibt reiner Abo-Betrag (Reihenfolge)
- AboOneTimeService::purgeAfterExecution: loescht alle Einmal-Artikel
  und rechnet Comp-Produkte neu - nur im Erfolgszweig (Cron + Retry)
- User-Retry in Sales Center und Portal mit Berechtigungspruefung,
  gemeinsames Confirm-Modal; Admin-Retry unveraendert
- Tests: AboMakeOrderOneTimeTest, AboUserRetryTest; Plan-Doku Phase 4

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin 2026-06-08 15:32:27 +00:00
parent 8288ea59ac
commit ee04146217
14 changed files with 536 additions and 19 deletions

View file

@ -6,6 +6,7 @@ use App\Cron\UserMakeOrder;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Services\AboHelper;
use App\Services\AboOneTimeService;
use App\Services\Incentive\IncentiveTracker;
use App\Services\MyLog;
use App\Services\Payment;
@ -74,7 +75,7 @@ class UserMakeAboOrder extends Command
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$this->error('Fehler beim Ausführen des Befehls: ' . $e->getMessage());
$this->error('Fehler beim Ausführen des Befehls: '.$e->getMessage());
return 1;
}
@ -164,7 +165,7 @@ class UserMakeAboOrder extends Command
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$this->error("Fehler bei Abo {$userAbo->id}: " . $e->getMessage());
$this->error("Fehler bei Abo {$userAbo->id}: ".$e->getMessage());
}
}
}
@ -178,7 +179,7 @@ class UserMakeAboOrder extends Command
private function makeOrder($userAbo)
{
\Log::channel('abo_order')->info('UserMakeAboOrder: Starte Bestellungserstellung', ['abo_id' => $userAbo->id]);
$this->info('Starte Bestellungserstellung für Abo: ' . $userAbo->id);
$this->info('Starte Bestellungserstellung für Abo: '.$userAbo->id);
$shoppingOrder = null;
$userOrder = new UserMakeOrder($userAbo);
@ -205,7 +206,7 @@ class UserMakeAboOrder extends Command
]);
$response = $userOrder->makePayment();
$this->info('makePayment response: ' . json_encode($response));
$this->info('makePayment response: '.json_encode($response));
// Prüfe ob Response ein Array ist (kann auch Objekt sein)
if (is_object($response)) {
@ -274,7 +275,7 @@ class UserMakeAboOrder extends Command
'status' => $response['status'],
]);
$this->info("Zahlung ausstehend für Abo {$userAbo->id}: {$response['status']}");
$this->updateAboOnError($userAbo, $shoppingOrder, 'Zahlung ausstehend: ' . $response['status']);
$this->updateAboOnError($userAbo, $shoppingOrder, 'Zahlung ausstehend: '.$response['status']);
} else {
// Unbekannter Status: Bestellung speichern, aber Abo nicht aktualisieren
\Log::channel('abo_order')->warning('UserMakeAboOrder: Unbekannter Zahlungsstatus', [
@ -283,7 +284,7 @@ class UserMakeAboOrder extends Command
'status' => $response['status'],
]);
$this->warn("Unbekannter Zahlungsstatus für Abo {$userAbo->id}: {$response['status']}");
$this->updateAboOnError($userAbo, $shoppingOrder, 'Unbekannter Status: ' . $response['status']);
$this->updateAboOnError($userAbo, $shoppingOrder, 'Unbekannter Status: '.$response['status']);
}
} catch (\Throwable $e) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Ausnahme bei der Bestellungserstellung', [
@ -291,11 +292,11 @@ class UserMakeAboOrder extends Command
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
$this->error("Ausnahme bei Abo {$userAbo->id}: " . $e->getMessage());
$this->error("Ausnahme bei Abo {$userAbo->id}: ".$e->getMessage());
// Bestellung existiert (z. B. Fehler bei Payone): Abo-Fehlerstatus, Bestellung bleibt nachvollziehbar
if ($shoppingOrder) {
$this->updateAboOnError($userAbo, $shoppingOrder, 'Exception: ' . $e->getMessage());
$this->updateAboOnError($userAbo, $shoppingOrder, 'Exception: '.$e->getMessage());
return $shoppingOrder;
}
@ -356,12 +357,15 @@ class UserMakeAboOrder extends Command
// Wie bei Payment::paymentStatusPaidAction: Incentive nur wenn Callback nicht lief
// (firstOrCreate verhindert Doppelungen wenn Payone später noch trackt)
IncentiveTracker::trackAboActivated($shoppingOrder);
// Nur bei Erfolg: Einmal-Artikel entfernen und Comp-Produkte neu bewerten.
AboOneTimeService::purgeAfterExecution($userAbo);
} catch (\Exception $e) {
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler beim Aktualisieren des Abos', [
'abo_id' => $userAbo->id,
'error' => $e->getMessage(),
]);
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: ".$e->getMessage());
throw $e; // Re-throw für besseres Error-Handling
}
}
@ -421,7 +425,7 @@ class UserMakeAboOrder extends Command
'abo_id' => $userAbo->id,
'error' => $e->getMessage(),
]);
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $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
}
}
@ -437,6 +441,6 @@ class UserMakeAboOrder 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';
}
}