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>
208 lines
8.3 KiB
PHP
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
|