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
d65922adfe
commit
3f74f76819
8 changed files with 433 additions and 17 deletions
|
|
@ -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