Abo Einmalprodukte: Phase 4 - Ausfuehrung, Purge & User-Retry
- UserMakeOrder: bestaetigte Einmal-Artikel in den Yard, is_abo_addon auf ShoppingOrderItem; amount bleibt reiner Abo-Betrag (Reihenfolge) - AboOneTimeService::purgeAfterExecution: loescht alle Einmal-Artikel und rechnet Comp-Produkte neu - nur im Erfolgszweig (Cron + Retry) - User-Retry in Sales Center und Portal mit Berechtigungspruefung, gemeinsames Confirm-Modal; Admin-Retry unveraendert - Tests: AboMakeOrderOneTimeTest, AboUserRetryTest; Plan-Doku Phase 4 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8288ea59ac
commit
ee04146217
14 changed files with 536 additions and 19 deletions
|
|
@ -6,6 +6,7 @@ use App\Cron\UserMakeOrder;
|
|||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\AboOneTimeService;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\Services\MyLog;
|
||||
use App\Services\Payment;
|
||||
|
|
@ -74,7 +75,7 @@ class UserMakeAboOrder extends Command
|
|||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
$this->error('Fehler beim Ausführen des Befehls: ' . $e->getMessage());
|
||||
$this->error('Fehler beim Ausführen des Befehls: '.$e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -164,7 +165,7 @@ class UserMakeAboOrder extends Command
|
|||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
$this->error("Fehler bei Abo {$userAbo->id}: " . $e->getMessage());
|
||||
$this->error("Fehler bei Abo {$userAbo->id}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -178,7 +179,7 @@ class UserMakeAboOrder extends Command
|
|||
private function makeOrder($userAbo)
|
||||
{
|
||||
\Log::channel('abo_order')->info('UserMakeAboOrder: Starte Bestellungserstellung', ['abo_id' => $userAbo->id]);
|
||||
$this->info('Starte Bestellungserstellung für Abo: ' . $userAbo->id);
|
||||
$this->info('Starte Bestellungserstellung für Abo: '.$userAbo->id);
|
||||
|
||||
$shoppingOrder = null;
|
||||
$userOrder = new UserMakeOrder($userAbo);
|
||||
|
|
@ -205,7 +206,7 @@ class UserMakeAboOrder extends Command
|
|||
]);
|
||||
|
||||
$response = $userOrder->makePayment();
|
||||
$this->info('makePayment response: ' . json_encode($response));
|
||||
$this->info('makePayment response: '.json_encode($response));
|
||||
|
||||
// Prüfe ob Response ein Array ist (kann auch Objekt sein)
|
||||
if (is_object($response)) {
|
||||
|
|
@ -274,7 +275,7 @@ class UserMakeAboOrder extends Command
|
|||
'status' => $response['status'],
|
||||
]);
|
||||
$this->info("Zahlung ausstehend für Abo {$userAbo->id}: {$response['status']}");
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Zahlung ausstehend: ' . $response['status']);
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Zahlung ausstehend: '.$response['status']);
|
||||
} else {
|
||||
// Unbekannter Status: Bestellung speichern, aber Abo nicht aktualisieren
|
||||
\Log::channel('abo_order')->warning('UserMakeAboOrder: Unbekannter Zahlungsstatus', [
|
||||
|
|
@ -283,7 +284,7 @@ class UserMakeAboOrder extends Command
|
|||
'status' => $response['status'],
|
||||
]);
|
||||
$this->warn("Unbekannter Zahlungsstatus für Abo {$userAbo->id}: {$response['status']}");
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Unbekannter Status: ' . $response['status']);
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Unbekannter Status: '.$response['status']);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Ausnahme bei der Bestellungserstellung', [
|
||||
|
|
@ -291,11 +292,11 @@ class UserMakeAboOrder extends Command
|
|||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
$this->error("Ausnahme bei Abo {$userAbo->id}: " . $e->getMessage());
|
||||
$this->error("Ausnahme bei Abo {$userAbo->id}: ".$e->getMessage());
|
||||
|
||||
// Bestellung existiert (z. B. Fehler bei Payone): Abo-Fehlerstatus, Bestellung bleibt nachvollziehbar
|
||||
if ($shoppingOrder) {
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Exception: ' . $e->getMessage());
|
||||
$this->updateAboOnError($userAbo, $shoppingOrder, 'Exception: '.$e->getMessage());
|
||||
|
||||
return $shoppingOrder;
|
||||
}
|
||||
|
|
@ -356,12 +357,15 @@ class UserMakeAboOrder extends Command
|
|||
// Wie bei Payment::paymentStatusPaidAction: Incentive nur wenn Callback nicht lief
|
||||
// (firstOrCreate verhindert Doppelungen wenn Payone später noch trackt)
|
||||
IncentiveTracker::trackAboActivated($shoppingOrder);
|
||||
|
||||
// Nur bei Erfolg: Einmal-Artikel entfernen und Comp-Produkte neu bewerten.
|
||||
AboOneTimeService::purgeAfterExecution($userAbo);
|
||||
} catch (\Exception $e) {
|
||||
\Log::channel('abo_order')->error('UserMakeAboOrder: Fehler beim Aktualisieren des Abos', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
|
||||
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: ".$e->getMessage());
|
||||
throw $e; // Re-throw für besseres Error-Handling
|
||||
}
|
||||
}
|
||||
|
|
@ -421,7 +425,7 @@ class UserMakeAboOrder extends Command
|
|||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: " . $e->getMessage());
|
||||
$this->error("Fehler beim Aktualisieren des Abos {$userAbo->id}: ".$e->getMessage());
|
||||
// Bei Fehler hier nicht re-throw, damit der Hauptprozess fortgesetzt werden kann
|
||||
}
|
||||
}
|
||||
|
|
@ -437,6 +441,6 @@ class UserMakeAboOrder extends Command
|
|||
$sec = intval($diff);
|
||||
$micro = $diff - $sec;
|
||||
|
||||
return $sec . ' Sekunden und ' . round($micro * 1000, 2) . ' ms';
|
||||
return $sec.' Sekunden und '.round($micro * 1000, 2).' ms';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,8 +177,13 @@ class UserMakeOrder
|
|||
}
|
||||
|
||||
// hier wird die Bestellung erstellt inkl aktueller Preise
|
||||
// (setzt user_abos.amount auf den REINEN Abo-Betrag)
|
||||
AboOrderCart::makeOrderYard($this->userAbo);
|
||||
|
||||
// Verbindlich bestätigte Einmal-Artikel zusätzlich in den Yard laden
|
||||
// (verändert user_abos.amount NICHT; Versand/Gewicht werden kombiniert berechnet).
|
||||
AboOrderCart::addOneTimeItemsToYard($this->userAbo);
|
||||
|
||||
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
||||
|
||||
// Debug: Logge welche Produkte im Cart sind
|
||||
|
|
@ -268,6 +273,7 @@ class UserMakeOrder
|
|||
'row_id' => $item->rowId,
|
||||
'product_id' => $item->id,
|
||||
'comp' => $item->options->comp,
|
||||
'is_abo_addon' => (bool) ($item->options->abo_addon ?? false),
|
||||
'qty' => $item->qty,
|
||||
'price' => $item->price,
|
||||
'price_net' => $price_net,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use App\Services\AboHelper;
|
|||
use App\Services\AboItemHistoryService;
|
||||
use App\Services\AboOneTimeService;
|
||||
use App\Services\AboOrderCart;
|
||||
use App\Services\AboRetryPaymentService;
|
||||
use App\Services\Shop;
|
||||
use App\Services\UserService;
|
||||
use App\Services\Util;
|
||||
|
|
@ -491,6 +492,18 @@ class AboController extends Controller
|
|||
return false;
|
||||
}
|
||||
|
||||
public function retryPayment($id, AboRetryPaymentService $retryPaymentService)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPortalPermission($user_abo);
|
||||
|
||||
$result = $retryPaymentService->retry($user_abo);
|
||||
|
||||
\Session()->flash($result['success'] ? 'alert-success' : 'alert-error', $result['message']);
|
||||
|
||||
return redirect(route('portal.my_subscriptions'));
|
||||
}
|
||||
|
||||
private function checkPortalPermission($user_abo)
|
||||
{
|
||||
$user = Auth::guard('customers')->user();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use App\Repositories\AboRepository;
|
|||
use App\Services\AboHelper;
|
||||
use App\Services\AboItemHistoryService;
|
||||
use App\Services\AboOrderCart;
|
||||
use App\Services\AboRetryPaymentService;
|
||||
use App\Services\Shop;
|
||||
use App\User;
|
||||
use Request;
|
||||
|
|
@ -507,6 +508,18 @@ class AboController extends Controller
|
|||
->make(true);
|
||||
}
|
||||
|
||||
public function retryPayment($view, $id, AboRetryPaymentService $retryPaymentService)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPermissions($view, $user_abo);
|
||||
|
||||
$result = $retryPaymentService->retry($user_abo);
|
||||
|
||||
\Session()->flash($result['success'] ? 'alert-success' : 'alert-error', $result['message']);
|
||||
|
||||
return redirect(route('user_abos_detail', [$view, $id]));
|
||||
}
|
||||
|
||||
private function checkPermissions($view, $user_abo)
|
||||
{
|
||||
\Log::info('checkPermissions', ['view' => $view, 'user_abo' => $user_abo]);
|
||||
|
|
|
|||
|
|
@ -192,6 +192,44 @@ class AboOneTimeService
|
|||
->sum(fn (UserAboOneTimeItem $item): float => (float) $item->price * (int) $item->qty), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt nach einer erfolgreichen Ausführung ALLE Einmal-Artikel des Abos
|
||||
* (bestätigte und offene Entwürfe, inkl. soft-deleted) und bewertet anschließend
|
||||
* die Kompensationsprodukte des reinen Abos neu (das durch Einmal-Artikel
|
||||
* verursachte Zusatzgewicht ist nun weg → überzählige Comp-Artikel werden entfernt).
|
||||
*
|
||||
* Wird ausschließlich im Erfolgszweig aufgerufen. Bei Zahlungsfehler bleiben die
|
||||
* Einmal-Artikel erhalten (Entscheidung #2/#3). Schlägt etwas fehl, wird der bereits
|
||||
* erfolgreiche Abo-Lauf NICHT zurückgerollt – Fehler werden nur protokolliert.
|
||||
*/
|
||||
public static function purgeAfterExecution(UserAbo $userAbo): void
|
||||
{
|
||||
try {
|
||||
UserAboOneTimeItem::withTrashed()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->get()
|
||||
->each(fn (UserAboOneTimeItem $item) => $item->forceDelete());
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->error('AboOneTimeService::purgeAfterExecution: Löschen der Einmal-Artikel fehlgeschlagen', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AboOrderCart::initYard($userAbo);
|
||||
AboOrderCart::makeOrderYard($userAbo);
|
||||
AboOrderCart::checkNumOfCompProducts($userAbo);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::channel('abo_order')->warning('AboOneTimeService::purgeAfterExecution: Comp-Neuberechnung übersprungen', [
|
||||
'abo_id' => $userAbo->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function hasUnconfirmedChanges(UserAbo $userAbo): bool
|
||||
{
|
||||
return UserAboOneTimeItem::withTrashed()
|
||||
|
|
|
|||
|
|
@ -157,6 +157,9 @@ class AboRetryPaymentService
|
|||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Nur bei Erfolg: Einmal-Artikel entfernen und Comp-Produkte neu bewerten.
|
||||
AboOneTimeService::purgeAfterExecution($userAbo);
|
||||
}
|
||||
|
||||
private function markAboError(UserAbo $userAbo, mixed $shoppingOrder): void
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue