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:
parent
78db952130
commit
0660f4e0a0
28 changed files with 1535 additions and 181 deletions
|
|
@ -227,13 +227,19 @@ class AboHelper
|
|||
}
|
||||
|
||||
/**
|
||||
* Sichtbarkeit des Einmalprodukte-Features. Maßgeblich ist allein das
|
||||
* konfigurierte Zeitfenster vor der Ausführung (Sales Center und Portal
|
||||
* gleichermaßen). Das frühere Review-Gate (nur VIP, Portal ausgeblendet)
|
||||
* wurde nach Abschluss der Abstimmung aufgehoben.
|
||||
* Sichtbarkeit des Einmalprodukte-Features:
|
||||
* - VIP-User (Admin) sehen/nutzen das Feature dauerhaft, unabhängig vom Zeitfenster.
|
||||
* - Alle anderen (normale Berater im Sales Center sowie Endkunden im Portal, die nie
|
||||
* auf dem `user`-Guard angemeldet sind) nur innerhalb des konfigurierten Zeitfensters
|
||||
* vor der Ausführung ({@see isOneTimeWindowOpen}, Setting `abo-onetime-window-days`).
|
||||
*/
|
||||
public static function isOneTimeFeatureVisible(UserAbo $userAbo): bool
|
||||
{
|
||||
$user = \Auth::guard('user')->user();
|
||||
if ($user instanceof User && $user->isVIP()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return self::isOneTimeWindowOpen($userAbo);
|
||||
}
|
||||
|
||||
|
|
@ -348,6 +354,80 @@ class AboHelper
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt ein bestehendes Abo nach einer erfolgreichen Recovery-Zahlung fort
|
||||
* (der Inhaber hat die offene Folgerechnung über den Checkout mit freier Zahlart beglichen).
|
||||
* Es wird KEIN neues Abo angelegt – nur das vorhandene Abo reaktiviert und der nächste Termin gesetzt.
|
||||
*/
|
||||
public static function advanceAboAfterRecoveryPayment(ShoppingPayment $shopping_payment): void
|
||||
{
|
||||
$order = $shopping_payment->shopping_order;
|
||||
if (! $order || ! $order->recovery_user_abo_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userAbo = UserAbo::find($order->recovery_user_abo_id);
|
||||
if (! $userAbo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bereits verknüpft (z. B. Payone-Status-URL vor Checkout-Redirect) oder wiederholter Aufruf
|
||||
if (UserAboOrder::where('shopping_order_id', $order->id)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aboInterval = (int) ($userAbo->abo_interval ?: $order->abo_interval);
|
||||
|
||||
// Die im Recovery-Checkout gewählte Zahlart wird als neues Abo-Zahlungsmittel übernommen,
|
||||
// damit auch künftige Folgeabbuchungen darüber laufen (das alte Token war i. d. R. defekt).
|
||||
$paymentTransaction = $shopping_payment->payment_transactions->last();
|
||||
$payoneUserId = $paymentTransaction && (int) $paymentTransaction->userid > 0
|
||||
? (int) $paymentTransaction->userid
|
||||
: (int) $userAbo->payone_userid;
|
||||
|
||||
\DB::transaction(function () use ($userAbo, $order, $aboInterval, $shopping_payment, $payoneUserId): void {
|
||||
$userAbo->update([
|
||||
'status' => 2,
|
||||
'active' => true,
|
||||
'clearingtype' => $shopping_payment->clearingtype,
|
||||
'wallettype' => $shopping_payment->wallettype,
|
||||
'carddata' => $shopping_payment->carddata,
|
||||
'payone_userid' => $payoneUserId,
|
||||
'next_date' => $aboInterval > 0 ? self::setNextDate(now(), $aboInterval) : $userAbo->next_date,
|
||||
'last_date' => now(),
|
||||
]);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $order->id,
|
||||
'status' => 1,
|
||||
'paid' => true,
|
||||
]);
|
||||
|
||||
if (! empty($shopping_payment->carddata)) {
|
||||
$userAbo->card_changes()->create([
|
||||
'masked_card' => UserAbo::formatMaskedCard($shopping_payment->carddata),
|
||||
'source' => 'recovery-checkout',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
$order->refresh();
|
||||
if ($order->paid) {
|
||||
IncentiveTracker::trackAboActivated($order);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('AboHelper::advanceAboAfterRecoveryPayment: Incentive-Tracking fehlgeschlagen', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'order_id' => $order->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
AboOneTimeService::purgeAfterExecution($userAbo);
|
||||
}
|
||||
|
||||
public static function createAboItems($user_abo, ShoppingPayment $shopping_payment)
|
||||
{
|
||||
foreach ($shopping_payment->shopping_order->shopping_order_items as $item) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue