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

@ -650,7 +650,7 @@ class AboController extends Controller
$this->checkPermissions($view, $user_abo);
$this->assertOwnerCardEntry($view, $user_abo);
if (! $user_abo->canUpdatePaymentMethod()) {
if (! app(AboPaymentMethodService::class)->canStartChange($user_abo)) {
\Session()->flash('alert-error', __('abo.card_update_not_allowed'));
return redirect(route('user_abos_detail', [$view, $user_abo->id]));
@ -693,12 +693,51 @@ class AboController extends Controller
return redirect(route('user_abos_payment_method_edit', [$view, $user_abo->id]))->withInput();
}
/**
* Startet den Stripe-Setup-Flow "Zahlungsart ändern" (Plan S5): Redirect
* zur Hosted-Seite, freie Zahlartenwahl, keine Abbuchung. Gilt auch für
* PAYONE-Bestandsabos (Umzug auf Stripe bei Erfolg).
*/
public function startStripePaymentMethodChange($view, $id, AboPaymentMethodService $service)
{
$user_abo = UserAbo::findOrFail($id);
$this->checkPermissions($view, $user_abo);
$this->assertOwnerCardEntry($view, $user_abo);
abort_unless($service->usesStripeChange(), 404);
$result = $service->beginStripeChange(
$user_abo,
route('user_abos_payment_method_return', [$view, $user_abo->id, 'success']).'?session_id={CHECKOUT_SESSION_ID}',
route('user_abos_payment_method_return', [$view, $user_abo->id, 'cancel']),
'crm',
);
if ($result->isRedirect()) {
return redirect()->away($result->redirectUrl);
}
\Session()->flash('alert-error', $result->errorMessage ?? __('abo.payment_method_change_failed'));
return redirect(route('user_abos_payment_method_edit', [$view, $user_abo->id]));
}
public function paymentMethodReturn($view, $id, $status, AboPaymentMethodService $service)
{
$user_abo = UserAbo::findOrFail($id);
$this->checkPermissions($view, $user_abo);
$this->assertOwnerCardEntry($view, $user_abo);
// Stripe-Setup-Return (Plan S5): Session prüfen und Zahlungsart verankern.
if ($status === 'success' && ($sessionId = request('session_id'))) {
if ($service->finalizeStripeChangeFromSession($user_abo, (string) $sessionId, 'crm')) {
\Session()->flash('alert-success', __('abo.payment_method_change_success'));
} else {
\Session()->flash('alert-error', __('abo.payment_method_change_failed'));
}
return redirect(route('user_abos_detail', [$view, $user_abo->id]));
}
$stash = session()->pull('abo_card_update.'.$user_abo->id);
if ($status === 'success' && is_array($stash)) {
@ -729,7 +768,11 @@ class AboController extends Controller
return redirect(route('user_abos_detail', [$view, $user_abo->id]));
}
if ($view !== 'ot' || $user_abo->is_for !== 'ot' || ! $user_abo->isCreditCard() || ! $user_abo->canUpdatePaymentMethod()) {
// Stripe-Setup-Flow: Link ist für jede Zahlart möglich (freie Wahl bei
// Stripe); die "nur CC"-Einschränkung gilt nur für den PAYONE-Legacy-Flow.
$requiresCreditCard = ! app(AboPaymentMethodService::class)->usesStripeChange();
if ($view !== 'ot' || $user_abo->is_for !== 'ot' || ($requiresCreditCard && ! $user_abo->isCreditCard()) || ! app(AboPaymentMethodService::class)->canStartChange($user_abo)) {
\Session()->flash('alert-error', __('abo.card_update_link_not_allowed'));
return redirect(route('user_abos_detail', [$view, $user_abo->id]));