- 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>
256 lines
9.1 KiB
PHP
256 lines
9.1 KiB
PHP
<?php
|
|
|
|
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;
|
|
use App\Models\UserAboOrder;
|
|
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;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
function heldAboSeed(): array
|
|
{
|
|
$country = Country::create([
|
|
'code' => 'DE',
|
|
'phone' => '49',
|
|
'en' => 'Germany',
|
|
'de' => 'Deutschland',
|
|
'es' => 'Alemania',
|
|
'fr' => 'Allemagne',
|
|
'it' => 'Germania',
|
|
'ru' => 'Германия',
|
|
'eu_country' => true,
|
|
]);
|
|
|
|
$account = UserAccount::create([
|
|
'first_name' => 'Klaus',
|
|
'last_name' => 'Kunde',
|
|
'address' => 'Hauptstrasse 1',
|
|
'zipcode' => '10115',
|
|
'city' => 'Berlin',
|
|
'country_id' => $country->id,
|
|
]);
|
|
|
|
$user = User::forceCreate([
|
|
'account_id' => $account->id,
|
|
'email' => 'klaus@example.com',
|
|
'password' => bcrypt('secret'),
|
|
'lang' => 'de',
|
|
]);
|
|
|
|
$shoppingUser = ShoppingUser::create([
|
|
'auth_user_id' => $user->id,
|
|
'billing_firstname' => 'Klaus',
|
|
'billing_lastname' => 'Kunde',
|
|
'billing_address' => 'Hauptstrasse 1',
|
|
'billing_zipcode' => '10115',
|
|
'billing_city' => 'Berlin',
|
|
'billing_country_id' => $country->id,
|
|
'billing_email' => $user->email,
|
|
'same_as_billing' => true,
|
|
'shipping_firstname' => 'Klaus',
|
|
'shipping_lastname' => 'Kunde',
|
|
'shipping_address' => 'Hauptstrasse 1',
|
|
'shipping_zipcode' => '10115',
|
|
'shipping_city' => 'Berlin',
|
|
'shipping_country_id' => $country->id,
|
|
'is_for' => 'me',
|
|
'is_from' => 'user_order',
|
|
'mode' => 'live',
|
|
]);
|
|
|
|
$userAbo = UserAbo::create([
|
|
'user_id' => $user->id,
|
|
'shopping_user_id' => $shoppingUser->id,
|
|
'is_for' => 'me',
|
|
'email' => $user->email,
|
|
'payone_userid' => 654321,
|
|
'clearingtype' => 'cc',
|
|
'active' => true,
|
|
'status' => 3, // abo_hold (Blockade)
|
|
'abo_interval' => 5,
|
|
'next_date' => now()->subDays(2), // ueberfaellig
|
|
]);
|
|
|
|
$order = ShoppingOrder::create([
|
|
'shopping_user_id' => $shoppingUser->id,
|
|
'auth_user_id' => $user->id,
|
|
'country_id' => $country->id,
|
|
'language' => 'de',
|
|
'user_shop_id' => 1,
|
|
'payment_for' => 6,
|
|
'total' => 50.00,
|
|
'subtotal' => 50.00,
|
|
'points' => 25,
|
|
'paid' => false,
|
|
'shipped' => 0,
|
|
'is_abo' => true,
|
|
'abo_interval' => 5,
|
|
'txaction' => 'error',
|
|
'mode' => 'live',
|
|
]);
|
|
|
|
$aboOrder = UserAboOrder::create([
|
|
'user_abo_id' => $userAbo->id,
|
|
'shopping_order_id' => $order->id,
|
|
'status' => 3, // abo_hold
|
|
'paid' => false,
|
|
]);
|
|
|
|
return [$userAbo, $order, $aboOrder, $user];
|
|
}
|
|
|
|
it('markiert eine angehaltene Abo-Bestellung als extern erledigt, schreibt Punkte gut und hebt die Blockade auf', function () {
|
|
[$userAbo, $order, $aboOrder, $user] = heldAboSeed();
|
|
|
|
// Vorher zaehlt die angehaltene Lieferung nicht als ausgefuehrte Lieferung.
|
|
expect(UserAbo::find($userAbo->id)->getCountPaidOrders())->toBe(0);
|
|
|
|
$result = app(AboExternalCompletionService::class)->complete($order);
|
|
|
|
expect($result['success'])->toBeTrue();
|
|
|
|
$order->refresh();
|
|
$aboOrder->refresh();
|
|
$userAbo->refresh();
|
|
|
|
// Bestellung bleibt unbezahlt, keine Rechnung
|
|
expect((bool) $order->paid)->toBeFalse()
|
|
->and((int) $order->shipped)->toBe(2)
|
|
->and($order->isInvoice())->toBeFalse();
|
|
|
|
// Abo-Bestellung: gruener Status 8, weiterhin unbezahlt
|
|
expect((int) $aboOrder->status)->toBe(8)
|
|
->and((bool) $aboOrder->paid)->toBeFalse();
|
|
|
|
// Blockade aufgehoben: zurueck auf okay, aktiv, next_date in der Zukunft
|
|
expect((int) $userAbo->status)->toBe(2)
|
|
->and($userAbo->active)->toBeTrue();
|
|
expect(Carbon::parse($userAbo->getRawOriginal('next_date'))->isFuture())->toBeTrue();
|
|
|
|
// Abo-Lieferungs-Zaehler beruecksichtigt die extern erledigte Ausfuehrung.
|
|
$fresh = UserAbo::find($userAbo->id);
|
|
expect($fresh->getCountPaidOrders())->toBe(1)
|
|
->and($fresh->getCountOrders())->toBe(1);
|
|
|
|
// Punkte gutgeschrieben - mit heutigem Datum (Markierungsdatum), nicht dem
|
|
// vergangenen Bestelldatum, damit sie in der laufenden Monatsauswertung landen.
|
|
$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)
|
|
->and((int) $volume->month)->toBe((int) date('m'))
|
|
->and((int) $volume->year)->toBe((int) date('Y'))
|
|
->and($volume->date)->toBe(date('d.m.Y'));
|
|
});
|
|
|
|
it('trackt Incentive-Punkte mit dem Bestelldatum (Ausfuehrungsmonat), nicht mit heute', function () {
|
|
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::parse('2026-04-10');
|
|
ShoppingOrder::where('id', $order->id)->update(['created_at' => $executionDate]);
|
|
|
|
Incentive::factory()->active()->create([
|
|
'qualification_start' => '2026-04-01',
|
|
'qualification_end' => '2026-04-30',
|
|
'calculation_end' => '2026-05-31',
|
|
]);
|
|
|
|
app(AboExternalCompletionService::class)->complete($order->fresh());
|
|
|
|
// Incentive-Abo wurde dem April-Fenster zugeordnet, activated_at = Bestelldatum.
|
|
$newAbo = IncentiveNewAbo::where('user_abo_id', $userAbo->id)->first();
|
|
expect($newAbo)->not->toBeNull()
|
|
->and($newAbo->activated_at->toDateString())->toBe('2026-04-10');
|
|
|
|
// Incentive-Zaehler (qualified_abos) des Teilnehmers wurde erhoeht.
|
|
$participant = IncentiveParticipant::find($newAbo->participant_id);
|
|
expect((int) $participant->qualified_abos)->toBe(1);
|
|
|
|
// 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((int) date('m'))
|
|
->and((int) $volume->year)->toBe((int) date('Y'));
|
|
|
|
Carbon::setTestNow();
|
|
});
|
|
|
|
it('ist idempotent und schreibt Punkte nicht doppelt gut', function () {
|
|
[, $order] = heldAboSeed();
|
|
|
|
$service = app(AboExternalCompletionService::class);
|
|
$first = $service->complete($order);
|
|
$second = $service->complete($order->fresh());
|
|
|
|
expect($first['success'])->toBeTrue()
|
|
->and($second['success'])->toBeFalse();
|
|
|
|
expect(UserSalesVolume::where('shopping_order_id', $order->id)->count())->toBe(1);
|
|
});
|
|
|
|
it('verweigert die Aktion fuer eine bereits bezahlte Abo-Bestellung', function () {
|
|
[, $order, $aboOrder] = heldAboSeed();
|
|
$aboOrder->update(['paid' => true]);
|
|
|
|
$result = app(AboExternalCompletionService::class)->complete($order->fresh());
|
|
|
|
expect($result['success'])->toBeFalse();
|
|
expect(UserSalesVolume::where('shopping_order_id', $order->id)->exists())->toBeFalse();
|
|
});
|
|
|
|
it('verweigert die Aktion fuer eine Nicht-Abo-Bestellung', function () {
|
|
[, $order] = heldAboSeed();
|
|
$order->update(['is_abo' => false]);
|
|
|
|
$result = app(AboExternalCompletionService::class)->complete($order->fresh());
|
|
|
|
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();
|
|
|
|
$result = app(AboExternalCompletionService::class)->completeForAbo($userAbo);
|
|
|
|
expect($result['success'])->toBeTrue();
|
|
expect((int) UserAboOrder::where('shopping_order_id', $order->id)->value('status'))->toBe(8);
|
|
expect((int) $userAbo->fresh()->status)->toBe(2);
|
|
});
|