Neue Spalten user_abos.status_reason / paused_until und
user_abo_orders.error_message. Der Grund wird zentral am Model gepflegt
(saving-Event): Zahlungsfehler, Kuendigung/Deaktivierung mit Akteur aus dem
Formular-Kanal, Loeschen beim Zurueckkehren in den Lauf; die Pause setzt
paused_until und paused_by_*. Cron- und Retry-Zahlung schreiben den Fehlertext
des Anbieters an die Abo-Bestellung.
AboStatusExplainer uebersetzt das in einen Satz ("Pausiert bis ... vom Kunden
am ...", "Zahlung am ... fehlgeschlagen - Grund: ...", "Gekuendigt am ... vom
Berater"), Partial _status_explain in Abo-Detail (CRM, Portal, Admin),
Abo-Uebersicht, Kundenabo-Liste, Team-Kundenabos und Admin-Datatable; dort
neuer Filter "Zustand" (pausiert / Zahlung fehlgeschlagen). Backfill-Kommando
abo:backfill-status-reasons fuer Bestandsdaten, Fallback ueber das
Statusprotokoll und die Pausenfelder.
Sprachdateien de/en/es/fr, Tests in AboStatusExplainTest, Plan-Status.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
291 lines
16 KiB
PHP
291 lines
16 KiB
PHP
@extends('layouts.layout-2')
|
|
|
|
@section('content')
|
|
<h4 class="font-weight-bold py-2 mb-2 d-flex justify-content-between align-items-center w-100 ">
|
|
<div>
|
|
@if($view === 'ot') {{ __('navigation.customerabos') }} @endif
|
|
@if($view === 'me') {{ __('navigation.myabos') }} @endif
|
|
/ {{ __('navigation.overview') }}
|
|
</div>
|
|
</h4>
|
|
|
|
@if(isset($chartData))
|
|
@include('user.abo._abo_chart')
|
|
@endif
|
|
|
|
@if($view === 'ot' && isset($filterMonth))
|
|
<div class="card mb-4">
|
|
<div class="card-body py-3">
|
|
<form method="GET" action="{{ url()->current() }}" class="form-inline">
|
|
<label class="text-muted small mr-2 mb-0">{{ __('abo.filter_added_period') }}</label>
|
|
<select name="month" class="form-control form-control-sm mr-2 mb-1">
|
|
<option value="0">{{ __('tables.all') }}</option>
|
|
@foreach($chartMonths as $monthNum => $monthName)
|
|
<option value="{{ $monthNum }}" {{ (int) $filterMonth === (int) $monthNum ? 'selected' : '' }}>{{ $monthName }}</option>
|
|
@endforeach
|
|
</select>
|
|
<select name="year" class="form-control form-control-sm mr-2 mb-1">
|
|
@foreach($chartYears as $y)
|
|
<option value="{{ $y }}" {{ (int) $filterYear === (int) $y ? 'selected' : '' }}>{{ $y }}</option>
|
|
@endforeach
|
|
</select>
|
|
<button type="submit" class="btn btn-sm btn-secondary mb-1">{{ __('abo.filter') }}</button>
|
|
@if($filterMonth)
|
|
<a href="{{ url()->current() }}?year={{ $filterYear }}" class="btn btn-sm btn-outline-secondary ml-2 mb-1">{{ __('abo.reset') }}</a>
|
|
@endif
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if (Session::has('alert-success'))
|
|
<div class="alert alert-success">
|
|
{{ Session::get('alert-success') }}
|
|
</div>
|
|
@endif
|
|
@if (Session::has('alert-error'))
|
|
<div class="alert alert-danger">
|
|
{{ Session::get('alert-error') }}
|
|
</div>
|
|
@endif
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
@php
|
|
$abo_payment_links = $abo_payment_links ?? collect();
|
|
@endphp
|
|
|
|
@if($view === 'ot' && $abo_payment_links->isNotEmpty())
|
|
@foreach($abo_payment_links as $paymentLink)
|
|
<div class="card mb-4 border-warning">
|
|
<div class="card-body pb-2">
|
|
<div class="row">
|
|
<div class="col-sm-7">
|
|
<h5 class="mb-1">
|
|
<a href="{{ route('user_order_payment_links_detail', [$paymentLink->identifier]) }}">
|
|
{{ __('navigation.abo') }} <span class="text-muted">#{{ $paymentLink->identifier }}</span>
|
|
</a>
|
|
{!! \App\Services\OrderPaymentService::getTypeBadge($paymentLink) !!}
|
|
</h5>
|
|
</div>
|
|
<div class="col-sm-5 text-right">
|
|
<a href="{{ route('user_order_payment_links_detail', [$paymentLink->identifier]) }}" class="btn btn-sm btn-secondary">
|
|
<span class="fa fa-edit"></span>{{ __('abo.abo_details') }}
|
|
</a>
|
|
@if(\App\Services\OrderPaymentService::canDeleteInstance($paymentLink))
|
|
{!! Form::open(['action' => route('user_abos_payment_link_delete', [$paymentLink->identifier]), 'class' => 'd-inline-block ml-1']) !!}
|
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ __('confirm_delete') }}');">
|
|
<span class="fa fa-trash"></span> {{ __('delete') }}
|
|
</button>
|
|
{!! Form::close() !!}
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<hr class="m-0">
|
|
<div class="card-body pb-1">
|
|
@if(\App\Services\OrderPaymentService::isPaymentFailed($paymentLink))
|
|
<div class="alert alert-danger mb-3">
|
|
<strong>{{ __('abo.pending_payment_failed_title') }}</strong><br>
|
|
{{ __('abo.payment_link_failed_copy') }}
|
|
</div>
|
|
@elseif(\App\Services\OrderPaymentService::isPaymentProcessing($paymentLink))
|
|
<div class="alert alert-info mb-3">
|
|
<strong>{{ __('abo.pending_payment_processing_title') }}</strong><br>
|
|
{{ __('abo.payment_link_processing_copy') }}
|
|
</div>
|
|
@else
|
|
<div class="alert alert-warning mb-3">
|
|
<strong>{{ __('abo.payment_link_sent_title') }}</strong><br>
|
|
{{ __('abo.payment_link_sent_copy') }}
|
|
</div>
|
|
@endif
|
|
<div class="row">
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('tables.date') }}</div>
|
|
<div class="font-weight-bold">{{ $paymentLink->created_at->format('d.m.Y') }}</div>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('tables.status') }}</div>
|
|
{!! \App\Services\OrderPaymentService::getStatusBadge($paymentLink) !!}
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('tables.amount') }}</div>
|
|
{{ $paymentLink->getAmountFormatted() }} €
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('tables.email') }}</div>
|
|
{{ $paymentLink->shopping_data['billing_email'] ?? '-' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
@endif
|
|
|
|
@if((!$user_abos || $user_abos->isEmpty()) && $abo_payment_links->isEmpty())
|
|
<div class="card mb-4">
|
|
<div class="card-body pb-2">
|
|
<div class="row">
|
|
<div class="col-sm-12">
|
|
<h5>{{ __('abo.abo_delivery') }}</h5>
|
|
|
|
</div>
|
|
<hr class="m-0">
|
|
|
|
<div class="col-sm-12 mb-4">
|
|
<a href="{{ route('user_order_my_delivery', ['abo-me']) }}" class="btn btn-secondary rounded-pill">
|
|
<span class="ion ion-md-add"></span> {{ __('abo.add_new_abo') }} </a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@php
|
|
// Kundenabos laufen ueber die sortierbare Liste; als Karte bleiben nur
|
|
// die Ausnahmefaelle mit offener Erstzahlung, weil dort Aktionsbuttons
|
|
// haengen (Kundenwunsch 2026-09-07).
|
|
$cardAbos = $view === 'ot'
|
|
? collect($user_abos)->filter(fn ($abo) => $abo->isPendingInitialPayment())
|
|
: collect($user_abos);
|
|
$listAbos = $view === 'ot'
|
|
? collect($user_abos)->reject(fn ($abo) => $abo->isPendingInitialPayment())->values()
|
|
: collect();
|
|
@endphp
|
|
|
|
@foreach($cardAbos as $user_abo)
|
|
<div class="card mb-4">
|
|
@if($user_abo->isPendingInitialPayment())
|
|
<div class="card-body pb-0">
|
|
@if($user_abo->hasFailedInitialPayment())
|
|
<div class="alert alert-danger mb-3">
|
|
<strong>{{ __('abo.pending_payment_failed_title') }}</strong><br>
|
|
{{ __('abo.pending_payment_failed_copy') }}
|
|
{!! Form::open(['action' => route('user_abos_delete_pending', [$view, $user_abo->id]), 'class' => 'd-inline-block ml-2']) !!}
|
|
<button type="submit" class="btn btn-sm btn-danger">{{ __('abo.pending_delete_button') }}</button>
|
|
{!! Form::close() !!}
|
|
</div>
|
|
@elseif($user_abo->hasInitialPaymentStatus())
|
|
<div class="alert alert-info mb-3">
|
|
<strong>{{ __('abo.pending_payment_processing_title') }}</strong><br>
|
|
{{ __('abo.pending_payment_processing_copy') }}
|
|
</div>
|
|
@else
|
|
<div class="alert alert-warning mb-3">
|
|
<strong>{{ __('abo.pending_payment_waiting_title') }}</strong><br>
|
|
{{ __('abo.pending_payment_waiting_copy') }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
<div class="card-body pb-2">
|
|
<div class="row">
|
|
<div class="col-sm-6">
|
|
<h5 class="mb-1">
|
|
<a href="{{ route('user_abos_detail', [$view, $user_abo->id]) }}">
|
|
{{ __('navigation.abo') }} <span class="text-muted">{{ '#'.$user_abo->payone_userid }}</span>
|
|
</a>
|
|
</h5>
|
|
</div>
|
|
<div class="col-sm-6 text-right">
|
|
<a href="{{ route('user_abos_detail', [$view, $user_abo->id]) }}" class="btn btn-sm btn-secondary float-right">
|
|
<span class="fa fa-edit"></span>{{ __('abo.abo_details') }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<hr class="m-0">
|
|
<div class="card-body pb-1">
|
|
<div class="row">
|
|
<div class="col-md-3 mb-2">
|
|
<div class="text-muted small">{{ __('tables.start_date') }}</div>
|
|
<div class="font-weight-bold">{{ $user_abo->start_date }}</div>
|
|
</div>
|
|
<div class="col-md-3 mb-2">
|
|
<div class="text-muted small">{{ __('tables.next_date') }} </div>
|
|
<div class="font-weight-bold">{{ $user_abo->next_date }}</div>
|
|
</div>
|
|
<div class="col-md-3 mb-2">
|
|
<div class="text-muted small">{{ __('tables.abo_delivery_day') }}</div>
|
|
<div class="font-weight-bold">{{ \App\Services\HTMLHelper::getAboStrLang($user_abo->abo_interval) }}</div>
|
|
</div>
|
|
<div class="col-md-3 mb-2">
|
|
<div class="text-muted small">{{ __('tables.last_date') }}</div>
|
|
<div class="font-weight-bold">{{ $user_abo->last_date }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<hr class="m-0">
|
|
<div class="card-body pb-1">
|
|
<div class="row">
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('tables.status') }} / {{ __('tables.active') }}</div>
|
|
{!! get_active_badge($user_abo->active) !!}
|
|
@include('admin.abo._status_explain', ['abo' => $user_abo])
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('tables.abo_delivery') }}</div>
|
|
{{ $user_abo->getCountOrders() }}
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
@if($view === 'ot')
|
|
<div class="text-muted small">{{ __('navigation.points') }}</div>
|
|
<strong class="text-primary">{{ $user_abo->getFormattedTotalPoints() }} Pkt.</strong>
|
|
@else
|
|
<div class="text-muted small">{{ __('tables.amount') }}</div>
|
|
{{ $user_abo->getFormattedAmount() }} €
|
|
@endif
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('tables.payment') }}</div>
|
|
{{ $user_abo->getPaymentType() }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@if($view === 'ot')
|
|
<hr class="m-0">
|
|
<div class="card-body pb-1">
|
|
<div class="row">
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('First name') }}</div>
|
|
@if($user_abo->shopping_user)
|
|
{{ $user_abo->shopping_user->billing_firstname }}
|
|
@endif
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('Last name') }}</div>
|
|
@if($user_abo->shopping_user)
|
|
{{ $user_abo->shopping_user->billing_lastname }}
|
|
@endif
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('E-Mail') }}</div>
|
|
@if($user_abo->shopping_user)
|
|
{{ $user_abo->shopping_user->billing_email }}
|
|
@endif
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<div class="text-muted small">{{ __('tables.customer') }}</div>
|
|
@if($user_abo->shopping_user)
|
|
<a class="btn btn-sm btn-secondary" href="{{ route('user_customer_detail', $user_abo->shopping_user->id) }}"><i class="fa fa-edit"></i></a>
|
|
@endif
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
|
|
@if($view === 'ot' && $listAbos->isNotEmpty())
|
|
@include('user.abo._customer_abo_list', [
|
|
'listAbos' => $listAbos,
|
|
'customerAboForecast' => $customerAboForecast ?? null,
|
|
'customerAboBilled' => $customerAboBilled ?? null,
|
|
'customerAboBilledTotal' => $customerAboBilledTotal ?? null,
|
|
])
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endsection
|