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>
This commit is contained in:
parent
83ef5f1e52
commit
1108381988
36 changed files with 2366 additions and 26 deletions
|
|
@ -9,7 +9,9 @@ use App\Services\AboExternalCompletionService;
|
|||
use App\Services\AboItemHistoryService;
|
||||
use App\Services\AboOrderCart;
|
||||
use App\Services\AboRetryPaymentService;
|
||||
use App\Services\HTMLHelper;
|
||||
use App\Services\Shop;
|
||||
use Carbon\Carbon;
|
||||
use Request;
|
||||
|
||||
class AboController extends Controller
|
||||
|
|
@ -66,7 +68,7 @@ class AboController extends Controller
|
|||
{
|
||||
$months = [];
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$months[$month] = \Carbon\Carbon::create(null, $month, 1)->locale(app()->getLocale())->translatedFormat('F');
|
||||
$months[$month] = Carbon::create(null, $month, 1)->locale(app()->getLocale())->translatedFormat('F');
|
||||
}
|
||||
|
||||
return $months;
|
||||
|
|
@ -164,6 +166,16 @@ class AboController extends Controller
|
|||
return redirect(route('admin_abos_detail', [$id]));
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$user_abo->delete();
|
||||
|
||||
\Session()->flash('alert-success', __('abo.admin_delete_success'));
|
||||
|
||||
return redirect(route('admin_abos'));
|
||||
}
|
||||
|
||||
public function datatable()
|
||||
{
|
||||
|
||||
|
|
@ -220,7 +232,7 @@ class AboController extends Controller
|
|||
return $user_abo->next_date;
|
||||
})
|
||||
->addColumn('abo_interval', function (UserAbo $user_abo) {
|
||||
return \App\Services\HTMLHelper::getAboStrLang($user_abo->abo_interval);
|
||||
return HTMLHelper::getAboStrLang($user_abo->abo_interval);
|
||||
})
|
||||
->addColumn('status', function (UserAbo $user_abo) {
|
||||
return $user_abo->getStatusFormated();
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ class OrderController extends Controller
|
|||
/**
|
||||
* Bestellung erneut in den Warenkorb legen.
|
||||
*/
|
||||
public function myOrderCreate(int $id)
|
||||
public function myOrderCreate($id)
|
||||
{
|
||||
$user = Auth::guard('customers')->user();
|
||||
$shopping_order = ShoppingOrder::with('member.shop')->findOrFail($id);
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@ use App\Models\UserAboOneTimeItem;
|
|||
use App\Repositories\AboRepository;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\AboItemHistoryService;
|
||||
use App\Services\AboOneTimeService;
|
||||
use App\Services\AboOrderCart;
|
||||
use App\Services\AboPaymentMethodService;
|
||||
use App\Services\AboRecoveryCheckoutService;
|
||||
use App\Services\HTMLHelper;
|
||||
use App\Services\OrderPaymentService;
|
||||
use App\Services\Payment\PaymentProviderManager;
|
||||
use App\Services\Shop;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Request;
|
||||
use Yard;
|
||||
|
||||
|
|
@ -64,8 +68,8 @@ class AboController extends Controller
|
|||
}
|
||||
|
||||
if ($view === 'ot') {
|
||||
$selectedYear = (int) \Request::get('year', now()->year);
|
||||
$selectedMonth = (int) \Request::get('month', 0);
|
||||
$selectedYear = (int) Request::get('year', now()->year);
|
||||
$selectedMonth = (int) Request::get('month', 0);
|
||||
$baseQuery = UserAbo::where('member_id', \Auth::user()->id)
|
||||
->where('status', '>=', 1)
|
||||
->where('is_for', 'ot');
|
||||
|
|
@ -83,8 +87,8 @@ class AboController extends Controller
|
|||
'isAdmin' => false,
|
||||
'chartData' => AboHelper::getMonthlyAboCounts($baseQuery, $selectedYear, 'ot', \Auth::user()->id),
|
||||
'chartYear' => $selectedYear,
|
||||
'chartYears' => \App\Services\HTMLHelper::getYearRange(2026),
|
||||
'chartMonths' => \App\Services\HTMLHelper::getTransMonths(),
|
||||
'chartYears' => HTMLHelper::getYearRange(2026),
|
||||
'chartMonths' => HTMLHelper::getTransMonths(),
|
||||
'filterMonth' => $selectedMonth,
|
||||
'filterYear' => $selectedYear,
|
||||
]);
|
||||
|
|
@ -168,8 +172,8 @@ class AboController extends Controller
|
|||
|
||||
if ($changeLocked) {
|
||||
$message = __('abo.error_change_locked', [
|
||||
'days' => (int) now()->diffInDays(\Carbon\Carbon::parse($user_abo->next_date), false),
|
||||
'lock' => \App\Repositories\AboRepository::getLockDaysChange(),
|
||||
'days' => (int) now()->diffInDays(Carbon::parse($user_abo->next_date), false),
|
||||
'lock' => AboRepository::getLockDaysChange(),
|
||||
]);
|
||||
}
|
||||
// addProduct
|
||||
|
|
@ -322,7 +326,7 @@ class AboController extends Controller
|
|||
}
|
||||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
$message = (new \App\Services\AboOneTimeService)->handleAction($user_abo, $request->validated());
|
||||
$message = (new AboOneTimeService)->handleAction($user_abo, $request->validated());
|
||||
|
||||
AboOrderCart::initYard($user_abo);
|
||||
AboOrderCart::makeOrderYard($user_abo);
|
||||
|
|
@ -719,6 +723,12 @@ class AboController extends Controller
|
|||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPermissions($view, $user_abo);
|
||||
|
||||
if (app(PaymentProviderManager::class)->aboPaymentsFrozen()) {
|
||||
\Session()->flash('alert-error', __('payment.abo_payments_frozen'));
|
||||
|
||||
return redirect(route('user_abos_detail', [$view, $user_abo->id]));
|
||||
}
|
||||
|
||||
if ($view !== 'ot' || $user_abo->is_for !== 'ot' || ! $user_abo->isCreditCard() || ! $user_abo->canUpdatePaymentMethod()) {
|
||||
\Session()->flash('alert-error', __('abo.card_update_link_not_allowed'));
|
||||
|
||||
|
|
|
|||
93
app/Http/Controllers/Web/AboCardUpdateController.php
Normal file
93
app/Http/Controllers/Web/AboCardUpdateController.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue