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>
This commit is contained in:
Kevin 2026-07-09 14:45:17 +00:00
parent 6f21845607
commit ce4750fd1a
68 changed files with 2856 additions and 236 deletions

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Stripe-Einführung Phase S4 (dev/2026-07-08-stripe/):
* Abo-fähige Stripe-Zahlungen. Der Webhook schreibt Customer-/PaymentMethod-ID
* an die Zahlung (Transport zum Abo in createNewAbo/advanceAboAfterRecoveryPayment);
* user_abo_id markiert wiederkehrende Cron-Abbuchungen, damit der Paid-Pfad
* daraus kein neues Abo anlegt.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('shopping_payments', function (Blueprint $table) {
if (! Schema::hasColumn('shopping_payments', 'stripe_customer_id')) {
$table->string('stripe_customer_id')->nullable()->after('external_reference');
}
if (! Schema::hasColumn('shopping_payments', 'stripe_payment_method_id')) {
$table->string('stripe_payment_method_id')->nullable()->after('stripe_customer_id');
}
if (! Schema::hasColumn('shopping_payments', 'user_abo_id')) {
$table->unsignedBigInteger('user_abo_id')->nullable()->index()->after('stripe_payment_method_id');
}
});
}
public function down(): void
{
Schema::table('shopping_payments', function (Blueprint $table) {
foreach (['stripe_customer_id', 'stripe_payment_method_id', 'user_abo_id'] as $column) {
if (Schema::hasColumn('shopping_payments', $column)) {
$table->dropColumn($column);
}
}
});
}
};

View file

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Stripe-Einführung Phase S4 (dev/2026-07-08-stripe/):
* clearingtype war varchar(3) (PAYONE-Kürzel cc/wlt/elv/vor/fnc/sb) — MySQL hat
* 'stripe' still zu 'str' abgeschnitten (SQLite in den Tests erzwingt keine Länge,
* daher unentdeckt). Spalten verbreitern und die abgeschnittenen Bestandswerte
* reparieren. Anzeige-/Logikpfade lesen payment_provider, nicht clearingtype —
* der Fix ist Datenhygiene.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('shopping_payments', function (Blueprint $table) {
$table->string('clearingtype', 20)->nullable(false)->change();
});
Schema::table('user_abos', function (Blueprint $table) {
$table->string('clearingtype', 20)->nullable(false)->change();
});
DB::table('shopping_payments')
->where('payment_provider', 'stripe')
->where('clearingtype', 'str')
->update(['clearingtype' => 'stripe']);
DB::table('user_abos')
->where('payment_provider', 'stripe')
->where('clearingtype', 'str')
->update(['clearingtype' => 'stripe']);
}
public function down(): void
{
// Rückweg bewusst ohne Verkürzung auf varchar(3): das würde Daten
// abschneiden. Nur die Werte zurückdrehen.
DB::table('shopping_payments')
->where('payment_provider', 'stripe')
->where('clearingtype', 'stripe')
->update(['clearingtype' => 'str']);
DB::table('user_abos')
->where('payment_provider', 'stripe')
->where('clearingtype', 'stripe')
->update(['clearingtype' => 'str']);
}
};