mivita/app/Console/Commands/StripeProcessPendingEvents.php
Kevin ce4750fd1a Stripe-Integration (S4, S5, S7.6): Abos, Zahlungsart ändern, Zahlarten-Kommunikation
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>
2026-07-09 14:45:17 +00:00

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