Abo: extern-erledigt für gelöschte Abos + Lieferhistorie-Umzug (Ticket Kunz Mai-Punkte)
- AboExternalCompletionService: Bestellungen gelöschter/ersetzter Abos können
nachträglich extern erledigt werden (Punkte + Status 8 + Versandstatus);
das gelöschte Abo wird dabei nicht reaktiviert
- IncentiveTracker::trackSalesVolume: Abo-Zuordnung inkl. gelöschter Abos
(withTrashed) - nachgebuchte Umsätze bereits getrackter Abos zählen weiter
- Neue Wartungs-Commands: abo:extern-done {shopping_order_id} und
abo:move-orders {from_abo} {to_abo} (--dry-run, Guards: gleicher Kunde)
- Latent datumsabhängigen Alt-Test in AboExternalCompletionTest repariert
(Buchung nutzt date(), nicht Carbon::setTestNow)
Fall Kunz (dev/2026-08-12-intensiv-tickets): Dannenhauers gelieferte, aber am
17.06. beim Nachtragen vergessene Mai-Order 53274 → abo:extern-done (+54 P);
Bockhackers Lieferhistorie von gelöschtem Abo 197 → 496 umgehängt (Zähler 4).
Bockhackers 24 P existieren bereits (Juni-Buchung der Mai-Lieferung).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
867dc21b57
commit
f6faad9ae7
8 changed files with 433 additions and 17 deletions
45
app/Console/Commands/AboExternDoneOrder.php
Normal file
45
app/Console/Commands/AboExternDoneOrder.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Services\AboExternalCompletionService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Wartungs-Command: eine angehaltene Abo-Bestellung als "extern erledigt" markieren
|
||||
* (Punkte gutschreiben, Lieferung zaehlt, Bestellung bleibt unbezahlt).
|
||||
*
|
||||
* Entspricht dem Admin-Button "extern erledigt", funktioniert aber auch fuer
|
||||
* Bestellungen geloeschter/ersetzter Abos, die im UI nicht mehr erreichbar sind.
|
||||
*/
|
||||
class AboExternDoneOrder extends Command
|
||||
{
|
||||
protected $signature = 'abo:extern-done {shopping_order_id : ID der Abo-Bestellung}';
|
||||
|
||||
protected $description = 'Angehaltene Abo-Bestellung als extern erledigt markieren (Punkte + Lieferzähler, auch bei gelöschtem Abo)';
|
||||
|
||||
public function handle(AboExternalCompletionService $service): int
|
||||
{
|
||||
$order = ShoppingOrder::find($this->argument('shopping_order_id'));
|
||||
|
||||
if (! $order) {
|
||||
$this->error('Bestellung nicht gefunden.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$result = $service->complete($order);
|
||||
|
||||
if (! $result['success']) {
|
||||
$this->error($result['message']);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("Bestellung {$order->id} extern erledigt: {$result['message']}");
|
||||
$this->info("Punkte: {$order->points} | Netto: {$order->subtotal} | Berater: ".($order->member_id ?? $order->auth_user_id));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
80
app/Console/Commands/AboMoveOrders.php
Normal file
80
app/Console/Commands/AboMoveOrders.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Wartungs-Command: Lieferhistorie (user_abo_orders) eines geloeschten/ersetzten
|
||||
* Abos auf das Nachfolge-Abo desselben Kunden umhaengen.
|
||||
*
|
||||
* Hintergrund: Muss ein Abo geloescht und neu angelegt werden (z. B. Wechsel des
|
||||
* Zahlungsmittels), startet der Lieferzaehler des neuen Abos bei 0 - Mindestlaufzeit
|
||||
* und "x-te Lieferung" gehen verloren. Dieses Command uebertraegt die Historie.
|
||||
*/
|
||||
class AboMoveOrders extends Command
|
||||
{
|
||||
protected $signature = 'abo:move-orders
|
||||
{from_abo : ID des alten (i. d. R. gelöschten) Abos}
|
||||
{to_abo : ID des Nachfolge-Abos}
|
||||
{--dry-run : Nur anzeigen, nichts ändern}';
|
||||
|
||||
protected $description = 'Lieferhistorie (user_abo_orders) eines ersetzten Abos auf das Nachfolge-Abo umhängen';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$from = UserAbo::withTrashed()->find($this->argument('from_abo'));
|
||||
$to = UserAbo::find($this->argument('to_abo'));
|
||||
|
||||
if (! $from || ! $to) {
|
||||
$this->error('Abo nicht gefunden (from: '.($from ? 'ok' : 'fehlt').', to: '.($to ? 'ok' : 'fehlt').').');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($from->id === $to->id) {
|
||||
$this->error('from_abo und to_abo sind identisch.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$sameCustomer = strcasecmp(trim((string) $from->email), trim((string) $to->email)) === 0
|
||||
&& (int) $from->member_id === (int) $to->member_id
|
||||
&& $from->is_for === $to->is_for;
|
||||
|
||||
if (! $sameCustomer) {
|
||||
$this->error("Abos gehören nicht zum selben Kunden (from: {$from->email} / Berater {$from->member_id}, to: {$to->email} / Berater {$to->member_id}). Abbruch.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$orders = UserAboOrder::where('user_abo_id', $from->id)->orderBy('id')->get();
|
||||
|
||||
if ($orders->isEmpty()) {
|
||||
$this->info("Keine user_abo_orders bei Abo {$from->id} - nichts zu tun.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Abo {$from->id} ({$from->email}".($from->trashed() ? ', gelöscht' : '').") → Abo {$to->id}: {$orders->count()} Lieferung(en)");
|
||||
$this->table(
|
||||
['user_abo_order', 'shopping_order', 'Status', 'bezahlt'],
|
||||
$orders->map(fn (UserAboOrder $o) => [$o->id, $o->shopping_order_id, $o->status, $o->paid ? 'ja' : 'nein'])->all()
|
||||
);
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
$this->info('DRY-RUN - nichts geändert.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
UserAboOrder::where('user_abo_id', $from->id)->update(['user_abo_id' => $to->id]);
|
||||
|
||||
$to->refresh();
|
||||
$this->info("Umgehängt. Abo {$to->id} zählt jetzt {$to->getCountPaidOrders()} ausgeführte Lieferung(en).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,9 @@ class AboExternalCompletionService
|
|||
}
|
||||
|
||||
$userAboOrder = UserAboOrder::where('shopping_order_id', $shoppingOrder->id)->first();
|
||||
$userAbo = $userAboOrder?->user_abo;
|
||||
// Auch geloeschte Abos zulassen: eine ausstehende Lieferung eines ersetzten/
|
||||
// geloeschten Abos muss nachtraeglich erledigt werden koennen (Punkte + Zaehler).
|
||||
$userAbo = $userAboOrder ? $userAboOrder->user_abo()->withTrashed()->first() : null;
|
||||
|
||||
if (! $userAboOrder || ! $userAbo) {
|
||||
return ['success' => false, 'message' => __('abo.extern_done_no_order')];
|
||||
|
|
@ -65,18 +67,24 @@ class AboExternalCompletionService
|
|||
$shoppingOrder->save();
|
||||
|
||||
// Blockade aufheben: Abo zurueck in den regulaeren Zyklus.
|
||||
$userAbo->update([
|
||||
'status' => self::STATUS_ABO_OKAY,
|
||||
'active' => true,
|
||||
'last_date' => now(),
|
||||
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
|
||||
]);
|
||||
// Geloeschte (ersetzte) Abos bleiben unangetastet - dort werden nur
|
||||
// Punkte und Lieferstatus nachgezogen, kein neuer Zyklus gestartet.
|
||||
if (! $userAbo->trashed()) {
|
||||
$userAbo->update([
|
||||
'status' => self::STATUS_ABO_OKAY,
|
||||
'active' => true,
|
||||
'last_date' => now(),
|
||||
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Wie bei einer erfolgreichen Ausfuehrung: Incentive tracken und
|
||||
// Einmal-Artikel entfernen. Beide Aufrufe sind idempotent.
|
||||
IncentiveTracker::trackAboActivated($shoppingOrder);
|
||||
AboOneTimeService::purgeAfterExecution($userAbo);
|
||||
if (! $userAbo->trashed()) {
|
||||
AboOneTimeService::purgeAfterExecution($userAbo);
|
||||
}
|
||||
|
||||
\Log::channel('abo_order')->info('AboExternalCompletionService: Bestellung extern erledigt', [
|
||||
'abo_id' => $userAbo->id,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use App\Models\IncentiveParticipant;
|
|||
use App\Models\IncentivePointsLog;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
|
|
@ -197,7 +198,11 @@ class IncentiveTracker
|
|||
$order = ShoppingOrder::find($user_sales_volume->shopping_order_id);
|
||||
|
||||
if ($order) {
|
||||
$userAboFromOrder = $order->getUserAbo();
|
||||
// Bewusst inkl. geloeschter Abos: ein bereits getracktes Abo kann
|
||||
// geloescht/ersetzt worden sein, waehrend Umsaetze nachgebucht werden
|
||||
// (z. B. extern erledigte Lieferungen) - die zaehlen weiterhin.
|
||||
$userAboFromOrder = UserAboOrder::where('shopping_order_id', $order->id)
|
||||
->first()?->user_abo()->withTrashed()->first();
|
||||
if (! $userAboFromOrder || ! in_array($userAboFromOrder->is_for, ['ot', 'me'], true)) {
|
||||
$userAboFromOrder = null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue