Abo (A3): Status und Grund eines Abos sichtbar machen

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>
This commit is contained in:
Kevin 2026-09-16 10:24:18 +00:00
parent e8057f9f7b
commit d6efe56cd4
22 changed files with 977 additions and 15 deletions

View file

@ -5,6 +5,7 @@ namespace App\Services;
use App\Cron\UserMakeOrder;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Services\Abo\AboStatusExplainer;
use App\Services\Incentive\IncentiveTracker;
use App\Services\Payment\PaymentProviderManager;
use Illuminate\Support\Facades\DB;
@ -86,7 +87,7 @@ class AboRetryPaymentService
}
$this->logPaymentError($userAbo, $shoppingOrder, $response);
$this->markAboError($userAbo, $shoppingOrder);
$this->markAboError($userAbo, $shoppingOrder, AboStatusExplainer::paymentErrorMessage($response));
$paymentAttemptRecorded = true;
$this->sendPaymentErrorMail($userOrder, $shoppingOrder, $response);
@ -107,7 +108,7 @@ class AboRetryPaymentService
]);
if ($shoppingOrder && ! $paymentAttemptRecorded) {
$this->markAboError($userAbo, $shoppingOrder);
$this->markAboError($userAbo, $shoppingOrder, mb_substr('Exception: '.$e->getMessage(), 0, 255));
}
return [
@ -170,9 +171,9 @@ class AboRetryPaymentService
AboOneTimeService::purgeAfterExecution($userAbo);
}
private function markAboError(UserAbo $userAbo, mixed $shoppingOrder): void
private function markAboError(UserAbo $userAbo, mixed $shoppingOrder, ?string $errorMessage = null): void
{
DB::transaction(function () use ($userAbo, $shoppingOrder): void {
DB::transaction(function () use ($userAbo, $shoppingOrder, $errorMessage): void {
$userAbo->update([
'status' => 3,
'last_date' => now(),
@ -182,6 +183,7 @@ class AboRetryPaymentService
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 3,
'error_message' => $errorMessage,
'paid' => false,
]);
});