mivita/app/Http/Controllers/Web/AboCardUpdateController.php
Kevin 671022a28d Abo: Kreditkarte aktualisieren (CRM, Portal, Berater-Link) + Admin-Abo-Löschung
- Karten-Update ohne Abbuchung via PAYONE creditcardcheck/pseudocardpan
  (AboPaymentMethodService, CardUpdateResult/CreditCardCheckData-DTOs)
- Eingabe nur durch Abo-Inhaber: CRM- und Portal-Formular + zeitlich
  begrenzter Berater→Kunde-Link (AboCardUpdateLink + Mail)
- Änderungshistorie in user_abo_card_changes, Anzeige im Admin-Abo-Detail
- Admin: Abo löschen (destroy) inkl. Test
- Portal-Fix: myOrderCreate wirft 404 statt TypeError bei ungültiger ID
- VIP-Sichtbarkeitstests für Einmalartikel-Fenster ergänzt
- Enthält bereits die Freeze-Guards für den Stripe-Notbetrieb (Folge-Commit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:56:19 +00:00

93 lines
3.1 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')]);
}
public function return(string $token, string $status, AboPaymentMethodService $service)
{
$link = $this->resolveRedeemableLink($token);
$user_abo = $link->user_abo;
$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 || ! $abo->canUpdatePaymentMethod()) {
abort(410, __('abo.card_update_link_invalid'));
}
return $link;
}
}