Stripe-Integration (S0–S3): Einmalzahlungen über Stripe Checkout + Notfall-Go-Live

Wegen PAYONE-Ausfall vorgezogen live geschaltet: alle Einmalzahlungen
(Shop + Salescenter) laufen über Stripe Hosted Checkout, Abos sind bis
Phase S4 eingefroren.

- Provider-Abstraktion: PaymentProvider-Enum, PaymentGatewayInterface,
  PaymentProviderManager (Settings-Schalter, provider-treu je Bestand),
  PayoneGateway als unveränderter Adapter
- StripePaymentService: Checkout Session (eine Gesamtposition, Metadata,
  success/cancel-Redirects), StripeClientWrapper (lazy, Dark Launch),
  StripeCustomerService, StripeTransactionRecorder (client_secret-Redaction)
- Webhook /stripe/webhook: Signaturprüfung, Idempotenz über
  stripe_webhook_events (Crash-Retry-Reprocessing), txaction-Prioritäten,
  Betrags-Guard, lockForUpdate-Paid-Pfad, genau eine Bestätigungsmail
- Zahlungsmethoden-Anzeige aus charge.succeeded (carddata inkl. Wallet,
  auftragsunabhängiges Backfill aus dem Event-Store)
- Checkout: Wartungsmodus mit Admin-Bypass, Abo-Freeze-Hinweis,
  Stripe-Option (Online-Zahlung) statt Zahlartenwahl, Vorkasse ausblendbar
- Abo-Freeze-Guards: Cron user:make_abo_order, Retry, Kartenupdate
- Admin-Settings-Karte Zahlungen (Provider, Wartung, Freeze, Vorkasse)
- Hotfix: 500 bei Vorkasse ohne Clearing-Daten in admin/sales/_detail
  und checkout-final (PAYONE-Ausfall)
- Doku dev/2026-07-08-stripe/ (Plan S0–S8, Konzept, IST-Analyse)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-07-08 15:56:41 +00:00
parent 1108381988
commit e1da90ff91
48 changed files with 4428 additions and 831 deletions

View file

@ -0,0 +1,97 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Stripe-Einführung Phase S1 (dev/2026-07-08-stripe/):
* Jede Zahlung / jedes Abo trägt seinen Zahlungsanbieter fest, damit der
* Einstellungs-Schalter nur NEUE Vorgänge betrifft. Default 'payone'
* backfillt zugleich alle Bestandszeilen. PAYONE-Spalten bleiben unverändert.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('user_abos', function (Blueprint $table) {
if (! Schema::hasColumn('user_abos', 'payment_provider')) {
$table->string('payment_provider', 20)->default('payone')->index()->after('payone_userid');
}
if (! Schema::hasColumn('user_abos', 'stripe_customer_id')) {
$table->string('stripe_customer_id')->nullable()->after('carddata');
}
if (! Schema::hasColumn('user_abos', 'stripe_payment_method_id')) {
$table->string('stripe_payment_method_id')->nullable()->after('stripe_customer_id');
}
});
Schema::table('shopping_payments', function (Blueprint $table) {
if (! Schema::hasColumn('shopping_payments', 'payment_provider')) {
$table->string('payment_provider', 20)->default('payone')->index()->after('clearingtype');
}
if (! Schema::hasColumn('shopping_payments', 'external_reference')) {
$table->string('external_reference')->nullable()->after('reference');
}
});
Schema::table('payment_transactions', function (Blueprint $table) {
if (! Schema::hasColumn('payment_transactions', 'provider')) {
$table->string('provider', 20)->default('payone')->after('shopping_payment_id');
}
});
Schema::table('users', function (Blueprint $table) {
if (! Schema::hasColumn('users', 'stripe_customer_id')) {
$table->string('stripe_customer_id')->nullable();
}
});
if (Schema::hasTable('customers')) {
Schema::table('customers', function (Blueprint $table) {
if (! Schema::hasColumn('customers', 'stripe_customer_id')) {
$table->string('stripe_customer_id')->nullable();
}
});
}
}
public function down(): void
{
Schema::table('user_abos', function (Blueprint $table) {
foreach (['payment_provider', 'stripe_customer_id', 'stripe_payment_method_id'] as $column) {
if (Schema::hasColumn('user_abos', $column)) {
$table->dropColumn($column);
}
}
});
Schema::table('shopping_payments', function (Blueprint $table) {
foreach (['payment_provider', 'external_reference'] as $column) {
if (Schema::hasColumn('shopping_payments', $column)) {
$table->dropColumn($column);
}
}
});
Schema::table('payment_transactions', function (Blueprint $table) {
if (Schema::hasColumn('payment_transactions', 'provider')) {
$table->dropColumn('provider');
}
});
Schema::table('users', function (Blueprint $table) {
if (Schema::hasColumn('users', 'stripe_customer_id')) {
$table->dropColumn('stripe_customer_id');
}
});
if (Schema::hasTable('customers')) {
Schema::table('customers', function (Blueprint $table) {
if (Schema::hasColumn('customers', 'stripe_customer_id')) {
$table->dropColumn('stripe_customer_id');
}
});
}
}
};

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Stripe-Einführung Phase S2 (dev/2026-07-08-stripe/):
* Idempotenz-Store + Audit-Log für Stripe-Webhook-Events. Die eindeutige
* event_id verhindert Doppelverarbeitung bei Stripe-Retries (Muster:
* Duplikat-Erkennung des PAYONE-Webhooks).
*/
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('stripe_webhook_events')) {
return;
}
Schema::create('stripe_webhook_events', function (Blueprint $table) {
$table->id();
$table->string('event_id')->unique();
$table->string('type')->index();
$table->string('api_version')->nullable();
$table->json('payload')->nullable();
$table->timestamp('processed_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('stripe_webhook_events');
}
};