Abo-Recovery: fehlgeschlagene Folgezahlung im Checkout nachholen

- Pay-now-Flow: bei fehlgeschlagenem Abo (Status 3) begleicht der Inhaber die
  offene Rechnung im Standard-Checkout mit frei waehlbarer, abo-faehiger Zahlart
  (nur eigenes Abo: Berater me-View + Kundenportal), payNow-Controller + Routen
- AboRecoveryCheckoutService baut die ShoppingInstance aus den Abo-Artikeln;
  neue Spalte shopping_orders.recovery_user_abo_id verknuepft Bestellung<->Abo
- Bei Erfolg: AboHelper::advanceAboAfterRecoveryPayment schreibt das Abo fort
  (Status->2, next_date) und uebernimmt die gewaehlte Zahlart als neues Token
  fuer kuenftige Abbuchungen
- Neue Pay-now-Fehlermail (AboPaymentFailedMail) ersetzt die generische
  Fehlermail im Abo-Cron; Link fuehrt auf die Abo-Detailseite (CRM bzw. Portal)
- Retry-/"extern erledigt"-Buttons nur noch fuer Admins; tote user/portal-
  Retry-Routen entfernt
- Checkout: Recovery-Hinweis statt "Abschluss des Abonnements", Lieferadresse
  vorbelegt, korrekter Zurueck-Link (Abo statt Shop)
- UserAbo::selfServiceDetailUrl zentralisiert die Abo-URL (Mail, Back-Links)
- Test-Command abo:test-recovery; Tests fuer payNow, Advance-Logik, Fehlermail

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-07-02 10:38:05 +00:00
parent 694ce58d19
commit e7b40bc513
28 changed files with 1535 additions and 181 deletions

View file

@ -58,6 +58,7 @@ class CheckoutController extends Controller
$is_for = $shopping_data['is_for'] ?? false;
$is_abo = isset($shopping_data['is_abo']) ? (bool) $shopping_data['is_abo'] : false;
$abo_interval = $shopping_data['abo_interval'] ?? 0;
$recovery_user_abo_id = $shopping_data['recovery_user_abo_id'] ?? null;
$homeparty_id = $shopping_data['homeparty_id'] ?? null;
$shopping_user = null;
@ -88,6 +89,7 @@ class CheckoutController extends Controller
'is_for' => $is_for,
'is_abo' => $is_abo,
'abo_interval' => $abo_interval,
'recovery_user_abo_id' => $recovery_user_abo_id,
'customer_order_source_options' => ShoppingOrder::customerOrderSourceOptions(),
'shopping_data' => $shopping_data,
'user_shop' => Util::getUserShop(),
@ -522,8 +524,10 @@ class CheckoutController extends Controller
$this->checkoutRepo->sessionDestroy(true);
Util::setInstanceStatus(3, true); // link_pending
// Abo erstellen, falls nötig
if ($ShoppingPayment->shopping_order->is_abo) {
// Abo erstellen bzw. bei Recovery-Zahlung das bestehende Abo fortschreiben
if ($ShoppingPayment->shopping_order->recovery_user_abo_id) {
AboHelper::advanceAboAfterRecoveryPayment($ShoppingPayment);
} elseif ($ShoppingPayment->shopping_order->is_abo) {
AboHelper::createNewAbo($ShoppingPayment);
}
@ -534,6 +538,7 @@ class CheckoutController extends Controller
'pay_trans' => $payt,
'is_checkout' => true,
'yard_instance' => $this->instance,
'back_url' => $this->recoveryBackUrl($ShoppingPayment->shopping_order),
];
return view('web.templates.checkout-final', $data);
@ -562,8 +567,10 @@ class CheckoutController extends Controller
$this->checkoutRepo->sessionDestroy(true);
Util::setInstanceStatus(3, true); // link_pending
// Abo erstellen, falls nötig
if ($payt->shopping_payment->shopping_order->is_abo) {
// Abo erstellen bzw. bei Recovery-Zahlung das bestehende Abo fortschreiben
if ($payt->shopping_payment->shopping_order->recovery_user_abo_id) {
AboHelper::advanceAboAfterRecoveryPayment($payt->shopping_payment);
} elseif ($payt->shopping_payment->shopping_order->is_abo) {
AboHelper::createNewAbo($payt->shopping_payment);
}
@ -578,11 +585,25 @@ class CheckoutController extends Controller
'pay_trans' => $payt,
'is_checkout' => true,
'yard_instance' => $this->instance,
'back_url' => $this->recoveryBackUrl($payt->shopping_payment->shopping_order),
];
return view('web.templates.checkout-final', $data);
}
/**
* Back-Link auf der Bestätigungsseite: bei einer Recovery-Zahlung zurück auf die Abo-Detailseite
* (Berater-CRM bzw. Kundenportal), sonst null → das Blade fällt auf den Shop-Back-Link zurück.
*/
private function recoveryBackUrl($shoppingOrder): ?string
{
if (! $shoppingOrder || ! $shoppingOrder->recovery_user_abo_id) {
return null;
}
return \App\Models\UserAbo::find($shoppingOrder->recovery_user_abo_id)?->selfServiceDetailUrl();
}
/**
* Speichert die Zahlungsdaten des Benutzers
*