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:
Kevin 2026-07-08 15:56:19 +00:00
parent 63c36555a5
commit 671022a28d
36 changed files with 2366 additions and 26 deletions

View file

@ -0,0 +1,107 @@
<div class="card">
<div class="card-body">
<h5 class="font-weight-bold">{{ __('abo.update_card_title') }}</h5>
@if(app(\App\Services\Payment\PaymentProviderManager::class)->aboPaymentsFrozen())
<div class="alert alert-warning mb-0">
<strong>{{ __('payment.abo_payments_frozen_title') }}</strong><br>
{{ __('payment.abo_payments_frozen') }}
</div>
<a href="{{ $backUrl ?? route('portal.my_subscriptions') }}" class="btn btn-default mt-3">{{ __('abo.back') }}</a>
@else
<p class="text-muted">{{ __('abo.card_update_intro') }}</p>
@if (isset($errors) && $errors->any())
<div class="alert alert-danger p-2">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if (Session::has('alert-error'))
<div class="alert alert-danger p-2">{{ Session::get('alert-error') }}</div>
@endif
@if ($user_abo->maskedCard())
<div class="alert alert-info p-2">
<i class="ion ion-md-card mr-2" style="font-size: 1.2rem; line-height: 1rem;"></i>
<span>{{ __('abo.current_card') }}: <strong>{{ $user_abo->maskedCard() }}</strong></span>
</div>
@endif
<form method="POST" action="{{ $formAction ?? route('portal_abos_payment_method_update', [$user_abo->id]) }}">
@csrf
<div class="form-group">
<label for="cc_cardtype">{{ __('payment.credit_card') }}*</label>
<select id="cc_cardtype" name="cc_cardtype" class="form-control custom-select">
<option value="V" @if (old('cc_cardtype') === 'V') selected @endif>{{ __('payment.VISA') }}
</option>
<option value="M" @if (old('cc_cardtype') === 'M') selected @endif>
{{ __('payment.Mastercard') }}</option>
</select>
</div>
<div class="row">
<div class="col-md-12">
<label>{{ __('payment.owner') }}*</label>
</div>
<div class="form-group col-md-6">
<input type="text" name="cc_cardholder_first" id="cc_cardholder_first" class="form-control"
value="{{ old('cc_cardholder_first') }}" placeholder="{{ __('First name') }}">
</div>
<div class="form-group col-md-6">
<input type="text" name="cc_cardholder_last" id="cc_cardholder_last" class="form-control"
value="{{ old('cc_cardholder_last') }}" placeholder="{{ __('Last name') }}">
</div>
</div>
<div class="row">
<div class="form-group col-md-12">
<label>{{ __('payment.credit_card_number') }}*</label>
<input type="text" name="cc_cardpan" id="cc_cardpan" class="form-control"
value="{{ old('cc_cardpan') }}" placeholder="{{ __('payment.credit_card_number') }}"
inputmode="numeric" autocomplete="off">
</div>
<div class="form-group col-md-6">
<label for="cc_cardexpiremonth">{{ __('payment.valid') }}
{{ __('validation.attributes.month') }}*</label>
<select id="cc_cardexpiremonth" name="cc_cardexpiremonth" class="form-control custom-select">
@foreach (range(1, 12) as $month)
@php($month = sprintf('%02d', $month))
<option value="{{ $month }}" @if (old('cc_cardexpiremonth') === $month) selected @endif>
{{ $month }}</option>
@endforeach
</select>
</div>
<div class="form-group col-md-6">
<label for="cc_cardexpireyear">{{ __('payment.valid') }}
{{ __('validation.attributes.year') }}*</label>
<select id="cc_cardexpireyear" name="cc_cardexpireyear" class="form-control custom-select">
@foreach (range(date('Y'), date('Y', strtotime('+10 years'))) as $year)
<option value="{{ $year }}" @if (old('cc_cardexpireyear') === (string) $year) selected @endif>
{{ $year }}</option>
@endforeach
</select>
</div>
<div class="form-group col-md-6">
<label for="cc_cardcvc2">{{ __('payment.verification_no') }}*</label>
<input type="text" name="cc_cardcvc2" id="cc_cardcvc2" class="form-control"
value="{{ old('cc_cardcvc2') }}" placeholder="CVC*" inputmode="numeric" autocomplete="off">
</div>
</div>
<div class="alert alert-warning d-flex align-items-center" role="alert">
<i class="ion ion-md-lock mr-2" style="font-size: 1.5rem; line-height: 1;"></i>
<span>{{ __('abo.card_security_notice') }}</span>
</div>
<button type="submit" class="btn btn-primary mt-2">{{ __('abo.update_payment_method') }}</button>
<a href="{{ $backUrl ?? route('portal.my_subscriptions') }}" class="btn btn-default mt-2">{{ __('abo.back') }}</a>
</form>
@endif
</div>
</div>

View file

@ -0,0 +1,20 @@
@extends('portal.layouts.public')
@section('content')
<div class="text-center" style="max-width: 640px; margin: 0 auto;">
@if ($success)
<i class="ion ion-md-checkmark-circle text-success" style="font-size: 3rem;"></i>
<h4 class="font-weight-bold mt-3">{{ __('abo.card_update_done_success_title') }}</h4>
<p class="text-muted">{{ __('abo.card_update_done_success_copy') }}</p>
@else
<i class="ion ion-md-close-circle text-danger" style="font-size: 3rem;"></i>
<h4 class="font-weight-bold mt-3">{{ __('abo.card_update_done_failed_title') }}</h4>
<p class="text-muted">{{ __('abo.card_update_done_failed_copy') }}</p>
@endif
<p class="text-muted mt-3">{{ __('abo.card_update_portal_hint') }}</p>
<a href="{{ \App\Services\Util::getMyMivitaPortalUrl() }}" class="btn btn-outline-secondary btn-sm">
<i class="ion ion-md-home mr-1"></i>{{ __('abo.card_update_to_portal') }}
</a>
</div>
@endsection

View file

@ -0,0 +1,20 @@
@extends('portal.layouts.public')
@section('content')
<div style="max-width: 640px; margin: 0 auto;">
<h4 class="font-weight-bold mb-3">{{ __('abo.update_card_title') }}</h4>
@include('portal.abo._payment_method_form', [
'user_abo' => $user_abo,
'formAction' => route('abo_card_update_store', [$token]),
'backUrl' => \App\Services\Util::getMyMivitaPortalUrl(),
])
<div class="alert alert-light border mt-3 text-center">
<p class="mb-2 text-muted">{{ __('abo.card_update_portal_hint') }}</p>
<a href="{{ \App\Services\Util::getMyMivitaPortalUrl() }}" class="btn btn-secondary btn-sm">
<i class="ion ion-md-home mr-1"></i>{{ __('abo.card_update_to_portal') }}
</a>
</div>
</div>
@endsection

View file

@ -0,0 +1,12 @@
@extends('portal.layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-2 mb-2">
{{ __('abo.update_card_title') }}
<span class="text-muted">{{ '#' . $user_abo->payone_userid }}</span>
</h4>
@include('portal.abo._payment_method_form', ['user_abo' => $user_abo])
@endsection