mivita/tests/Feature/AboUpdateAuthorizationTest.php
Kevin ac3bd6e8ea Statistiken/Abo: Kundenfeedback 2026-07-24 komplett umgesetzt + Steuern: DATEV-Fixes 08+09/2025 (Sammel-Commit Branch-WIP)
Feedback-Umsetzung (dev/2026-07-22/feedback-analyse-2026-07-24.md, alle 10 Punkte):
- VIP-Statistik: eigener Berater ('ich', Linie 0) in Neue-Kundenabos- und beiden Top-10-Listen; Top-10-Umsatz sortiert nach abgerechneten Ist-Punkten + Abo-Anzahl-Spalte; Fußnote nicht-payline-wirksame Punkte (SalesVolumePoints::nonPaylinePoints)
- Führungskräfte-Liste: beide Sichten 'direkt' (an nächster FK abgeschnitten, additiv) und 'gesamt' (komplette Downline), Gesamt-Spalte nach Kundenzählweise
- Team-Abos: je Ebene 'abgerechnet (Monat)' und 'bis zum 25. erwartet'; Payline-Karte mit Teamabo-/Kundenabo-Zählern
- Incentive-Live-Ranking: Qualifizierte zuerst nummeriert (fixt auch isWinner rank<=max_winners)
- Porto: Netto-/Brutto-Kennzeichnung (Berater netto, Endkunden inkl. MwSt., portal_view-Flag; auch CRM-/Shop-Warenkorb + Checkout), Yard::shipping_tax-Fix
- Struktur-Ansicht: Abo-Badges 'A' (eigenes Abo) und '(n)' (Kundenabos) je Teampartner, gebatcht
- Abo-Löschung: Softdelete setzt active=0 (UserAbo deleting-Hook) + Bereinigungs-Migration für gelöschte-aber-aktive Abos
- Doku: technische Analyse + Kundinnen-Feedback-Dokument unter dev/2026-07-22/
- Tests: IncentiveRankingTest, TeamStructureAboBadgesTest, YardShippingTaxTest (neu); BackofficeStatisticsListsTest, TeamAboOverviewTest, PaylineForecastServiceTest, AboAdminDeleteTest, AboOneTimeViewTest erweitert

Außerdem enthalten (Branch-WIP vor dem Feedback): DATEV-Abgleich-Fixes 08+09/2025, USt-ID-Live-Validierung sowie restliche unkommittete Stände (u. a. Abo-Pause-Sprachkeys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:56:02 +00:00

223 lines
6.4 KiB
PHP

<?php
use App\Http\Controllers\Portal\AboController as PortalAboController;
use App\Http\Controllers\User\AboController;
use App\Models\Customer;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Repositories\AboRepository;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-07-22'));
URL::resolveMissingNamedRoutesUsing(
fn (string $name, array $parameters) => '/'.$name.'/'.implode('/', $parameters)
);
});
afterEach(function () {
Carbon::setTestNow();
});
function auaMakeUser(): User
{
return User::forceCreate([
'email' => 'aua-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'm_level' => 1,
'payment_account' => now()->addYear(),
]);
}
function auaMakeAbo(User $user, int $paidDeliveries = 6, string $nextDate = '2026-08-05', ?string $lastPauseDate = null): UserAbo
{
$abo = UserAbo::create([
'user_id' => $user->id,
'member_id' => $user->id,
'shopping_user_id' => 0,
'email' => 'aua-abo-'.uniqid('', true).'@example.com',
'is_for' => 'me',
'payone_userid' => 0,
'clearingtype' => 'cc',
'amount' => 5000,
'active' => true,
'status' => 2,
'abo_interval' => 5,
'start_date' => '2026-01-05',
'last_date' => '2026-07-05',
'next_date' => $nextDate,
'last_pause_date' => $lastPauseDate,
]);
for ($i = 0; $i < $paidDeliveries; $i++) {
UserAboOrder::create([
'user_abo_id' => $abo->id,
'shopping_order_id' => 0,
'status' => 2,
'paid' => true,
]);
}
return $abo->fresh();
}
/**
* Simuliert den POST des Abo-Einstellungs-Modals an AboController@update.
*
* @param array<string, mixed> $payload
*/
function auaCallUpdate(string $routeView, int $routeId, array $payload): mixed
{
app('request')->merge($payload);
return (new AboController(app(AboRepository::class)))->update($routeView, $routeId);
}
it('blockiert das Kündigen eines fremden Abos über eine manipulierte Body-id', function (): void {
$attacker = auaMakeUser();
$victim = auaMakeUser();
$attackerAbo = auaMakeAbo($attacker);
$victimAbo = auaMakeAbo($victim);
$this->actingAs($attacker);
expect(fn () => auaCallUpdate('me', $attackerAbo->id, [
'action' => 'abo_update_settings',
'id' => $victimAbo->id,
'view' => 'admin',
'abo_interval' => 5,
'abo_next_month' => now()->addMonth()->format('Y-m'),
'abo_cancel' => 'true',
]))->toThrow(HttpException::class);
expect((int) $victimAbo->fresh()->status)->toBe(2)
->and($victimAbo->fresh()->active)->toBeTruthy();
});
it('ignoriert ein gespooftes view=admin im Body: Mindestlaufzeit gilt weiterhin', function (): void {
$user = auaMakeUser();
$abo = auaMakeAbo($user, paidDeliveries: 2);
$this->actingAs($user);
auaCallUpdate('me', $abo->id, [
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => 'admin',
'abo_interval' => 5,
'abo_next_month' => now()->addMonth()->format('Y-m'),
'abo_cancel' => 'true',
]);
expect((int) $abo->fresh()->status)->toBe(2)
->and($abo->fresh()->active)->toBeTruthy();
});
it('ignoriert ein gespooftes view=admin im Body: Pause-Intervall gilt weiterhin', function (): void {
$user = auaMakeUser();
$abo = auaMakeAbo($user, lastPauseDate: '2026-06-01');
$this->actingAs($user);
auaCallUpdate('me', $abo->id, [
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => 'admin',
'abo_interval' => 5,
'abo_next_month' => now()->addMonth()->format('Y-m'),
'abo_pause' => 'true',
]);
expect($abo->fresh()->getRawOriginal('next_date'))->toStartWith('2026-08-05');
});
it('kündigt das eigene Abo nach erfüllter Mindestlaufzeit weiterhin regulär', function (): void {
$user = auaMakeUser();
$abo = auaMakeAbo($user);
$this->actingAs($user);
auaCallUpdate('me', $abo->id, [
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => 'me',
'abo_interval' => 5,
'abo_cancel' => 'true',
]);
expect((int) $abo->fresh()->status)->toBe(4)
->and($abo->fresh()->active)->toBeFalsy();
});
it('blockiert die Kündigung serverseitig vor erreichter Mindestlaufzeit', function (): void {
$user = auaMakeUser();
$abo = auaMakeAbo($user, paidDeliveries: 2);
$this->actingAs($user);
$repository = new AboRepository;
$repository->setModel($abo);
$repository->update([
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => 'me',
'abo_interval' => 5,
'abo_cancel' => 'true',
]);
expect((int) $abo->fresh()->status)->toBe(2)
->and($abo->fresh()->active)->toBeTruthy();
});
it('zeigt im Portal nach der Kündigung wieder die Abo-Neuanlage an', function (): void {
$shoppingUser = ShoppingUser::create([
'auth_user_id' => 1,
'billing_firstname' => 'Karla',
'billing_lastname' => 'Kundin',
'billing_address' => 'Kundenweg 2',
'billing_zipcode' => '11111',
'billing_city' => 'Kundenstadt',
'billing_country_id' => 1,
'shipping_country_id' => 1,
'billing_email' => 'aua-kundin@example.com',
'same_as_billing' => true,
'mode' => 'test',
'wp_order_number' => 0,
]);
UserAbo::create([
'user_id' => 1,
'member_id' => 1,
'shopping_user_id' => $shoppingUser->id,
'email' => 'aua-kundin@example.com',
'is_for' => 'ot',
'payone_userid' => 0,
'clearingtype' => 'cc',
'amount' => 5000,
'active' => false,
'status' => 4,
'abo_interval' => 5,
'cancel_date' => now(),
]);
$customer = Customer::create([
'name' => 'Karla Kundin',
'email' => 'aua-kundin@example.com',
'shopping_user_id' => $shoppingUser->id,
]);
$this->actingAs($customer, 'customers');
$response = (new PortalAboController)->myAbo();
expect($response->name())->toBe('portal.abo.my_abo_create');
});