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>
128 lines
4.6 KiB
PHP
128 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Web;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Abo\UpdateAboCreditCardRequest;
|
|
use App\Models\AboCardUpdateLink;
|
|
use App\Services\AboPaymentMethodService;
|
|
|
|
/**
|
|
* Login-free credit-card update via a tokenized link (sent by a consultant to a customer).
|
|
* Card data is entered exclusively by the customer here; routes are rate-limited. The token
|
|
* is looked up by its SHA-256 hash and is single-use + TTL-bound.
|
|
*/
|
|
class AboCardUpdateController extends Controller
|
|
{
|
|
public function show(string $token)
|
|
{
|
|
$link = $this->resolveRedeemableLink($token);
|
|
|
|
return view('portal.abo.card_update_link', [
|
|
'user_abo' => $link->user_abo,
|
|
'token' => $token,
|
|
]);
|
|
}
|
|
|
|
public function store(UpdateAboCreditCardRequest $request, string $token, AboPaymentMethodService $service)
|
|
{
|
|
$link = $this->resolveRedeemableLink($token);
|
|
$user_abo = $link->user_abo;
|
|
|
|
$returnUrls = [
|
|
'successurl' => route('abo_card_update_return', [$token, 'success']),
|
|
'errorurl' => route('abo_card_update_return', [$token, 'error']),
|
|
'backurl' => route('abo_card_update_return', [$token, 'cancel']),
|
|
];
|
|
|
|
$result = $service->updateCreditCard($user_abo, $request->toDto(), $returnUrls, 'link');
|
|
|
|
if ($result->isApproved()) {
|
|
$link->forceFill(['used_at' => now()])->save();
|
|
|
|
return view('portal.abo.card_update_done', ['success' => true]);
|
|
}
|
|
|
|
if ($result->isRedirect()) {
|
|
session(['abo_card_update.'.$user_abo->id => [
|
|
'carddata' => $result->carddata,
|
|
'userid' => $result->userid,
|
|
]]);
|
|
|
|
return redirect()->away($result->redirectUrl);
|
|
}
|
|
|
|
return redirect(route('abo_card_update_show', [$token]))
|
|
->withErrors(['cc_cardpan' => $result->errorMessage ?? __('abo.card_update_failed')]);
|
|
}
|
|
|
|
/**
|
|
* Startet den Stripe-Setup-Flow "Zahlungsart ändern" (Plan S5) über den
|
|
* tokenisierten Link: Redirect zur Hosted-Seite, freie Zahlartenwahl.
|
|
*/
|
|
public function startStripe(string $token, AboPaymentMethodService $service)
|
|
{
|
|
$link = $this->resolveRedeemableLink($token);
|
|
abort_unless($service->usesStripeChange(), 404);
|
|
|
|
$result = $service->beginStripeChange(
|
|
$link->user_abo,
|
|
route('abo_card_update_return', [$token, 'success']).'?session_id={CHECKOUT_SESSION_ID}',
|
|
route('abo_card_update_return', [$token, 'cancel']),
|
|
'link',
|
|
);
|
|
|
|
if ($result->isRedirect()) {
|
|
return redirect()->away($result->redirectUrl);
|
|
}
|
|
|
|
return redirect(route('abo_card_update_show', [$token]))
|
|
->withErrors(['cc_cardpan' => $result->errorMessage ?? __('abo.payment_method_change_failed')]);
|
|
}
|
|
|
|
public function return(string $token, string $status, AboPaymentMethodService $service)
|
|
{
|
|
$link = $this->resolveRedeemableLink($token);
|
|
$user_abo = $link->user_abo;
|
|
|
|
// Stripe-Setup-Return (Plan S5): Session prüfen, Zahlungsart verankern, Link entwerten.
|
|
if ($status === 'success' && ($sessionId = request('session_id'))) {
|
|
if ($service->finalizeStripeChangeFromSession($user_abo, (string) $sessionId, 'link')) {
|
|
$link->forceFill(['used_at' => now()])->save();
|
|
|
|
return view('portal.abo.card_update_done', ['success' => true]);
|
|
}
|
|
|
|
return view('portal.abo.card_update_done', ['success' => false]);
|
|
}
|
|
|
|
$stash = session()->pull('abo_card_update.'.$user_abo->id);
|
|
|
|
if ($status === 'success' && is_array($stash)) {
|
|
$service->applyNewCard($user_abo, $stash['carddata'], (int) ($stash['userid'] ?? 0), 'link');
|
|
$link->forceFill(['used_at' => now()])->save();
|
|
|
|
return view('portal.abo.card_update_done', ['success' => true]);
|
|
}
|
|
|
|
$user_abo->forceFill(['card_update_pending_at' => null])->save();
|
|
|
|
return view('portal.abo.card_update_done', ['success' => false]);
|
|
}
|
|
|
|
private function resolveRedeemableLink(string $token): AboCardUpdateLink
|
|
{
|
|
$link = AboCardUpdateLink::where('token_hash', AboCardUpdateLink::hashToken($token))->first();
|
|
|
|
if (! $link || ! $link->isRedeemable()) {
|
|
abort(410, __('abo.card_update_link_invalid'));
|
|
}
|
|
|
|
$abo = $link->user_abo;
|
|
if (! $abo || ! app(AboPaymentMethodService::class)->canStartChange($abo)) {
|
|
abort(410, __('abo.card_update_link_invalid'));
|
|
}
|
|
|
|
return $link;
|
|
}
|
|
}
|