mivita/resources/views/user/abo/detail.blade.php
Kevin ce4750fd1a 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>
2026-07-09 14:45:17 +00:00

208 lines
8.3 KiB
PHP

@extends('layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-2 mb-2">
<a href="{{ route('user_abos', [$view]) }}" class="btn btn-sm btn-default float-right">{{ __('back') }}</a>
<div>
@if ($view === 'ot')
{{ __('navigation.customerabo') }}
@endif
@if ($view === 'me')
{{ __('navigation.myabo') }}
@endif
<span class="text-muted">{{ '#' . $user_abo->payone_userid }}</span>
</div>
</h4>
@if (Session::has('alert-success'))
<div class="col-sm-12">
<div class="alert alert-success p-2 mt-2">
<ul>
<li>{{ Session::get('alert-success') }}</li>
</ul>
</div>
</div>
@endif
@if (Session::has('alert-error'))
<div class="col-sm-12">
<div class="alert alert-danger p-2 mt-2">
<ul>
<li>{{ Session::get('alert-error') }}</li>
</ul>
</div>
</div>
@endif
@if (Session::has('alert-warning'))
<div class="col-sm-12">
<div class="alert alert-warning p-2 mt-2">
<ul>
<li>{{ Session::get('alert-warning') }}</li>
</ul>
</div>
</div>
@endif
@if ($view === 'me' && $user_abo->status === 3 && $user_abo->active)
<div class="card mt-3 mb-3 border-info">
<div class="card-body">
<h6 class="font-weight-bold text-info">{{ __('abo.pay_now_title') }}</h6>
<p class="mb-2">{{ __('abo.pay_now_copy') }}</p>
<form method="POST" action="{{ route('user_abos_pay_now', [$view, $user_abo->id]) }}">
@csrf
<button type="submit" class="btn btn-info">
<i class="ion ion-md-card mr-1"></i>{{ __('abo.pay_now_button') }}
</button>
</form>
</div>
</div>
@endif
<div class="card">
@include('admin.abo._detail')
</div>
<div class="card mt-3">
@include('admin.abo._detail_abo_info')
</div>
<div class="card mt-3">
@include('admin.customer._customer_detail', ['shopping_user' => $customer_detail])
</div>
@php
// Selbstbedienung: Add-only (Mindestlaufzeit) und Change-Lock richten sich nach der
// Abo-Sicht ($view), NICHT nach Admin-Status. So gelten für VIP/Admin auf der eigenen
// Abo-Seite dieselben Regeln wie für jeden Berater (kein Admin-Bypass).
$addOnlyMode = App\Services\AboHelper::isAddOnlyMode($user_abo, $view);
$aboChangeLocked = App\Services\AboHelper::isAboChangeLocked($user_abo, $view);
@endphp
{!! Form::open([
'action' => route('user_abos_update', [$view, $user_abo->id]),
'class' => 'form-horizontal',
'id' => 'cart-order-form',
'data-add-only-mode' => $addOnlyMode ? '1' : '0',
]) !!}
<input type="hidden" name="is_for" value="{{ $user_abo->is_for }}">
@php
$oneTimeWindowOpen = isset($one_time_window_open) ? $one_time_window_open : false;
$summary = isset($summary) ? $summary : null;
$cartInstance = \App\Services\AboOrderCart::INSTANCE;
@endphp
@if ($oneTimeWindowOpen)
<div class="row mt-3">
<div class="col-lg-8">
<div class="card" id="onetime_order_card"
data-onetime-route="{{ route('user_abos_onetime', [$view, $user_abo->id]) }}">
@include('admin.abo._order_onetime', ['summary' => $summary])
</div>
<div class="card mt-3">
@include('admin.abo._order_abo', [
'add_only_mode' => $addOnlyMode,
'split_mode' => true,
'summary' => $summary,
'change_locked' => $aboChangeLocked,
])
</div>
<div id="holder_html_view_comp_product">
@if ($comp_products && Yard::instance($cartInstance)->getNumComp() > 0)
@include('user.order.comp_product', [
'cart_instance' => $cartInstance,
'base_comp_count' => $base_comp_count ?? Yard::instance($cartInstance)->getNumComp(),
])
@endif
</div>
</div>
<div class="col-lg-4" id="combined_summary_col">
<div class="card js-abo-sticky" id="combined_summary_card">
@include('admin.abo._order_combined_summary', ['summary' => $summary])
</div>
</div>
</div>
@else
<div class="card mt-3">
@include('admin.abo._order_abo', [
'add_only_mode' => $addOnlyMode,
'change_locked' => $aboChangeLocked,
])
</div>
<div id="holder_html_view_comp_product">
@if ($comp_products && Yard::instance($cartInstance)->getNumComp() > 0)
@include('user.order.comp_product', [
'cart_instance' => $cartInstance,
'base_comp_count' => $base_comp_count ?? Yard::instance($cartInstance)->getNumComp(),
])
@endif
</div>
@endif
{{ Form::close() }}
<div class="card mt-3 mb-3">
@include('admin.abo._executions')
</div>
@php($canStartPaymentChange = app(\App\Services\AboPaymentMethodService::class)->canStartChange($user_abo))
@if ($user_abo->isCreditCard() || $canStartPaymentChange)
<div class="card mt-3 mb-3">
<div class="card-body">
<h6 class="font-weight-bold">{{ __('abo.payment_method') }}</h6>
<p class="mb-2">
{{ $user_abo->getPaymentType() }}@if ($user_abo->maskedCard())
: {{ $user_abo->maskedCard() }}
@endif
</p>
@if ($view === 'me' && $canStartPaymentChange)
<a href="{{ route('user_abos_payment_method_edit', [$view, $user_abo->id]) }}"
class="btn btn-sm btn-primary">
{{ __('abo.update_payment_method') }}
</a>
@endif
@if ($view === 'ot' && $canStartPaymentChange)
<form method="POST" action="{{ route('user_abos_payment_method_send_link', [$view, $user_abo->id]) }}"
class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="ion ion-md-mail mr-1"></i>{{ __('abo.card_update_link_button') }}
</button>
</form>
@endif
@if ($user_abo->card_changes->isNotEmpty())
<div class="mt-3">
<small class="text-muted d-block mb-1">{{ __('abo.card_change_history') }}</small>
<ul class="list-unstyled mb-0 small">
@foreach ($user_abo->card_changes->take(5) as $change)
<li>
<i class="ion ion-md-time text-muted mr-1"></i>
{{ $change->created_at->format('d.m.Y H:i') }}@if ($change->masked_card)
— {{ $change->masked_card }}
@endif
</li>
@endforeach
</ul>
</div>
@endif
</div>
</div>
@endif
<a href="{{ route('user_abos', [$view]) }}" class="btn btn-sm btn-default float-right">{{ __('back') }}</a>
@include('admin.abo._confirm_add_modal')
@endsection
@section('scripts')
<script src="{{ asset('/js/iq-modal-cart.js') }}?v=1{{ get_file_last_time('/js/iq-modal-cart.js') }}"></script>
<script src="{{ asset('/js/iq-abo-onetime.js') }}?v=1{{ get_file_last_time('/js/iq-abo-onetime.js') }}"></script>
<script type="application/javascript">
var iqModalCart = IqModalCart.init();
var iqAboOneTime = IqAboOneTime.init();
$( document ).ready(function() {
});
</script>
@endsection