mivita/tests/Feature/Incentive/IncentiveVipOnlyTest.php
Kevin c4edbf97e4 Recruiting Challenge: Storno nimmt Neupartner raus, Rang und Auszahlung nur mit Bestaetigung
Kundenentscheid 25.09.2026:
- Storno: Ist jede bezahlte Starterpaket-Bestellung eines Neupartners
  storniert (txaction = cancelled), bekommt das Tracking cancelled_at,
  eine Storno-Logzeile nimmt die Einmalpunkte zurueck. Nachtlauf und
  Neuaufbau markieren; "Meine Berechnung" zeigt den Partner durchgestrichen
  mit "Storniert am ..., zaehlt nicht".
- Gezaehlt wird fuer alle, einen Rang bekommen nur bestaetigte Teilnehmer;
  Export und Summen nur fuer sie. Unbestaetigte sehen "Deine Praemie nach
  Bestaetigung", im Admin "Teilnahme nicht bestaetigt, keine Auszahlung".
- Datenschutz: andere Berater sehen Unbestaetigte in der Rangliste nicht
  mehr (auch nicht anonym), VIP-Konten schon, der eigene Eintrag bleibt.
- Teilnahmebedingungen Punkt 2 und 7 und Hinweistexte in de/en/es/fr.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 10:44:49 +00:00

320 lines
13 KiB
PHP

<?php
use App\Models\Incentive;
use App\Models\IncentiveParticipant;
use App\Models\UserAccount;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
/**
* E1: "Nur fuer VIP sichtbar" blendet ein aktives Incentive fuer normale Berater aus
* (Navigation, Dashboard, Teaser, Rangliste, Details), VIP-Konten sehen es zur Abstimmung.
* Dazu der Umbau der Ranglisten-Seite: Teilnahme-Block oben, solange nicht bestaetigt.
* Berater-Routen sind host-abhaengig (CRM-Host).
*/
beforeEach(function () {
$this->previousHttpHost = $_SERVER['HTTP_HOST'] ?? null;
$_SERVER['HTTP_HOST'] = config('domains.domains.crm.host');
$this->refreshApplication();
$this->artisan('migrate');
});
afterEach(function () {
if ($this->previousHttpHost === null) {
unset($_SERVER['HTTP_HOST']);
} else {
$_SERVER['HTTP_HOST'] = $this->previousHttpHost;
}
});
function vipTestUser(int $admin = 0): User
{
$account = UserAccount::create(['first_name' => 'Vera', 'last_name' => 'Berater']);
return User::forceCreate([
'email' => 'vip-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'admin' => $admin,
'account_id' => $account->id,
'payment_account' => now()->addYear(),
'wizard' => 100,
'active' => 1,
'active_date' => now()->subMonths(6),
]);
}
function vipOnlyChallenge(array $overrides = []): Incentive
{
return Incentive::factory()->create(array_merge([
'name' => 'Recruiting Challenge',
'slug' => 'recruiting-challenge-vip-'.uniqid(),
'qualification_start' => '2026-10-01',
'qualification_end' => '2027-01-31',
'calculation_end' => '2027-01-31',
'points_partner_onetime' => 1,
'points_abo_onetime' => 0,
'min_direct_partners' => 0,
'min_customer_abos' => 0,
'max_winners' => 3,
'amount_per_partner' => 50,
'bonus_rank_1' => 500,
'include_sales_points' => false,
'vip_only' => true,
], $overrides));
}
it('zeigt ein VIP-Incentive in der Navigation nur VIP-Konten', function () {
$public = Incentive::factory()->create(['qualification_start' => '2026-04-01']);
$vipOnly = vipOnlyChallenge();
expect(Incentive::activeForNavigation(vipTestUser())->pluck('id')->all())->toBe([$public->id])
->and(Incentive::activeForNavigation(vipTestUser(admin: 1))->pluck('id')->all())->toBe([$public->id, $vipOnly->id])
->and(Incentive::activeForNavigation(null)->pluck('id')->all())->toBe([$public->id])
->and($vipOnly->isVisibleFor(vipTestUser()))->toBeFalse()
->and($vipOnly->isVisibleFor(vipTestUser(admin: 1)))->toBeTrue()
->and($public->isVisibleFor(vipTestUser()))->toBeTrue();
});
it('liefert normalen Beratern 404 fuer Teaser, Rangliste, Details und Teilnahme', function () {
$incentive = vipOnlyChallenge();
$this->actingAs(vipTestUser());
$this->get(route('user_incentive_teaser', [$incentive->slug]))->assertNotFound();
$this->get(route('user_incentive_show', [$incentive->slug]))->assertRedirect(route('user_incentive_teaser', [$incentive->slug]).'#ranking');
$this->get(route('user_incentive_details', [$incentive->slug]))->assertNotFound();
$this->post(route('user_incentive_participate', [$incentive->slug]), ['accept_terms' => 1])->assertNotFound();
});
it('zeigt VIP-Konten Teaser und Rangliste eines VIP-Incentives', function () {
$incentive = vipOnlyChallenge();
$this->actingAs(vipTestUser(admin: 1));
$this->get(route('user_incentive_teaser', [$incentive->slug]))->assertOk();
});
it('zeigt in der Navigation ein VIP-Badge am VIP-Incentive', function () {
$incentive = vipOnlyChallenge();
$public = vipOnlyChallenge(['vip_only' => false, 'name' => 'Offenes Incentive']);
$this->actingAs(vipTestUser(admin: 1));
$html = $this->get(route('user_incentive_teaser', [$public->slug]))->assertOk()->getContent();
$vipEntry = substr($html, strpos($html, route('user_incentive_teaser', [$incentive->slug])), 600);
$publicEntry = substr($html, strpos($html, route('user_incentive_teaser', [$public->slug]).'"'), 600);
expect($vipEntry)->toContain('badge badge-secondary')
->and($publicEntry)->not->toContain('badge badge-secondary');
});
it('zeigt die Dashboard-Karte eines VIP-Incentives normalen Beratern nicht', function () {
vipOnlyChallenge(['name' => 'VIP Dashboard Challenge']);
$this->actingAs(vipTestUser());
$this->get(route('home'))->assertOk()->assertDontSee('VIP Dashboard Challenge');
});
it('zeigt die Dashboard-Karte eines VIP-Incentives VIP-Konten mit nur einem Button zur Seite', function () {
vipOnlyChallenge(['name' => 'VIP Dashboard Challenge']);
$this->actingAs(vipTestUser(admin: 1));
$this->get(route('home'))->assertOk()
->assertSee('VIP Dashboard Challenge')
->assertSee(__('incentive.dashboard_btn_teaser'))
->assertDontSee(__('incentive.dashboard_btn_ranking'))
->assertDontSee('#ranking');
});
it('zeigt allen Beratern ein Incentive ohne VIP-Sperre', function () {
$incentive = vipOnlyChallenge(['vip_only' => false]);
$this->actingAs(vipTestUser());
$this->get(route('user_incentive_teaser', [$incentive->slug]))->assertOk();
});
it('speichert den VIP-Haken im Admin und zeigt ihn auf der Detailseite', function () {
$this->actingAs(vipTestUser(admin: 2));
$this->post(route('admin_incentive_store'), [
'name' => 'VIP Test Challenge',
'qualification_start' => '01.10.2026',
'qualification_end' => '31.01.2027',
'calculation_end' => '31.01.2027',
'points_partner_onetime' => 1,
'points_abo_onetime' => 0,
'min_direct_partners' => 0,
'min_customer_abos' => 0,
'max_winners' => 3,
'include_sales_points' => '0',
'vip_only' => '1',
'status' => 1,
])->assertRedirect(route('admin_incentives'));
$incentive = Incentive::where('name', 'VIP Test Challenge')->firstOrFail();
expect($incentive->vip_only)->toBeTrue();
$this->get(route('admin_incentive_show', [$incentive->id]))
->assertOk()
->assertSee('id="incentive-vip-only-badge"', false);
});
describe('Incentive-Seite: Teilnahme-Block oben, Rangliste unten', function () {
it('zeigt ohne Bestaetigung den Teilnahme-Block mit Praemie zuerst, dann Trennlinie und Inhalt', function () {
$incentive = vipOnlyChallenge(['vip_only' => false, 'image' => 'challenge.jpg', 'terms' => '<p>Bedingungen Text</p>']);
$this->actingAs(vipTestUser());
$response = $this->get(route('user_incentive_teaser', [$incentive->slug]))->assertOk();
$html = $response->getContent();
$join = strpos($html, 'id="incentive-join"');
$divider = strpos($html, 'class="inc-divider"');
$hero = strpos($html, 'class="incentive-hero mb-5"');
$howItWorks = strpos($html, 'id="incentive-how-it-works"');
$ranking = strpos($html, 'id="ranking"');
expect($join)->not->toBeFalse()
->and($divider)->toBeGreaterThan($join)
->and($hero)->toBeGreaterThan($divider)
->and($howItWorks)->toBeGreaterThan($hero)
->and($ranking)->toBeGreaterThan($howItWorks)
->and(substr_count($html, 'id="terms"'))->toBe(1);
$response->assertSee(__('incentive.join_title'))
->assertSee('50,00 €')
->assertSee(__('incentive.join_highlight_rank', ['amount' => '500,00 €', 'n' => 1]))
->assertSee(__('incentive.join_terms_toggle'))
->assertSee('Bedingungen Text', false)
->assertSee('id="incentive-payout-tile"', false)
->assertSee('id="incentive-how-it-works"', false)
->assertDontSee(__('incentive.section_points'))
->assertDontSee(__('incentive.points_short'));
});
it('nennt den Block "Teilnahme bestaetigen", wenn Punkte schon laufen', function () {
$incentive = vipOnlyChallenge(['vip_only' => false]);
$user = vipTestUser();
IncentiveParticipant::factory()->unconfirmed()->create([
'incentive_id' => $incentive->id, 'user_id' => $user->id,
'qualified_partners' => 2, 'total_points' => 2, 'is_qualified' => true, 'rank' => 1,
]);
$this->actingAs($user);
$this->get(route('user_incentive_teaser', [$incentive->slug]))
->assertOk()
->assertSee(__('incentive.join_confirm_title'))
->assertSee(__('incentive.payout_after_confirmation'))
->assertSee('600,00 €');
});
it('zeigt bestaetigten Teilnehmern den Kopf mit Stand statt des Teilnahme-Blocks', function () {
$incentive = vipOnlyChallenge(['vip_only' => false]);
$user = vipTestUser();
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id, 'user_id' => $user->id,
'qualified_partners' => 1, 'total_points' => 1, 'is_qualified' => true, 'rank' => 1,
]);
$this->actingAs($user);
$this->get(route('user_incentive_teaser', [$incentive->slug]))
->assertOk()
->assertDontSee('id="incentive-join"', false)
->assertSee('id="incentive-participant-header"', false)
->assertSee(__('incentive.my_details'))
->assertSee(__('incentive.join_terms_toggle'))
->assertSee('id="ranking"', false)
->assertSee('id="incentive-how-it-works"', false)
->assertDontSee(__('incentive.section_points'));
});
});
describe('Teaser-Seite der Challenge', function () {
it('zeigt den Call to Action ganz oben, die Praemie je Teampartner und keine Galerie', function () {
$incentive = vipOnlyChallenge(['vip_only' => false, 'image' => 'challenge.jpg', 'bonus_rank_2' => 300, 'bonus_rank_3' => 200]);
$this->actingAs(vipTestUser());
$response = $this->get(route('user_incentive_teaser', [$incentive->slug]))->assertOk();
$html = $response->getContent();
$join = strpos($html, 'id="incentive-join"');
$hero = strpos($html, 'class="incentive-hero mb-5"');
expect($join)->not->toBeFalse()
->and($hero)->toBeGreaterThan($join);
$response->assertSee(__('incentive.join_title'))
->assertSee(__('incentive.payout_per_partner_label'))
->assertSee('id="incentive-bonus-tile"', false)
->assertSee('id="incentive-how-it-works"', false)
->assertSee(__('incentive.section_how_it_works'))
->assertDontSee(__('incentive.section_points'))
->assertDontSee('class="gallery-section', false)
->assertDontSee(__('incentive.gallery_title'));
});
it('nimmt Galerie-Bilder nur aus dem eigenen Ordner des Incentives', function () {
$incentive = vipOnlyChallenge(['vip_only' => false, 'image' => 'hero.jpg']);
$other = vipOnlyChallenge(['vip_only' => false, 'image' => 'alt.jpg']);
$dir = public_path($incentive->imageDirectory());
@mkdir($dir, 0777, true);
file_put_contents($dir.'/hero.jpg', 'x');
file_put_contents($dir.'/b.jpg', 'x');
file_put_contents($dir.'/a.jpg', 'x');
try {
expect($incentive->galleryImages())->toBe([$incentive->imageDirectory().'/a.jpg', $incentive->imageDirectory().'/b.jpg'])
->and($incentive->heroImagePath())->toBe($incentive->imageDirectory().'/hero.jpg')
->and($other->galleryImages())->toBe([])
->and($other->heroImagePath())->toBe('img/incentive/'.$other->image);
} finally {
array_map('unlink', glob($dir.'/*') ?: []);
@rmdir($dir);
}
});
});
/**
* Kundenentscheid 25.09.2026 (Datenschutz): Unbestaetigte zaehlen im Hintergrund mit,
* andere Berater sehen sie aber nicht, auch nicht anonym.
*/
function rankingPrivacyChallenge(): Incentive
{
$incentive = vipOnlyChallenge(['vip_only' => false]);
$confirmedUser = vipTestUser();
$confirmedUser->account->update(['first_name' => 'Anna', 'last_name' => 'Bestaetigt']);
$unconfirmedUser = vipTestUser();
$unconfirmedUser->account->update(['first_name' => 'Nina', 'last_name' => 'Offen']);
IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id, 'user_id' => $confirmedUser->id,
'qualified_partners' => 1, 'total_points' => 1, 'rank' => 1,
]);
IncentiveParticipant::factory()->unconfirmed()->create([
'incentive_id' => $incentive->id, 'user_id' => $unconfirmedUser->id,
'qualified_partners' => 3, 'total_points' => 3, 'rank' => null,
]);
return $incentive;
}
it('blendet unbestaetigte Teilnehmer in der Rangliste fuer normale Berater aus', function () {
$incentive = rankingPrivacyChallenge();
$this->actingAs(vipTestUser());
$this->get(route('user_incentive_teaser', [$incentive->slug]))
->assertOk()
->assertSee('Anna Bestaetigt')
->assertDontSee('Nina Offen')
->assertDontSee(__('incentive.anonymous_consultant'));
});
it('zeigt VIP-Konten auch die unbestaetigten Teilnehmer', function () {
$incentive = rankingPrivacyChallenge();
$this->actingAs(vipTestUser(admin: 1));
$this->get(route('user_incentive_teaser', [$incentive->slug]))
->assertOk()
->assertSee('Anna Bestaetigt')
->assertSee('Nina Offen');
});