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 671022a28d
commit 6f21845607
48 changed files with 4428 additions and 831 deletions

View file

@ -133,25 +133,25 @@
<strong>{{ __('payment.account_holder') }}:</strong>
</td>
<td style="padding: 5px 0;">
{{ $pay_trans->transmitted_data['clearing_bankaccountholder'] }}</td>
{{ $pay_trans->transmitted_data['clearing_bankaccountholder'] ?? '—' }}</td>
</tr>
<tr>
<td style="padding: 5px 10px 5px 0;"><strong>{{ __('payment.IBAN') }}:</strong>
</td>
<td style="padding: 5px 0; font-family: monospace; letter-spacing: 1px;">
{{ $pay_trans->transmitted_data['clearing_bankiban'] }}</td>
{{ $pay_trans->transmitted_data['clearing_bankiban'] ?? '—' }}</td>
</tr>
<tr>
<td style="padding: 5px 10px 5px 0;"><strong>{{ __('payment.BIC') }}:</strong>
</td>
<td style="padding: 5px 0; font-family: monospace;">
{{ $pay_trans->transmitted_data['clearing_bankbic'] }}</td>
{{ $pay_trans->transmitted_data['clearing_bankbic'] ?? '—' }}</td>
</tr>
<tr>
<td style="padding: 5px 10px 5px 0;"><strong>{{ __('payment.bank') }}:</strong>
</td>
<td style="padding: 5px 0;">
{{ $pay_trans->transmitted_data['clearing_bankname'] }}</td>
{{ $pay_trans->transmitted_data['clearing_bankname'] ?? '—' }}</td>
</tr>
<tr style="border-top: 1px solid #ffc107;">
<td style="padding: 10px 10px 5px 0;">

View file

@ -940,7 +940,43 @@
<div class="toggle-content">
<div class="row nomargin-bottom">
@if(($shopping_user->abo_options === 1) && (Util::getUserPaymentFor() >= 3))
@if(!empty($abo_payments_frozen))
<div class="col-lg-12 nomargin clearfix">
<div class="alert alert-warning mb-0">
<strong>{{ __('payment.abo_payments_frozen_title') }}</strong><br>
{{ __('payment.abo_payments_frozen') }}
</div>
</div>
@elseif(!empty($stripe_active))
{{-- Stripe Hosted Checkout: Zahlartenwahl passiert bei Stripe --}}
<div class="col-lg-12 nomargin clearfix">
<label class="mt-0 fs-14 fw-400 radio pull-left nomargin-top" style="width: 100%">
{!! Form::radio('payment_method', 'stripe', (!old('payment_method') ? '1' : '')) !!}
<i></i> <span class="weight-400"><span class="fa fa-check text-primary"></span> {{ __('payment.stripe_online') }}</span>
<img class="float-right" width="90" src="{{asset('images/payments-assets/stripe.png')}}" alt="Stripe">
</label>
<p class="fs-13 mb-1">{{ __('payment.stripe_online_methods') }}</p>
<p class="fs-12 text-muted mb-0">{{ __('payment.stripe_online_copy') }}</p>
</div>
@if(array_key_exists('VOR', $payment_methods_active) && in_array($payment_methods_active['VOR'], $payment_methods))
<div class="col-lg-12 nomargin clearfix">
<label class="mt-4 fs-14 fw-400 radio pull-left" style="width: 100%">
{!! Form::radio('payment_method', 'vor', '') !!}
<i></i> <span class="weight-400"><span class="fa fa-check text-primary"></span> {{ __('payment.prepayment') }}</span>
<img class="float-right" width="90" src="{{asset('images/payments-assets/vor.png')}}" alt="Vorkasse">
</label>
</div>
@endif
@if(array_key_exists('FNC', $payment_methods_active) && in_array($payment_methods_active['FNC'], $payment_methods))
<div class="col-lg-12 nomargin clearfix">
<label class="mt-4 fs-14 fw-400 radio pull-left" style="width: 100%">
{!! Form::radio('payment_method', 'fnc#MIV', '') !!}
<i></i> <span class="weight-400"><span class="fa fa-check text-primary"></span> {{ __('payment.purchase_on_account') }}</span>
<img class="float-right" width="90" src="{{asset('images/payments-assets/fnc.png')}}" alt="Rechnungskauf">
</label>
</div>
@endif
@elseif(($shopping_user->abo_options === 1) && (Util::getUserPaymentFor() >= 3))
@if(array_key_exists('SEPA', $payment_methods_active) && in_array($payment_methods_active['SEPA'], $payment_methods))
<div class="col-lg-12 nomargin clearfix">
<label class="mt-4 fs-14 fw-400 radio pull-left" style="width: 100%">
@ -1331,8 +1367,12 @@
</div>
@endif
@if(!empty($abo_payments_frozen))
<div class="alert alert-warning mt-4">{{ __('payment.abo_payments_frozen') }}</div>
@else
<button type="submit" class="btn btn-primary btn-lg btn-block size-18 btn-text-500 mt-4 faa-parent animated-hover"><i class="fa fa-mail-forward faa-horizontal"></i> {{ __('payment.buy_now') }}</button>
<hr><p class="text-center" style="line-height: 1.2em;"><em class="small text-center"> <i class="fa fa-lock"></i> {{ __('payment.buy_now_copy') }}</em></p>
@endif
</div>
</div>