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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,49 @@ Umsetzung:
|
|||
Verifiziert auf der Live-Kopie (19.08.): 13 August-Bestellungen geprüft, 12 bereits
|
||||
gebucht, genau 1 nachgebucht (Order 59900, Jasmin Grund +200 P), Juni/Juli unberührt.
|
||||
|
||||
## Nachtrag 2026-08-21: Ticket Kunz Teil 2 – Mai-Punkte & Bockhacker-Lieferzähler
|
||||
|
||||
Re-Verifikation mit DHL-Daten ergab ein anderes Bild als die erste Analyse:
|
||||
|
||||
- Die Mai-Lieferungen **wurden versandt und zugestellt** (DHL 12.05.), aber nie bezahlt
|
||||
(`txaction='prev'`). Am 17.06. hat ein Admin zwei Bestellungen per „extern erledigt"
|
||||
(Status 8) nachgebucht: Bockhackers **Mai**-Order 53619 (24 P) und Dannenhauers
|
||||
**Juni**-Order 55443 (54 P) – Buchungsmonat jeweils Juni (Systemregel: aktueller Monat).
|
||||
- **Bockhacker:** Es fehlt nichts – die 24 P der Mai-Lieferung existieren (als Juni
|
||||
gebucht). Kein Nachtrag, sonst Doppelbuchung.
|
||||
- **Dannenhauer:** Ihre Mai-Order 53274 (geliefert!) wurde beim Nachtragen am 17.06.
|
||||
**vergessen** → 54 P fehlten wirklich. Nachgebucht am 21.08. per neuem Command
|
||||
`abo:extern-done 53274` (Buchung August, keine Retro-Buchung; +54 P auch im Incentive).
|
||||
Hinweis: Abo 198 ist seit 06.07. gelöscht, Kundin hat kein aktives Abo mehr; die
|
||||
Orders 54890/55441/55442/57268 wurden laut DHL nie versandt und bleiben offen.
|
||||
- **Bockhacker Lieferzähler:** Abo 197 (09.04.) wurde am 06.07. gelöscht und durch
|
||||
Abo 496 ersetzt → Zähler startete bei 0. Per neuem Command `abo:move-orders 197 496`
|
||||
wurde die Lieferhistorie umgehängt; Abo 496 zählt jetzt **4 ausgeführte Lieferungen**
|
||||
(09.04., 12.05. [extern erledigt], 10.07., 10.08.). Laut DHL war die Juli-Lieferung
|
||||
die **3.** (nicht die 4. wie im Ticket) – es gab keine Juni-Sendung an Bockhacker;
|
||||
die Juni-*Buchung* vom 17.06. gehörte zur Mai-Lieferung.
|
||||
|
||||
Code-Änderungen dafür:
|
||||
|
||||
- `AboExternalCompletionService`: funktioniert jetzt auch für Bestellungen
|
||||
gelöschter/ersetzter Abos (Punkte + Status 8 + Versandstatus; das gelöschte Abo wird
|
||||
**nicht** reaktiviert, kein neuer Zyklus).
|
||||
- `IncentiveTracker::trackSalesVolume`: Abo-Zuordnung inkl. gelöschter Abos
|
||||
(`withTrashed`) – nachgebuchte Umsätze bereits getrackter, inzwischen gelöschter
|
||||
Abos zählen weiter im Incentive.
|
||||
- Neue Wartungs-Commands: `abo:extern-done {shopping_order_id}` und
|
||||
`abo:move-orders {from_abo} {to_abo} [--dry-run]` (Guards: gleicher Kunde/Berater).
|
||||
- Fix eines latent datumsabhängigen Tests in `AboExternalCompletionTest`
|
||||
(Erwartung nutzte `setTestNow`, Produktion bucht mit `date()`).
|
||||
|
||||
**Live-Ausführung nach Deploy:**
|
||||
|
||||
```bash
|
||||
php artisan abo:extern-done 53274 # Dannenhauer Mai-Lieferung: +54 P an Kunz
|
||||
php artisan abo:move-orders 197 496 # Bockhacker Lieferhistorie → Abo 496
|
||||
php artisan incentive:calculate 2 # optional sofort (sonst Nacht-Cron 05:00)
|
||||
```
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use App\Models\Country;
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveNewAbo;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
|
|
@ -11,6 +12,7 @@ use App\Models\UserAccount;
|
|||
use App\Models\UserSalesVolume;
|
||||
use App\Services\AboExternalCompletionService;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
|
@ -134,7 +136,7 @@ it('markiert eine angehaltene Abo-Bestellung als extern erledigt, schreibt Punkt
|
|||
// Blockade aufgehoben: zurueck auf okay, aktiv, next_date in der Zukunft
|
||||
expect((int) $userAbo->status)->toBe(2)
|
||||
->and($userAbo->active)->toBeTrue();
|
||||
expect(\Carbon\Carbon::parse($userAbo->getRawOriginal('next_date'))->isFuture())->toBeTrue();
|
||||
expect(Carbon::parse($userAbo->getRawOriginal('next_date'))->isFuture())->toBeTrue();
|
||||
|
||||
// Abo-Lieferungs-Zaehler beruecksichtigt die extern erledigte Ausfuehrung.
|
||||
$fresh = UserAbo::find($userAbo->id);
|
||||
|
|
@ -153,14 +155,14 @@ it('markiert eine angehaltene Abo-Bestellung als extern erledigt, schreibt Punkt
|
|||
});
|
||||
|
||||
it('trackt Incentive-Punkte mit dem Bestelldatum (Ausfuehrungsmonat), nicht mit heute', function () {
|
||||
\Carbon\Carbon::setTestNow('2026-06-15');
|
||||
Carbon::setTestNow('2026-06-15');
|
||||
|
||||
[$userAbo, $order, , $user] = heldAboSeed();
|
||||
|
||||
// Die vorgesehene Ausfuehrung lag im April; das Incentive-Fenster deckt nur
|
||||
// April ab (nicht Juni/heute). Wuerde der Tracker "heute" verwenden, faende
|
||||
// er das Incentive nicht und legte keinen IncentiveNewAbo-Eintrag an.
|
||||
$executionDate = \Carbon\Carbon::parse('2026-04-10');
|
||||
$executionDate = Carbon::parse('2026-04-10');
|
||||
ShoppingOrder::where('id', $order->id)->update(['created_at' => $executionDate]);
|
||||
|
||||
Incentive::factory()->active()->create([
|
||||
|
|
@ -177,15 +179,16 @@ it('trackt Incentive-Punkte mit dem Bestelldatum (Ausfuehrungsmonat), nicht mit
|
|||
->and($newAbo->activated_at->toDateString())->toBe('2026-04-10');
|
||||
|
||||
// Incentive-Zaehler (qualified_abos) des Teilnehmers wurde erhoeht.
|
||||
$participant = \App\Models\IncentiveParticipant::find($newAbo->participant_id);
|
||||
$participant = IncentiveParticipant::find($newAbo->participant_id);
|
||||
expect((int) $participant->qualified_abos)->toBe(1);
|
||||
|
||||
// Reguläre Provisionspunkte hingegen mit heutigem Datum (Juni).
|
||||
// Reguläre Provisionspunkte hingegen mit dem Buchungstag (SalesPointsVolume
|
||||
// nutzt date(), nicht Carbon::setTestNow - daher gegen die echte Uhr pruefen).
|
||||
$volume = UserSalesVolume::where('shopping_order_id', $order->id)->first();
|
||||
expect((int) $volume->month)->toBe(6)
|
||||
->and((int) $volume->year)->toBe(2026);
|
||||
expect((int) $volume->month)->toBe((int) date('m'))
|
||||
->and((int) $volume->year)->toBe((int) date('Y'));
|
||||
|
||||
\Carbon\Carbon::setTestNow();
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
it('ist idempotent und schreibt Punkte nicht doppelt gut', function () {
|
||||
|
|
@ -220,6 +223,28 @@ it('verweigert die Aktion fuer eine Nicht-Abo-Bestellung', function () {
|
|||
expect($result['success'])->toBeFalse();
|
||||
});
|
||||
|
||||
it('erledigt Bestellungen geloeschter (ersetzter) Abos: Punkte + Zaehler, ohne das Abo zu reaktivieren', function () {
|
||||
[$userAbo, $order, $aboOrder, $user] = heldAboSeed();
|
||||
$userAbo->delete();
|
||||
|
||||
$result = app(AboExternalCompletionService::class)->complete($order->fresh());
|
||||
|
||||
expect($result['success'])->toBeTrue();
|
||||
expect((int) $aboOrder->fresh()->status)->toBe(8);
|
||||
|
||||
// Abo bleibt geloescht und unveraendert - kein neuer Zyklus, keine Reaktivierung
|
||||
$trashed = UserAbo::withTrashed()->find($userAbo->id);
|
||||
expect($trashed->trashed())->toBeTrue()
|
||||
->and((int) $trashed->getRawOriginal('status'))->toBe(3)
|
||||
->and(Carbon::parse($trashed->getRawOriginal('next_date'))->isPast())->toBeTrue();
|
||||
|
||||
// Punkte trotzdem gutgeschrieben
|
||||
$volume = UserSalesVolume::where('shopping_order_id', $order->id)->first();
|
||||
expect($volume)->not->toBeNull()
|
||||
->and((float) $volume->points)->toBe(25.0)
|
||||
->and((int) $volume->user_id)->toBe($user->id);
|
||||
});
|
||||
|
||||
it('erledigt ueber die Abo-Detailseite die juengste angehaltene Bestellung', function () {
|
||||
[$userAbo, $order] = heldAboSeed();
|
||||
|
||||
|
|
|
|||
88
tests/Feature/AboMoveOrdersTest.php
Normal file
88
tests/Feature/AboMoveOrdersTest.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function moveOrdersSeedAbo(string $email, int $memberId, bool $deleted = false): UserAbo
|
||||
{
|
||||
$country = Country::firstOrCreate(['code' => 'DE'], [
|
||||
'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania',
|
||||
'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
|
||||
$shoppingUser = ShoppingUser::create([
|
||||
'auth_user_id' => $memberId,
|
||||
'member_id' => $memberId,
|
||||
'billing_email' => $email,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'is_for' => 'ot',
|
||||
]);
|
||||
|
||||
$abo = UserAbo::create([
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'member_id' => $memberId,
|
||||
'is_for' => 'ot',
|
||||
'email' => $email,
|
||||
'payone_userid' => 1,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => ! $deleted,
|
||||
'status' => 2,
|
||||
'abo_interval' => 5,
|
||||
]);
|
||||
|
||||
if ($deleted) {
|
||||
$abo->delete();
|
||||
}
|
||||
|
||||
return $abo;
|
||||
}
|
||||
|
||||
it('haengt die Lieferhistorie eines geloeschten Abos auf das Nachfolge-Abo um', function () {
|
||||
$old = moveOrdersSeedAbo('kundin@example.com', 42, deleted: true);
|
||||
$new = moveOrdersSeedAbo('kundin@example.com', 42);
|
||||
|
||||
UserAboOrder::create(['user_abo_id' => $old->id, 'shopping_order_id' => 1001, 'status' => 2, 'paid' => true]);
|
||||
UserAboOrder::create(['user_abo_id' => $old->id, 'shopping_order_id' => 1002, 'status' => 8, 'paid' => false]);
|
||||
UserAboOrder::create(['user_abo_id' => $new->id, 'shopping_order_id' => 1003, 'status' => 2, 'paid' => true]);
|
||||
|
||||
$this->artisan('abo:move-orders', ['from_abo' => $old->id, 'to_abo' => $new->id])
|
||||
->assertSuccessful();
|
||||
|
||||
expect(UserAboOrder::where('user_abo_id', $old->id)->count())->toBe(0)
|
||||
->and(UserAboOrder::where('user_abo_id', $new->id)->count())->toBe(3);
|
||||
|
||||
// bezahlte + extern erledigte (Status 8) Lieferungen zaehlen fuer die Mindestlaufzeit
|
||||
expect(UserAbo::find($new->id)->getCountPaidOrders())->toBe(3);
|
||||
});
|
||||
|
||||
it('verweigert das Umhaengen zwischen Abos verschiedener Kunden', function () {
|
||||
$old = moveOrdersSeedAbo('kundin-a@example.com', 42, deleted: true);
|
||||
$new = moveOrdersSeedAbo('kundin-b@example.com', 42);
|
||||
|
||||
UserAboOrder::create(['user_abo_id' => $old->id, 'shopping_order_id' => 1001, 'status' => 2, 'paid' => true]);
|
||||
|
||||
$this->artisan('abo:move-orders', ['from_abo' => $old->id, 'to_abo' => $new->id])
|
||||
->assertFailed();
|
||||
|
||||
expect(UserAboOrder::where('user_abo_id', $old->id)->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('aendert im dry-run nichts', function () {
|
||||
$old = moveOrdersSeedAbo('kundin@example.com', 42, deleted: true);
|
||||
$new = moveOrdersSeedAbo('kundin@example.com', 42);
|
||||
|
||||
UserAboOrder::create(['user_abo_id' => $old->id, 'shopping_order_id' => 1001, 'status' => 2, 'paid' => true]);
|
||||
|
||||
$this->artisan('abo:move-orders', ['from_abo' => $old->id, 'to_abo' => $new->id, '--dry-run' => true])
|
||||
->assertSuccessful();
|
||||
|
||||
expect(UserAboOrder::where('user_abo_id', $old->id)->count())->toBe(1)
|
||||
->and(UserAboOrder::where('user_abo_id', $new->id)->count())->toBe(0);
|
||||
});
|
||||
122
tests/Feature/Incentive/DeletedAboSalesVolumeIncentiveTest.php
Normal file
122
tests/Feature/Incentive/DeletedAboSalesVolumeIncentiveTest.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveNewAbo;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\IncentivePointsLog;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Carbon::setTestNow(Carbon::parse('2026-08-19 12:00:00'));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
it('zaehlt nachgebuchte Umsatzpunkte auch, wenn das getrackte Kundenabo inzwischen geloescht ist', function () {
|
||||
$consultant = User::forceCreate([
|
||||
'email' => 'berater-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
]);
|
||||
|
||||
$incentive = Incentive::factory()->create([
|
||||
'qualification_start' => '2026-04-01',
|
||||
'qualification_end' => '2026-08-31',
|
||||
'calculation_end' => '2026-08-31',
|
||||
'status' => 1,
|
||||
]);
|
||||
$participant = IncentiveParticipant::factory()->create([
|
||||
'incentive_id' => $incentive->id,
|
||||
'user_id' => $consultant->id,
|
||||
]);
|
||||
|
||||
$country = Country::firstOrCreate(['code' => 'DE'], [
|
||||
'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania',
|
||||
'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
|
||||
$shoppingUser = ShoppingUser::create([
|
||||
'auth_user_id' => $consultant->id,
|
||||
'member_id' => $consultant->id,
|
||||
'billing_email' => 'kundin-'.uniqid('', true).'@example.com',
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'is_for' => 'ot',
|
||||
]);
|
||||
|
||||
$abo = UserAbo::create([
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'member_id' => $consultant->id,
|
||||
'is_for' => 'ot',
|
||||
'email' => 'kundin-'.uniqid('', true).'@example.com',
|
||||
'payone_userid' => 1,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => true,
|
||||
'status' => 2,
|
||||
'abo_interval' => 5,
|
||||
]);
|
||||
|
||||
IncentiveNewAbo::create([
|
||||
'participant_id' => $participant->id,
|
||||
'user_abo_id' => $abo->id,
|
||||
'activated_at' => '2026-04-09',
|
||||
]);
|
||||
|
||||
$order = ShoppingOrder::create([
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'auth_user_id' => $consultant->id,
|
||||
'country_id' => $country->id,
|
||||
'user_shop_id' => 1,
|
||||
'payment_for' => 6,
|
||||
'total' => 92.60,
|
||||
'subtotal' => 82.79,
|
||||
'points' => 54,
|
||||
'paid' => false,
|
||||
'is_abo' => true,
|
||||
'mode' => 'live',
|
||||
]);
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'shopping_order_id' => $order->id,
|
||||
'status' => 8,
|
||||
'paid' => false,
|
||||
]);
|
||||
|
||||
// Abo wurde inzwischen geloescht (ersetzt) - wie im Fall Dannenhauer (#198)
|
||||
$abo->delete();
|
||||
|
||||
$usv = UserSalesVolume::create([
|
||||
'user_id' => $consultant->id,
|
||||
'shopping_order_id' => $order->id,
|
||||
'month' => 8,
|
||||
'year' => 2026,
|
||||
'points' => 54,
|
||||
'status_points' => 1,
|
||||
'total_net' => 82.79,
|
||||
'status' => 2,
|
||||
]);
|
||||
|
||||
IncentiveTracker::trackSalesVolume($usv);
|
||||
|
||||
$log = IncentivePointsLog::where('participant_id', $participant->id)
|
||||
->where('user_sales_volume_id', $usv->id)
|
||||
->where('is_storno', false)
|
||||
->first();
|
||||
|
||||
expect($log)->not->toBeNull()
|
||||
->and((int) $log->points_accumulated)->toBe(54)
|
||||
->and((int) $log->month)->toBe(8);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue