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:
Kevin 2026-06-08 15:32:27 +00:00
parent 8288ea59ac
commit ee04146217
14 changed files with 536 additions and 19 deletions

View file

@ -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()

View file

@ -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