10.April 2026

This commit is contained in:
Kevin Adametz 2026-04-10 17:15:27 +02:00
parent a00c42e770
commit f58c709945
208 changed files with 19280 additions and 2914 deletions

View file

@ -0,0 +1,119 @@
<?php
/**
* Nach fehlgeschlagener Cron-Zahlung kann UserAbo status 3 (abo_hold) sein.
* Späterer Payone-paid (setAboActive) muss das Abo wieder auf abo_okay (2) setzen.
*/
use App\Models\Country;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserShop;
use App\Services\AboHelper;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
it('setzt UserAbo von abo_hold auf abo_okay wenn Zahlung als bezahlt bestaetigt wird', function () {
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
$shipping = Shipping::create([
'name' => 'Standard',
'active' => true,
]);
$shippingCountry = ShippingCountry::create([
'shipping_id' => $shipping->id,
'country_id' => $country->id,
]);
$shopOwner = User::forceCreate([
'email' => 'shop-owner-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userShop = UserShop::create([
'user_id' => $shopOwner->id,
'name' => 'TS'.substr(uniqid('', true), 0, 8),
'slug' => 'ts-'.uniqid(),
'active' => true,
]);
$consultant = User::forceCreate([
'email' => 'consultant-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$customer = User::forceCreate([
'email' => 'customer-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'cust@example.com',
]);
$userAbo = UserAbo::create([
'user_id' => null,
'member_id' => $consultant->id,
'shopping_user_id' => $shoppingUser->id,
'is_for' => 'ot',
'email' => 'abo@example.com',
'payone_userid' => 999001,
'clearingtype' => 'cc',
'active' => true,
'status' => 3,
'abo_interval' => 1,
]);
$shoppingOrder = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'country_id' => $shippingCountry->id,
'user_shop_id' => $userShop->id,
'payment_for' => 6,
'points' => 10,
'is_abo' => true,
'paid' => false,
'txaction' => 'prev',
'mode' => 'test',
'total' => 100,
'subtotal' => 90,
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 3,
'paid' => false,
]);
AboHelper::setAboActive($shoppingOrder, 2, true);
$userAbo->refresh();
expect($userAbo->status)->toBe(2);
});

View file

@ -0,0 +1,100 @@
<?php
use App\Models\UserAbo;
use App\Repositories\AboRepository;
use Carbon\Carbon;
uses(Tests\TestCase::class);
beforeEach(function () {
$this->repository = new AboRepository;
});
/**
* Szenario: Heute = 15. März, next_date = 5. April (März-Ausführung bereits erfolgt).
* Änderung auf Tag 20 neues Datum soll der 20. April sein, NICHT der 20. März.
*/
it('erlaubt Datumsänderung auf den 20. wenn next_date im April liegt und heute der 15. März ist', function () {
Carbon::setTestNow('2026-03-15');
$abo = new UserAbo;
$abo->next_date = '2026-04-05';
$this->repository->setModel($abo);
$newNextDate = invadePrivateMethod($this->repository, 'calculateNewNextDate', [20]);
expect($newNextDate->format('Y-m-d'))->toBe('2026-04-20');
Carbon::setTestNow();
});
/**
* Szenario: Heute = 15. März, next_date = 25. März (im selben Monat, noch nicht ausgeführt).
* Änderung auf Tag 20 20. März ist nur 5 Tage entfernt Referenz bleibt now().
* setNextDate gibt 20. März zurück (5 Tage zu nah).
*/
it('berechnet neues Datum im selben Monat wenn next_date noch im März liegt', function () {
Carbon::setTestNow('2026-03-15');
$abo = new UserAbo;
$abo->next_date = '2026-03-25';
$this->repository->setModel($abo);
$newNextDate = invadePrivateMethod($this->repository, 'calculateNewNextDate', [20]);
expect($newNextDate->format('Y-m-d'))->toBe('2026-03-20');
Carbon::setTestNow();
});
/**
* Szenario: Heute = 15. März, next_date = 5. April.
* Änderung auf Tag 5 neues Datum soll 5. April sein (identisch mit aktuell).
*/
it('setzt Datum auf den 5. April wenn Tag 5 gewählt und next_date im April liegt', function () {
Carbon::setTestNow('2026-03-15');
$abo = new UserAbo;
$abo->next_date = '2026-04-05';
$this->repository->setModel($abo);
$newNextDate = invadePrivateMethod($this->repository, 'calculateNewNextDate', [5]);
expect($newNextDate->format('Y-m-d'))->toBe('2026-04-05');
Carbon::setTestNow();
});
/**
* Szenario: Heute = 15. März, kein next_date gesetzt Referenz ist now().
*/
it('verwendet heute als Referenz wenn kein next_date gesetzt ist', function () {
Carbon::setTestNow('2026-03-15');
$abo = new UserAbo;
$abo->next_date = null;
$this->repository->setModel($abo);
$newNextDate = invadePrivateMethod($this->repository, 'calculateNewNextDate', [20]);
expect($newNextDate->format('Y-m-d'))->toBe('2026-03-20');
Carbon::setTestNow();
});
/**
* Hilfsfunktion zum Aufruf privater Methoden.
*
* @param array<mixed> $args
*/
function invadePrivateMethod(object $object, string $methodName, array $args = []): mixed
{
$reflection = new ReflectionMethod($object, $methodName);
$reflection->setAccessible(true);
return $reflection->invoke($object, ...$args);
}

View file

@ -0,0 +1,71 @@
<?php
use App\Models\Country;
use App\Models\UserAccount;
use App\Models\UserBusiness;
use App\Models\UserLevel;
use App\Services\BusinessPlan\TreeCalcBot;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(Tests\TestCase::class);
uses(RefreshDatabase::class);
it('TreeCalcBot initBusinesslUserDetail terminiert bei Selbst-Sponsor ohne Downline-Rekursion', function () {
$country = Country::create([
'code' => 'AT',
'phone' => '43',
'en' => 'Austria',
'de' => 'Österreich',
'es' => 'Austria',
'fr' => 'Autriche',
'it' => 'Austria',
'ru' => 'Austria',
]);
$level = UserLevel::create([
'name' => 'Berater',
'margin' => 30,
'margin_shop' => 30,
'qual_kp' => 350,
'qual_pp' => 1000,
'pos' => 2,
'active' => true,
]);
$account = UserAccount::create([
'm_account' => random_int(900_000, 999_999),
'first_name' => 'Self',
'last_name' => 'Sponsor',
'country_id' => $country->id,
'shipping_country_id' => $country->id,
]);
$user = User::forceCreate([
'email' => 'self-sponsor-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'account_id' => $account->id,
'm_level' => $level->id,
'm_sponsor' => null,
'payment_account' => '2030-12-31 00:00:00',
'payment_shop' => '2030-12-31 00:00:00',
'active_date' => '2020-01-01 00:00:00',
'admin' => 0,
'confirmed' => 1,
]);
$user->update(['m_sponsor' => $user->id]);
$month = 6;
$year = 2099;
expect(
UserBusiness::where('user_id', $user->id)->where('month', $month)->where('year', $year)->exists()
)->toBeFalse();
$bot = new TreeCalcBot($month, $year, 'member');
$bot->initBusinesslUserDetail($user->fresh(['account', 'user_level', 'user_sponsor']));
expect($bot->business_user->businessUserItems)->toBeEmpty();
});

View file

@ -0,0 +1,117 @@
<?php
use App\Console\Commands\BusinessStoreOptimized;
use App\Models\UserLevel;
use App\Models\UserSalesVolume;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(Tests\TestCase::class, RefreshDatabase::class);
function makeCommand(): BusinessStoreOptimized
{
$command = new BusinessStoreOptimized;
$input = new \Symfony\Component\Console\Input\ArrayInput([]);
$output = new \Illuminate\Console\OutputStyle($input, new \Symfony\Component\Console\Output\NullOutput);
$command->setOutput($output);
return $command;
}
function callAssignMethod(BusinessStoreOptimized $command): void
{
$method = (new ReflectionClass($command))->getMethod('assignMonthlyQualKpBonusPoints');
$method->setAccessible(true);
$method->invoke($command);
}
function makeUserLevel(int $qualKp, int $pos = 7): UserLevel
{
return UserLevel::create([
'name' => 'Test Level '.$pos,
'qual_kp' => $qualKp,
'active' => 1,
'pos' => $pos,
'default' => 0,
]);
}
function makeUser(int $levelId, ?int $forceId = null): User
{
$attributes = [
'email' => fake()->unique()->safeEmail(),
'password' => bcrypt('secret'),
'lang' => 'de',
'm_level' => $levelId,
];
if ($forceId !== null) {
$attributes['id'] = $forceId;
}
return User::forceCreate($attributes);
}
it('assigns qual_kp points to user 486 when their level has qual_kp > 0', function () {
$level = makeUserLevel(690);
makeUser($level->id, 486);
callAssignMethod(makeCommand());
expect(UserSalesVolume::where('user_id', 486)
->where('month', date('m'))
->where('year', date('Y'))
->where('info', 'qual_kp_bonus')
->where('points', 690)
->exists()
)->toBeTrue();
});
it('does not assign bonus points to users not in the bonus list', function () {
$level = makeUserLevel(690);
$otherUser = makeUser($level->id);
callAssignMethod(makeCommand());
expect(UserSalesVolume::where('user_id', $otherUser->id)->where('info', 'qual_kp_bonus')->exists())
->toBeFalse();
});
it('skips user 486 when their level has qual_kp of zero', function () {
$level = makeUserLevel(0);
makeUser($level->id, 486);
callAssignMethod(makeCommand());
expect(UserSalesVolume::where('user_id', 486)->where('info', 'qual_kp_bonus')->exists())
->toBeFalse();
});
it('skips user 486 when they have no level assigned', function () {
User::forceCreate([
'id' => 486,
'email' => fake()->unique()->safeEmail(),
'password' => bcrypt('secret'),
'lang' => 'de',
'm_level' => null,
]);
callAssignMethod(makeCommand());
expect(UserSalesVolume::where('user_id', 486)->where('info', 'qual_kp_bonus')->exists())
->toBeFalse();
});
it('is idempotent and does not create duplicate entries for the same month', function () {
$level = makeUserLevel(690);
makeUser($level->id, 486);
$command = makeCommand();
callAssignMethod($command);
callAssignMethod($command);
expect(UserSalesVolume::where('user_id', 486)
->where('info', 'qual_kp_bonus')
->count()
)->toBe(1);
});

View file

@ -0,0 +1,316 @@
<?php
/**
* Validiert IncentiveTracker::trackSalesVolume für Umsatz aus einer weiteren Abo-Bestellung
* (gleicher Kunde/shopping_user_id wie beim Neuabo). Rechnung und UserSalesVolume entstehen
* über Payment::paymentStatusPaidAction / InvoiceRepository::createAndSalesVolume (Payone-API),
* nicht über user:make_abo_order.
*/
use App\Models\Country;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveParticipant;
use App\Models\IncentivePointsLog;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserSalesVolume;
use App\Models\UserShop;
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-05-15 10:00:00'));
});
afterEach(function () {
Carbon::setTestNow();
});
it('addiert Umsatzpunkte beim Berater wenn UserSalesVolume zur Abo-Verlängerung gehört (trackSalesVolume Pfad Neuabo)', function () {
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
$shipping = Shipping::create([
'name' => 'Standard',
'active' => true,
]);
$shippingCountry = ShippingCountry::create([
'shipping_id' => $shipping->id,
'country_id' => $country->id,
]);
$shopOwner = User::forceCreate([
'email' => 'shop-owner-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userShop = UserShop::create([
'user_id' => $shopOwner->id,
'name' => 'TS'.substr(uniqid('', true), 0, 8),
'slug' => 'ts-'.uniqid(),
'active' => true,
]);
$consultant = User::forceCreate([
'email' => 'consultant-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$customer = User::forceCreate([
'email' => 'customer-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'cust@example.com',
]);
$userAbo = UserAbo::create([
'user_id' => null,
'member_id' => $consultant->id,
'shopping_user_id' => $shoppingUser->id,
'is_for' => 'ot',
'email' => 'abo@example.com',
'payone_userid' => 999001,
'clearingtype' => 'cc',
'active' => true,
'status' => 2,
'abo_interval' => 1,
]);
$incentive = Incentive::factory()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-07-31',
'calculation_end' => '2026-08-31',
'status' => 1,
]);
$participant = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $consultant->id,
]);
IncentiveNewAbo::create([
'participant_id' => $participant->id,
'user_abo_id' => $userAbo->id,
'activated_at' => Carbon::now()->subMonth(),
]);
$shoppingOrder = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $customer->id,
'country_id' => $shippingCountry->id,
'user_shop_id' => $userShop->id,
'payment_for' => 6,
'points' => 75,
'is_abo' => true,
'paid' => true,
'txaction' => 'paid',
'mode' => 'test',
'total' => 100,
'subtotal' => 90,
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 2,
'paid' => true,
]);
$userSalesVolume = UserSalesVolume::create([
'user_id' => $customer->id,
'shopping_order_id' => $shoppingOrder->id,
'month' => 5,
'year' => 2026,
'date' => '15.05.2026',
'points' => 75,
'total_net' => 90,
'status_points' => 1,
'status' => 2,
'message' => 'Abo Verlängerung Test',
]);
IncentiveTracker::trackSalesVolume($userSalesVolume);
$participant->refresh();
expect(
IncentivePointsLog::where('participant_id', $participant->id)
->where('type', 'abo')
->where('user_sales_volume_id', $userSalesVolume->id)
->where('points_accumulated', 75)
->exists()
)->toBeTrue()
->and($participant->total_points)->toBeGreaterThan(0);
});
it('addiert Umsatzpunkte bei Verlaengerung auch wenn shopping_order einen replizierten ShoppingUser hat', function () {
$country = Country::create([
'code' => 'CH',
'phone' => '41',
'en' => 'Switzerland',
'de' => 'Schweiz',
'es' => 'Suiza',
'fr' => 'Suisse',
'it' => 'Svizzera',
'ru' => 'Швейцария',
]);
$shipping = Shipping::create([
'name' => 'Std-CH',
'active' => true,
]);
$shippingCountry = ShippingCountry::create([
'shipping_id' => $shipping->id,
'country_id' => $country->id,
]);
$shopOwner = User::forceCreate([
'email' => 'so-ch-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userShop = UserShop::create([
'user_id' => $shopOwner->id,
'name' => 'TSCH'.substr(uniqid('', true), 0, 6),
'slug' => 'tsch-'.uniqid(),
'active' => true,
]);
$consultant = User::forceCreate([
'email' => 'consultant-ch-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$customer = User::forceCreate([
'email' => 'customer-ch-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUserStamm = ShoppingUser::create([
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'cust-ch-'.uniqid('', true).'@example.com',
]);
$shoppingUserReplica = ShoppingUser::create([
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => $shoppingUserStamm->billing_email,
]);
$userAbo = UserAbo::create([
'user_id' => null,
'member_id' => $consultant->id,
'shopping_user_id' => $shoppingUserStamm->id,
'is_for' => 'ot',
'email' => $shoppingUserStamm->billing_email,
'payone_userid' => 999101,
'clearingtype' => 'cc',
'active' => true,
'status' => 2,
'abo_interval' => 1,
]);
$incentive = Incentive::factory()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-07-31',
'calculation_end' => '2026-08-31',
'status' => 1,
]);
$participant = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $consultant->id,
]);
IncentiveNewAbo::create([
'participant_id' => $participant->id,
'user_abo_id' => $userAbo->id,
'activated_at' => Carbon::now()->subMonth(),
]);
$shoppingOrder = ShoppingOrder::create([
'shopping_user_id' => $shoppingUserReplica->id,
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'country_id' => $shippingCountry->id,
'user_shop_id' => $userShop->id,
'payment_for' => 6,
'points' => 40,
'is_abo' => true,
'paid' => true,
'txaction' => 'paid',
'mode' => 'test',
'total' => 100,
'subtotal' => 90,
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 2,
'paid' => true,
]);
expect($shoppingOrder->shopping_user_id)->not->toBe($userAbo->shopping_user_id);
$userSalesVolume = UserSalesVolume::create([
'user_id' => $customer->id,
'shopping_order_id' => $shoppingOrder->id,
'month' => 5,
'year' => 2026,
'date' => '15.05.2026',
'points' => 40,
'total_net' => 90,
'status_points' => 1,
'status' => 2,
'message' => 'Abo Verlängerung Replikat',
]);
IncentiveTracker::trackSalesVolume($userSalesVolume);
$participant->refresh();
expect(
IncentivePointsLog::where('participant_id', $participant->id)
->where('type', 'abo')
->where('user_sales_volume_id', $userSalesVolume->id)
->where('points_accumulated', 40)
->exists()
)->toBeTrue();
});

View file

@ -0,0 +1,120 @@
<?php
use App\Models\Country;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveParticipant;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserShop;
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-05-15 10:00:00'));
});
afterEach(function () {
Carbon::setTestNow();
});
it('trackAboActivated erfasst Berater-Eigenabo (is_for me) wie ein Kundenabo', function () {
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
$shipping = Shipping::create(['name' => 'S', 'active' => true]);
$shippingCountry = ShippingCountry::create(['shipping_id' => $shipping->id, 'country_id' => $country->id]);
$consultant = User::forceCreate([
'email' => 'co-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userShop = UserShop::create([
'user_id' => $consultant->id,
'name' => 'TS'.substr(uniqid('', true), 0, 6),
'slug' => 'ts-'.uniqid(),
'active' => true,
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $consultant->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'me-'.uniqid('', true).'@example.com',
'is_from' => 'wizard',
]);
$userAbo = UserAbo::create([
'user_id' => $consultant->id,
'member_id' => null,
'shopping_user_id' => $shoppingUser->id,
'is_for' => 'me',
'email' => $shoppingUser->billing_email,
'payone_userid' => 999002,
'clearingtype' => 'cc',
'active' => true,
'status' => 2,
'abo_interval' => 1,
]);
$incentive = Incentive::factory()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-07-31',
'calculation_end' => '2026-08-31',
'status' => 1,
]);
$participant = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $consultant->id,
]);
$shoppingOrder = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $consultant->id,
'country_id' => $shippingCountry->id,
'user_shop_id' => $userShop->id,
'payment_for' => 6,
'points' => 50,
'is_abo' => true,
'paid' => true,
'txaction' => 'paid',
'mode' => 'test',
'total' => 100,
'subtotal' => 90,
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 2,
'paid' => true,
]);
IncentiveTracker::trackAboActivated($shoppingOrder->fresh());
expect(
IncentiveNewAbo::where('participant_id', $participant->id)
->where('user_abo_id', $userAbo->id)
->exists()
)->toBeTrue();
});

View file

@ -0,0 +1,110 @@
<?php
use App\Models\Country;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveParticipant;
use App\Models\IncentivePointsLog;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
uses(RefreshDatabase::class);
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-05-15 10:00:00'));
});
afterEach(function () {
Carbon::setTestNow();
});
it('zieht Eigenabo vor Qualifikationsbeginn bei incentive:calculate ohne skip-repair nach', function () {
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
$consultant = User::forceCreate([
'email' => 'co-calc-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $consultant->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'me-'.uniqid('', true).'@example.com',
'is_from' => 'wizard',
]);
$userAbo = UserAbo::create([
'user_id' => $consultant->id,
'member_id' => null,
'shopping_user_id' => $shoppingUser->id,
'is_for' => 'me',
'email' => $shoppingUser->billing_email,
'payone_userid' => 999002,
'clearingtype' => 'cc',
'active' => true,
'status' => 2,
'abo_interval' => 1,
]);
$userAbo->forceFill([
'created_at' => Carbon::parse('2026-01-15 12:00:00'),
'updated_at' => Carbon::parse('2026-01-15 12:00:00'),
])->save();
$incentive = Incentive::factory()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-07-31',
'calculation_end' => '2026-08-31',
'status' => 1,
'points_abo_onetime' => 400,
]);
$participant = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $consultant->id,
]);
$exitCode = Artisan::call('incentive:calculate', [
'incentive_id' => (string) $incentive->id,
]);
expect($exitCode)->toBe(0);
$participant->refresh();
expect(
IncentiveNewAbo::query()
->where('participant_id', $participant->id)
->where('user_abo_id', $userAbo->id)
->exists()
)->toBeTrue();
$log = IncentivePointsLog::query()
->where('participant_id', $participant->id)
->where('type', 'abo')
->where('source_id', $userAbo->id)
->first();
expect($log)->not->toBeNull()
->and($log->month)->toBe(4)
->and($log->year)->toBe(2026)
->and($log->points_onetime)->toBe(400);
expect($participant->total_points)->toBe(400);
});

View file

@ -0,0 +1,188 @@
<?php
/**
* Detailansicht: Akkumulierte Abo-Punkte muessen dem richtigen Neuabo zugeordnet werden,
* auch wenn die Verlaengerungsbestellung einen anderen shopping_user_id hat (Replikat).
*/
use App\Http\Controllers\Admin\IncentiveController;
use App\Models\Country;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveParticipant;
use App\Models\IncentivePointsLog;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserSalesVolume;
use App\Models\UserShop;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-05-15 12:00:00'));
});
afterEach(function () {
Carbon::setTestNow();
});
it('ordnet akkumulierte Abo-Punkte dem Neuabo ueber user_abo_id zu (Replikat-ShoppingUser)', function () {
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
$shipping = Shipping::create([
'name' => 'Standard',
'active' => true,
]);
$shippingCountry = ShippingCountry::create([
'shipping_id' => $shipping->id,
'country_id' => $country->id,
]);
$shopOwner = User::forceCreate([
'email' => 'shop-owner-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userShop = UserShop::create([
'user_id' => $shopOwner->id,
'name' => 'TS'.substr(uniqid('', true), 0, 8),
'slug' => 'ts-'.uniqid(),
'active' => true,
]);
$consultant = User::forceCreate([
'email' => 'consultant-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$customer = User::forceCreate([
'email' => 'customer-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUserStamm = ShoppingUser::create([
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'cust-detail-'.uniqid('', true).'@example.com',
]);
$shoppingUserReplica = ShoppingUser::create([
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => $shoppingUserStamm->billing_email,
]);
$userAbo = UserAbo::create([
'user_id' => null,
'member_id' => $consultant->id,
'shopping_user_id' => $shoppingUserStamm->id,
'is_for' => 'ot',
'email' => $shoppingUserStamm->billing_email,
'payone_userid' => 999201,
'clearingtype' => 'cc',
'active' => true,
'status' => 2,
'abo_interval' => 1,
]);
$incentive = Incentive::factory()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-07-31',
'calculation_end' => '2026-08-31',
'status' => 1,
'points_abo_onetime' => 100,
]);
$participant = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $consultant->id,
]);
$newAbo = IncentiveNewAbo::create([
'participant_id' => $participant->id,
'user_abo_id' => $userAbo->id,
'activated_at' => Carbon::parse('2026-04-10'),
]);
$shoppingOrder = ShoppingOrder::create([
'shopping_user_id' => $shoppingUserReplica->id,
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'country_id' => $shippingCountry->id,
'user_shop_id' => $userShop->id,
'payment_for' => 6,
'points' => 42,
'is_abo' => true,
'paid' => true,
'txaction' => 'paid',
'mode' => 'test',
'total' => 100,
'subtotal' => 90,
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 2,
'paid' => true,
]);
$userSalesVolume = UserSalesVolume::create([
'user_id' => $customer->id,
'shopping_order_id' => $shoppingOrder->id,
'month' => 5,
'year' => 2026,
'date' => '15.05.2026',
'points' => 42,
'total_net' => 90,
'status_points' => 1,
'status' => 2,
'message' => 'Verlängerung',
]);
IncentivePointsLog::create([
'participant_id' => $participant->id,
'type' => 'abo',
'source_type' => UserSalesVolume::class,
'source_id' => $userSalesVolume->id,
'source_label' => 'SV Test',
'month' => 5,
'year' => 2026,
'points_onetime' => 0,
'points_accumulated' => 42,
'user_sales_volume_id' => $userSalesVolume->id,
'incentive_new_abo_id' => $newAbo->id,
'is_storno' => false,
]);
$data = IncentiveController::buildParticipantDetailData($participant->fresh(['incentive', 'user']));
$aboRow = $data['abo_sources']->first();
expect($aboRow)->not->toBeNull()
->and($aboRow['id'])->toBe($newAbo->id)
->and(array_sum($aboRow['monthly']))->toBe(42);
});

View file

@ -0,0 +1,181 @@
<?php
use App\Http\Controllers\User\IncentiveController;
use App\Models\Incentive;
use App\Models\IncentiveParticipant;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('sortiert Teilnehmer mit Rang 1,2,… und ohne Rang zuletzt', function () {
$incentive = Incentive::factory()->create();
$makeUser = fn () => User::forceCreate([
'email' => 't-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userNoRank = $makeUser();
$userRank2 = $makeUser();
$userRank1 = $makeUser();
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $userNoRank->id,
'rank' => null,
]);
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $userRank2->id,
'rank' => 2,
]);
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $userRank1->id,
'rank' => 1,
]);
$orderedUserIds = IncentiveParticipant::query()
->where('incentive_id', $incentive->id)
->orderByIncentiveLeaderboard()
->pluck('user_id')
->all();
expect($orderedUserIds)->toBe([$userRank1->id, $userRank2->id, $userNoRank->id]);
});
it('sortiert qualifizierte Teilnehmer vor nicht qualifizierten, unabhängig von Punkten und Rang', function () {
$incentive = Incentive::factory()->create();
$makeUser = fn () => User::forceCreate([
'email' => 't-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userNotQualified = $makeUser();
$userQualified = $makeUser();
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $userNotQualified->id,
'is_qualified' => false,
'total_points' => 10_000,
'rank' => 1,
]);
IncentiveParticipant::factory()->qualified()->create([
'incentive_id' => $incentive->id,
'user_id' => $userQualified->id,
'total_points' => 500,
'rank' => 2,
]);
$orderedUserIds = IncentiveParticipant::query()
->where('incentive_id', $incentive->id)
->orderByIncentiveLeaderboard()
->pluck('user_id')
->all();
expect($orderedUserIds)->toBe([$userQualified->id, $userNotQualified->id]);
});
it('sortiert bei Punktgleichstand Teilnehmer mit Klarnamen (bestaetigte Teilnahme) vor anonymen', function () {
$incentive = Incentive::factory()->create();
$makeUser = fn () => User::forceCreate([
'email' => 't-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$anonymous = IncentiveParticipant::factory()->unconfirmed()->create([
'incentive_id' => $incentive->id,
'user_id' => $makeUser()->id,
'total_points' => 500,
'rank' => 5,
]);
$confirmed = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $makeUser()->id,
'accepted_terms_at' => now(),
'total_points' => 500,
'rank' => 5,
]);
$orderedUserIds = IncentiveParticipant::query()
->where('incentive_id', $incentive->id)
->orderByIncentiveLeaderboard()
->pluck('user_id')
->all();
expect($orderedUserIds)->toBe([$confirmed->user_id, $anonymous->user_id]);
});
it('begrenzt die User-Rangliste auf 30 Plaetze (Gewinner-Zone bleibt max_winners)', function () {
$incentive = Incentive::factory()->create(['max_winners' => 20]);
$makeUser = fn () => User::forceCreate([
'email' => 't-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
foreach (range(1, 35) as $i) {
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $makeUser()->id,
'total_points' => 1000 - $i,
]);
}
$ranking = IncentiveParticipant::where('incentive_id', $incentive->id)
->withRankingActivity()
->orderByIncentiveLeaderboard()
->limit(IncentiveController::USER_RANKING_DISPLAY_LIMIT)
->get();
expect($ranking)->toHaveCount(30);
});
it('blendet in der User-Ranglogik Teilnehmer ohne Partner, Abo und Punkte aus', function () {
$incentive = Incentive::factory()->create();
$makeUser = fn () => User::forceCreate([
'email' => 't-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $makeUser()->id,
'qualified_partners' => 0,
'qualified_abos' => 0,
'total_points' => 0,
]);
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $makeUser()->id,
'qualified_partners' => 0,
'qualified_abos' => 0,
'total_points' => 100,
]);
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $makeUser()->id,
'qualified_partners' => 1,
'qualified_abos' => 0,
'total_points' => 0,
]);
$ids = IncentiveParticipant::query()
->where('incentive_id', $incentive->id)
->withRankingActivity()
->orderBy('id')
->pluck('id')
->all();
expect($ids)->toHaveCount(2);
});

View file

@ -0,0 +1,177 @@
<?php
use App\Models\Country;
use App\Models\Incentive;
use App\Models\IncentiveNewPartner;
use App\Models\IncentiveParticipant;
use App\Models\Product;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingOrderItem;
use App\Models\ShoppingUser;
use App\Models\UserShop;
use App\Services\Incentive\IncentiveTracker;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
uses(RefreshDatabase::class);
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-05-15 12:00:00'));
});
afterEach(function () {
Carbon::setTestNow();
});
function createWizardRegistrationOrderWithProducts(User $registrant, User $shopOwner, array $products): ShoppingOrder
{
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
$shipping = Shipping::create(['name' => 'S', 'active' => true]);
$shippingCountry = ShippingCountry::create(['shipping_id' => $shipping->id, 'country_id' => $country->id]);
$userShop = UserShop::create([
'user_id' => $shopOwner->id,
'name' => 'T'.substr(uniqid('', true), 0, 6),
'slug' => 't-'.uniqid(),
'active' => true,
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $registrant->id,
'member_id' => $shopOwner->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'su-'.uniqid('', true).'@example.com',
'is_from' => 'wizard',
]);
$order = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $registrant->id,
'country_id' => $shippingCountry->id,
'user_shop_id' => $userShop->id,
'payment_for' => 1,
'paid' => true,
'txaction' => 'paid',
'mode' => 'test',
'total' => 100,
'subtotal' => 90,
]);
foreach ($products as $product) {
ShoppingOrderItem::create([
'shopping_order_id' => $order->id,
'product_id' => $product->id,
'qty' => 1,
'price' => 50,
]);
}
return $order->fresh(['shopping_order_items.product']);
}
function createProductForTest(bool $membershipOnly): Product
{
$id = DB::table('products')->insertGetId([
'name' => 'P '.uniqid(),
'title' => 'T',
'is_membership_only' => $membershipOnly,
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
return Product::query()->findOrFail($id);
}
it('trackNewPartner legt keinen Neupartner an bei reiner Mitgliedschaft ohne Starterpaket', function () {
$sponsor = User::forceCreate([
'email' => 'sp-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$newPartner = User::forceCreate([
'email' => 'np-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'm_sponsor' => $sponsor->id,
]);
$membershipProduct = createProductForTest(membershipOnly: true);
$order = createWizardRegistrationOrderWithProducts($newPartner, $sponsor, [$membershipProduct]);
expect($order->qualifiesForIncentiveTrackedPartner())->toBeFalse();
$incentive = Incentive::factory()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-07-31',
'calculation_end' => '2026-08-31',
'status' => 1,
]);
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $sponsor->id,
]);
IncentiveTracker::trackNewPartner($order);
expect(
IncentiveNewPartner::where('user_id', $newPartner->id)->exists()
)->toBeFalse();
});
it('trackNewPartner legt Neupartner an wenn Registrierung ein Starterpaket enthält', function () {
$sponsor = User::forceCreate([
'email' => 'sp-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$newPartner = User::forceCreate([
'email' => 'np-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'm_sponsor' => $sponsor->id,
]);
$starterProduct = createProductForTest(membershipOnly: false);
$order = createWizardRegistrationOrderWithProducts($newPartner, $sponsor, [$starterProduct]);
expect($order->qualifiesForIncentiveTrackedPartner())->toBeTrue();
$incentive = Incentive::factory()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-07-31',
'calculation_end' => '2026-08-31',
'status' => 1,
]);
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $sponsor->id,
]);
IncentiveTracker::trackNewPartner($order);
expect(
IncentiveNewPartner::where('user_id', $newPartner->id)->exists()
)->toBeTrue();
});

View file

@ -0,0 +1,37 @@
<?php
use App\Models\Incentive;
use App\Models\IncentiveParticipant;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('markiert Teilnehmer ohne Teilnahmebestaetigung als nicht oeffentlich (hasAcceptedTerms)', function () {
$incentive = Incentive::factory()->create();
$userA = User::forceCreate([
'email' => 'a-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userB = User::forceCreate([
'email' => 'b-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$anonymous = IncentiveParticipant::factory()->unconfirmed()->create([
'incentive_id' => $incentive->id,
'user_id' => $userA->id,
]);
$confirmed = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $userB->id,
'accepted_terms_at' => now(),
]);
expect($anonymous->hasAcceptedTerms())->toBeFalse()
->and($confirmed->hasAcceptedTerms())->toBeTrue();
});

View file

@ -0,0 +1,90 @@
<?php
use App\Http\Controllers\User\IncentiveController;
use App\Models\Incentive;
use App\Models\IncentiveParticipant;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('collectGalleryImages returns an array of image paths excluding the hero image', function () {
$incentive = Incentive::factory()->active()->create([
'image' => 'nikki-beach.jpg',
]);
$controller = new IncentiveController;
$method = new ReflectionMethod($controller, 'collectGalleryImages');
$method->setAccessible(true);
$result = $method->invoke($controller, $incentive);
expect($result)->toBeArray();
expect($result)->not->toContain('img/incentive/nikki-beach.jpg');
foreach ($result as $path) {
expect($path)->toStartWith('img/incentive/');
}
});
it('collectGalleryImages returns empty array when directory does not exist', function () {
$incentive = Incentive::factory()->active()->create([
'image' => 'nonexistent-hero.jpg',
]);
$controller = new IncentiveController;
$method = new ReflectionMethod($controller, 'collectGalleryImages');
$method->setAccessible(true);
$result = $method->invoke($controller, $incentive);
expect($result)->toBeArray();
});
it('collectGalleryImages returns sorted results', function () {
$incentive = Incentive::factory()->active()->create([
'image' => 'nikki-beach.jpg',
]);
$controller = new IncentiveController;
$method = new ReflectionMethod($controller, 'collectGalleryImages');
$method->setAccessible(true);
$result = $method->invoke($controller, $incentive);
$sorted = $result;
sort($sorted);
expect($result)->toBe($sorted);
});
it('teaser view receives galleryImages and participant data', function () {
$incentive = Incentive::factory()->active()->create();
$user = User::forceCreate([
'email' => 'teaser-' . uniqid('', true) . '@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'active' => 1,
]);
$participant = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $user->id,
'accepted_terms_at' => now(),
]);
$this->actingAs($user);
$controller = new IncentiveController;
$response = $controller->teaser($incentive->slug);
$data = $response->getData();
expect($data)->toHaveKeys(['incentive', 'participant', 'galleryImages'])
->and($data['incentive']->id)->toBe($incentive->id)
->and($data['participant']->id)->toBe($participant->id)
->and($data['galleryImages'])->toBeArray();
});

View file

@ -0,0 +1,25 @@
<?php
uses(Tests\TestCase::class);
it('does not throw when session locale is a malicious string', function () {
$response = $this->withSession(['locale' => '-1 or 5*5=25 --'])
->get('/impressum');
$response->assertSuccessful();
});
it('applies a valid session locale', function () {
$response = $this->withSession(['locale' => 'en'])
->get('/impressum');
$response->assertSuccessful();
expect(app()->getLocale())->toBe('en');
});
it('clears invalid session locale', function () {
$this->withSession(['locale' => '-1 or 5*5=25 --'])
->get('/impressum');
expect(session('locale'))->toBeNull();
});

View file

@ -0,0 +1,140 @@
<?php
/**
* Mitgliedschaftsprodukte (is_membership_only) dürfen nie qty > 1 haben.
*
* Absicherung in:
* - MembershipController::storePayment() qty wird auf 1 erzwungen
* - CardController::updateCard() qty-Änderung wird auf 1 begrenzt
*/
use App\Http\Controllers\Web\CardController;
use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
beforeEach(function () {
Schema::connection('sqlite')->table('products', function ($table) {
if (! Schema::connection('sqlite')->hasColumn('products', 'slug')) {
$table->string('slug')->nullable();
}
});
});
function createMembershipProduct(): Product
{
return Product::forceCreate([
'name' => 'MIVITA BUSINESS Paket',
'title' => 'Test',
'price' => 69.90,
'tax' => 19.00,
'active' => true,
'is_membership_only' => true,
'pos' => 1,
'show_at' => 3,
'show_on' => '["7","8"]',
'identifier' => 'show_order',
'action' => '["0","1"]',
'no_commission' => true,
'no_free_shipping' => false,
'free_shipping_consultant' => false,
'weight' => 0,
'points' => 0,
'slug' => 'mivita-business-paket',
]);
}
function createRegularProduct(): Product
{
return Product::forceCreate([
'name' => 'Aloe Vera Creme',
'title' => 'Test',
'price' => 29.90,
'tax' => 19.00,
'active' => true,
'is_membership_only' => false,
'pos' => 2,
'show_at' => 1,
'no_commission' => false,
'no_free_shipping' => false,
'free_shipping_consultant' => false,
'weight' => 200,
'points' => 10,
'slug' => 'aloe-vera-creme',
]);
}
it('begrenzt Mitgliedschaftsprodukt auf qty=1 bei CardController updateCard', function () {
$product = createMembershipProduct();
$cartItem = \Yard::instance('webshop')->add(
$product->id,
$product->name,
1,
$product->price,
false,
false,
['slug' => $product->slug, 'weight' => 0, 'points' => 0, 'no_commission' => true, 'no_free_shipping' => false, 'show_on' => $product->show_on]
);
$request = new Request(['quantity' => [$cartItem->rowId => 5]]);
app()->instance('request', $request);
\Request::swap($request);
$controller = new CardController;
$controller->updateCard();
$updatedItem = \Yard::instance('webshop')->get($cartItem->rowId);
expect($updatedItem->qty)->toBe(1);
\Yard::instance('webshop')->destroy();
});
it('laesst qty-Aenderung fuer normale Produkte in CardController updateCard zu', function () {
$product = createRegularProduct();
$cartItem = \Yard::instance('webshop')->add(
$product->id,
$product->name,
1,
$product->price,
false,
false,
['slug' => $product->slug, 'weight' => 200, 'points' => 10, 'no_commission' => false, 'no_free_shipping' => false]
);
$request = new Request(['quantity' => [$cartItem->rowId => 4]]);
app()->instance('request', $request);
\Request::swap($request);
$controller = new CardController;
$controller->updateCard();
$updatedItem = \Yard::instance('webshop')->get($cartItem->rowId);
expect($updatedItem->qty)->toBe(4);
\Yard::instance('webshop')->destroy();
});
it('erzwingt qty=1 im MembershipController fuer is_membership_only Produkte', function () {
$product = createMembershipProduct();
$qty_from_request = 5;
$enforced_qty = $product->is_membership_only ? 1 : $qty_from_request;
expect($enforced_qty)->toBe(1);
});
it('nutzt Request-qty im MembershipController fuer nicht-membership Produkte', function () {
$product = createRegularProduct();
$qty_from_request = 3;
$enforced_qty = $product->is_membership_only ? 1 : $qty_from_request;
expect($enforced_qty)->toBe(3);
});

View file

@ -0,0 +1,152 @@
<?php
/**
* @see \App\Console\Commands\RepairMissingAboFromOrders
*/
use App\Models\Country;
use App\Models\PaymentTransaction;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingPayment;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserShop;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function repairMissingAboSeedOrder(): ShoppingOrder
{
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
$shipping = Shipping::create([
'name' => 'Standard',
'active' => true,
]);
$shippingCountry = ShippingCountry::create([
'shipping_id' => $shipping->id,
'country_id' => $country->id,
]);
$shopOwner = User::forceCreate([
'email' => 'shop-owner-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userShop = UserShop::create([
'user_id' => $shopOwner->id,
'name' => 'TS'.substr(uniqid('', true), 0, 8),
'slug' => 'ts-'.uniqid(),
'active' => true,
]);
$consultant = User::forceCreate([
'email' => 'consultant-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$customer = User::forceCreate([
'email' => 'customer-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'cust-'.uniqid('', true).'@example.com',
'is_for' => 'ot',
]);
$shoppingOrder = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'country_id' => $shippingCountry->id,
'user_shop_id' => $userShop->id,
'payment_for' => 6,
'points' => 10,
'is_abo' => true,
'abo_interval' => 15,
'paid' => true,
'txaction' => 'paid',
'mode' => 'test',
'total' => 100,
'subtotal' => 90,
]);
$shoppingPayment = ShoppingPayment::create([
'shopping_order_id' => $shoppingOrder->id,
'reference' => 'ref-'.uniqid(),
'amount' => 10000,
'currency' => 'EUR',
'clearingtype' => 'cc',
'abo_interval' => 15,
'mode' => 'test',
]);
PaymentTransaction::create([
'shopping_payment_id' => $shoppingPayment->id,
'request' => 'transaction',
'txid' => 123456,
'userid' => 987654,
'status' => 'PAYONE',
'txaction' => 'paid',
'mode' => 'test',
]);
return $shoppingOrder;
}
it('zeigt fehlende Abo-Verknuepfung im Trockenlauf ohne Aenderung', function () {
$order = repairMissingAboSeedOrder();
expect(UserAbo::count())->toBe(0);
$exit = Artisan::call('abo:repair-missing', [
'--order' => (string) $order->id,
'--mode' => 'test',
]);
expect($exit)->toBe(0);
expect(UserAbo::count())->toBe(0);
});
it('repariert fehlende UserAbo-Verknuepfung mit --fix --force', function () {
$order = repairMissingAboSeedOrder();
expect(UserAboOrder::where('shopping_order_id', $order->id)->exists())->toBeFalse();
$exit = Artisan::call('abo:repair-missing', [
'--fix' => true,
'--force' => true,
'--order' => (string) $order->id,
'--mode' => 'test',
]);
expect($exit)->toBe(0);
$order->refresh();
expect($order->getUserAbo())->not->toBeNull();
expect($order->getUserAbo()->status)->toBe(2);
});

View file

@ -0,0 +1,171 @@
<?php
use App\Models\Country;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserShop;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function createPaypalHoldAbo(array $overrides = []): array
{
$country = Country::firstOrCreate(['code' => 'DE'], [
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
Shipping::firstOrCreate(['name' => 'Standard'], ['active' => true]);
$shipping = Shipping::where('name', 'Standard')->first();
ShippingCountry::firstOrCreate([
'shipping_id' => $shipping->id,
'country_id' => $country->id,
]);
$shopOwner = User::forceCreate([
'email' => 'shop-owner-'.uniqid('', true).'@test.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$uniqueSuffix = uniqid('', true);
$userShop = UserShop::create([
'user_id' => $shopOwner->id,
'name' => 'TS'.$uniqueSuffix,
'slug' => 'ts-'.$uniqueSuffix,
'active' => true,
]);
$consultant = User::forceCreate([
'email' => 'berater-'.uniqid('', true).'@test.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $consultant->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'abo-'.uniqid().'@test.com',
'is_for' => 'me',
'is_from' => 'user_order',
]);
$referenceOrder = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $consultant->id,
'member_id' => $consultant->id,
'country_id' => $country->id,
'user_shop_id' => $userShop->id,
'payment_for' => 3,
'points' => 10,
'is_abo' => true,
'paid' => true,
'txaction' => 'paid',
'mode' => 'test',
'total' => 100,
'subtotal' => 90,
'total_shipping' => 105,
]);
$aboData = array_merge([
'user_id' => $consultant->id,
'member_id' => $consultant->id,
'shopping_user_id' => $shoppingUser->id,
'is_for' => 'me',
'email' => $shoppingUser->billing_email,
'payone_userid' => rand(100000, 999999),
'clearingtype' => 'wlt',
'wallettype' => 'PPE',
'active' => true,
'status' => 3,
'abo_interval' => 5,
'start_date' => '2026-01-05',
'last_date' => '2026-04-05',
'next_date' => '2026-04-05',
], $overrides);
$userAbo = UserAbo::create($aboData);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $referenceOrder->id,
'status' => 3,
'paid' => false,
]);
return [
'userAbo' => $userAbo,
'consultant' => $consultant,
'shoppingUser' => $shoppingUser,
'referenceOrder' => $referenceOrder,
'country' => $country,
'userShop' => $userShop,
];
}
it('findet nur PayPal-Abos mit Status 3 und next_date 2026-04-05 im Dry-Run', function () {
createPaypalHoldAbo();
createPaypalHoldAbo(['clearingtype' => 'cc', 'wallettype' => null]);
createPaypalHoldAbo(['status' => 2]);
createPaypalHoldAbo(['next_date' => '2026-05-05']);
$this->artisan('abo:retry-failed-paypal', ['--dry-run' => true])
->assertSuccessful()
->expectsOutputToContain('Betroffene Abos: 1');
});
it('meldet wenn keine betroffenen Abos vorhanden sind', function () {
$this->artisan('abo:retry-failed-paypal', ['--dry-run' => true])
->assertSuccessful()
->expectsOutputToContain('Keine betroffenen PayPal-Abos');
});
it('überspringt Abos die heute bereits bezahlt wurden', function () {
$data = createPaypalHoldAbo();
$abo = $data['userAbo'];
UserAboOrder::create([
'user_abo_id' => $abo->id,
'shopping_order_id' => $data['referenceOrder']->id,
'status' => 1,
'paid' => true,
]);
$this->artisan('abo:retry-failed-paypal', ['--abo-id' => $abo->id])
->assertSuccessful()
->expectsOutputToContain('Bereits heute bezahlt');
});
it('zeigt korrekte Zusammenfassung im Dry-Run', function () {
createPaypalHoldAbo();
createPaypalHoldAbo();
createPaypalHoldAbo();
$this->artisan('abo:retry-failed-paypal', ['--dry-run' => true])
->assertSuccessful()
->expectsOutputToContain('Betroffene Abos: 3');
});
it('filtert korrekt nach einzelner Abo-ID', function () {
$first = createPaypalHoldAbo();
createPaypalHoldAbo();
$this->artisan('abo:retry-failed-paypal', ['--dry-run' => true, '--abo-id' => $first['userAbo']->id])
->assertSuccessful()
->expectsOutputToContain('Betroffene Abos: 1');
});

View file

@ -0,0 +1,119 @@
<?php
use App\Models\Country;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserShop;
use App\Services\SyS\PayoneCallbackTestbench;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-06-10 12:00:00'));
});
afterEach(function () {
Carbon::setTestNow();
});
it('setzt next_date auf heute und entfernt heutige UserAboOrders (Vorbereitung Cron)', function () {
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'x',
'fr' => 'x',
'it' => 'x',
'ru' => 'x',
]);
$shipping = Shipping::create(['name' => 'Std', 'active' => true]);
$shippingCountry = ShippingCountry::create([
'shipping_id' => $shipping->id,
'country_id' => $country->id,
]);
$shopOwner = User::forceCreate([
'email' => 'so-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$userShop = UserShop::create([
'user_id' => $shopOwner->id,
'name' => 'B'.substr(uniqid('', true), 0, 6),
'slug' => 'b-'.uniqid(),
'active' => true,
]);
$consultant = User::forceCreate([
'email' => 'co-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$customer = User::forceCreate([
'email' => 'cu-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $customer->id,
'member_id' => $consultant->id,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'billing_email' => 'c@example.com',
]);
$userAbo = UserAbo::create([
'user_id' => null,
'member_id' => $consultant->id,
'shopping_user_id' => $shoppingUser->id,
'is_for' => 'ot',
'email' => 'abo@example.com',
'payone_userid' => 999002,
'clearingtype' => 'cc',
'active' => true,
'status' => 2,
'abo_interval' => 1,
'next_date' => '2026-12-01',
]);
$order = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $customer->id,
'country_id' => $shippingCountry->id,
'user_shop_id' => $userShop->id,
'payment_for' => 6,
'points' => 10,
'paid' => true,
'mode' => 'test',
'total' => 50,
'subtotal' => 50,
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $order->id,
'status' => 1,
'paid' => true,
]);
expect(UserAboOrder::where('user_abo_id', $userAbo->id)->count())->toBe(1);
PayoneCallbackTestbench::prepareUserAboForCronRun($userAbo->fresh());
$userAbo->refresh();
expect(Carbon::parse($userAbo->next_date)->format('Y-m-d'))->toBe('2026-06-10')
->and(UserAboOrder::where('user_abo_id', $userAbo->id)->count())->toBe(0);
});