Phase S4 – Abos über Stripe (E2E komplett im Testmodus): - Abo-/Recovery-Checkout: customer_creation=always + setup_future_usage=off_session - StripePaymentService::chargeAbo (off-session PaymentIntent, Idempotency-Key), StripeGateway, Provider-Dispatch im Abbuchungs-Cron (PAYONE-Pfad unverändert) - Webhook: Zahlungsmittel-Capture vor Paid-Pfad, 503 payment_not_yet_visible bei Cron-Transaktions-Race; Command stripe:process-pending-events (Cron-Ende + 10-min Scheduler) verarbeitet liegengebliebene Events nach - createNewAbo/advanceAboAfterRecoveryPayment Stripe-aware inkl. Race-Absicherung; Recovery-Orders nutzen advanceAboAfterRecoveryPayment (latenter PAYONE-Duplikat-Bug) - Migrationen: Stripe-Abo-Spalten auf shopping_payments; clearingtype varchar(3)→(20) inkl. Reparatur der zu 'str' gekürzten Live-Zeilen Phase S5 – Zahlungsart ändern (Scope: jede Zahlart, nicht nur Kreditkarte): - Stripe Checkout mode=setup ohne Abbuchung; PAYONE-Abos ziehen beim Wechsel auf Stripe um (Auslaufmodell §7.2) - AboPaymentMethodService: beginStripeChange/finalize/applyStripePaymentMethod (idempotent, detacht alte PM, card_changes-Log); Webhook setup_intent.succeeded als Sicherheitsnetz; Einstiege Portal/CRM/Berater-Kunden-Link flow-abhängig - Views/Mails flow-abhängig („Zahlungsweise" statt „Kreditkarte") Phase S7.6 – Zahlarten-Kommunikation: - Einheitliche Zahlarten-Logos (SVG) + wiederverwendbares Partial payment_logos; Checkout/Footer zeigen Kern-Zahlarten + „···"-Icon, /zahlungsarten komplett neu mit allen aktiven Stripe-Zahlarten (gegen Account-Konfiguration verifiziert) und Vorkasse (PAYONE); Sofort/SEPA entfernt Tests: StripeAboTest (15), StripePaymentMethodChangeTest (10), PaymentMethodsCommunicationTest (28), PaymentProviderManagerTest angepasst. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.7 KiB
PHP
72 lines
2.7 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\StripeWebhookEvent;
|
|
use App\Services\Payment\Stripe\StripeWebhookHandler;
|
|
use Illuminate\Console\Command;
|
|
|
|
/**
|
|
* Verarbeitet gespeicherte, aber noch unverarbeitete Stripe-Webhook-Events nach
|
|
* (Plan dev/2026-07-08-stripe/, S6.4 vorgezogen in S4).
|
|
*
|
|
* Hintergrund: Der Abo-Cron bucht innerhalb einer DB-Transaktion ab — der Webhook
|
|
* kann den Commit überholen und wird dann mit 503 beantwortet. In Produktion
|
|
* stellt Stripe non-2xx erneut zu (mit Verzögerung), die Stripe CLI auf dem
|
|
* Test-Server jedoch nicht. Dieses Command schließt beide Lücken: es läuft am
|
|
* Ende des Abo-Crons und regelmäßig über den Scheduler.
|
|
*/
|
|
class StripeProcessPendingEvents extends Command
|
|
{
|
|
protected $signature = 'stripe:process-pending-events
|
|
{--event= : Nur dieses eine Event (event_id) nachverarbeiten}';
|
|
|
|
protected $description = 'Verarbeitet gespeicherte, unverarbeitete Stripe-Webhook-Events nach (z. B. nach Cron-Race oder verpasstem Retry)';
|
|
|
|
public function handle(StripeWebhookHandler $webhookHandler): int
|
|
{
|
|
$query = StripeWebhookEvent::whereNull('processed_at')
|
|
->whereIn('type', StripeWebhookHandler::HANDLED_TYPES)
|
|
->orderBy('id');
|
|
|
|
if ($this->option('event')) {
|
|
$query->where('event_id', $this->option('event'));
|
|
}
|
|
|
|
$pending = $query->get();
|
|
|
|
if ($pending->isEmpty()) {
|
|
$this->info('Keine unverarbeiteten Stripe-Events.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$processed = 0;
|
|
$skipped = 0;
|
|
|
|
foreach ($pending as $storedEvent) {
|
|
try {
|
|
if ($webhookHandler->processStoredEvent($storedEvent)) {
|
|
$processed++;
|
|
$this->info("Verarbeitet: {$storedEvent->event_id} ({$storedEvent->type})");
|
|
} else {
|
|
$skipped++;
|
|
$this->warn("Weiterhin nicht zuordenbar: {$storedEvent->event_id} ({$storedEvent->type})");
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$skipped++;
|
|
$this->error("Fehler bei {$storedEvent->event_id}: {$e->getMessage()}");
|
|
\Log::channel('stripe')->error('stripe:process-pending-events fehlgeschlagen', [
|
|
'event_id' => $storedEvent->event_id,
|
|
'type' => $storedEvent->type,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
\Log::channel('stripe')->info("stripe:process-pending-events: {$processed} verarbeitet, {$skipped} übersprungen.");
|
|
$this->info("Fertig: {$processed} verarbeitet, {$skipped} übersprungen.");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|