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>
371 lines
12 KiB
PHP
371 lines
12 KiB
PHP
<?php
|
|
|
|
use App\Exceptions\GdprDeletionException;
|
|
use App\Models\Country;
|
|
use App\Models\File;
|
|
use App\Models\ShoppingOrder;
|
|
use App\Models\ShoppingUser;
|
|
use App\Models\UserAbo;
|
|
use App\Models\UserAccount;
|
|
use App\Models\UserBusiness;
|
|
use App\Models\UserShop;
|
|
use App\Models\UserUpdateEmail;
|
|
use App\Services\GdprDeletionService;
|
|
use App\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
function gdprCountry(): Country
|
|
{
|
|
return Country::create([
|
|
'code' => 'DE',
|
|
'phone' => '49',
|
|
'en' => 'Germany',
|
|
'de' => 'Deutschland',
|
|
'es' => 'Alemania',
|
|
'fr' => 'Allemagne',
|
|
'it' => 'Germania',
|
|
'ru' => 'Германия',
|
|
]);
|
|
}
|
|
|
|
function gdprConsultant(array $userAttributes = []): User
|
|
{
|
|
$account = UserAccount::create([
|
|
'm_first_name' => 'Max',
|
|
'm_last_name' => 'Mustermann',
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'address' => 'Musterstraße 1',
|
|
'zipcode' => '12345',
|
|
'city' => 'Musterstadt',
|
|
'phone' => '123456',
|
|
'mobil' => '987654',
|
|
'tax_number' => '12/345/67890',
|
|
'tax_identification_number' => 'DE123456789',
|
|
'birthday' => '1990-05-01',
|
|
'bank_owner' => 'Max Mustermann',
|
|
'bank_iban' => 'DE02120300000000202051',
|
|
'bank_bic' => 'BYLADEM1001',
|
|
'website' => 'https://example.com',
|
|
'facebook' => 'max.mustermann',
|
|
]);
|
|
|
|
$user = User::forceCreate(array_merge([
|
|
'email' => 'gdpr-user-'.uniqid('', true).'@example.com',
|
|
'password' => Hash::make('secret'),
|
|
'lang' => 'de',
|
|
'account_id' => $account->id,
|
|
'notes' => 'Interne Notiz',
|
|
], $userAttributes));
|
|
|
|
return $user->fresh();
|
|
}
|
|
|
|
it('anonymisiert alle Stammdaten eines Beraters unwiderruflich', function () {
|
|
Mail::fake();
|
|
Storage::fake('user');
|
|
Storage::fake('public');
|
|
|
|
$user = gdprConsultant();
|
|
$originalEmail = $user->email;
|
|
|
|
Storage::disk('user')->put('uploads/'.$user->id.'/ausweis.pdf', 'pdf-content');
|
|
$file = File::create([
|
|
'user_id' => $user->id,
|
|
'identifier' => 'id_card',
|
|
'filename' => 'ausweis.pdf',
|
|
'dir' => 'uploads/'.$user->id.'/',
|
|
'original_name' => 'ausweis.pdf',
|
|
'ext' => 'pdf',
|
|
'mine' => 'application/pdf',
|
|
'size' => 11,
|
|
]);
|
|
|
|
DB::table('user_update_emails')->insert([
|
|
'user_id' => $user->id,
|
|
'email' => 'neue-mail@example.com',
|
|
'token' => 'token123',
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
$abo = UserAbo::create([
|
|
'user_id' => $user->id,
|
|
'shopping_user_id' => 0,
|
|
'is_for' => 'me',
|
|
'email' => $originalEmail,
|
|
'payone_userid' => 123456,
|
|
'stripe_customer_id' => 'cus_123',
|
|
'stripe_payment_method_id' => 'pm_123',
|
|
'clearingtype' => 'cc',
|
|
'carddata' => ['brand' => 'visa', 'last4' => '4242'],
|
|
'active' => false,
|
|
'status' => 4,
|
|
'abo_interval' => 1,
|
|
]);
|
|
|
|
$business = UserBusiness::create([
|
|
'user_id' => $user->id,
|
|
'month' => 6,
|
|
'year' => 2026,
|
|
'email' => $originalEmail,
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'user_birthday' => '01.05.1990',
|
|
'user_phone' => '49 987654',
|
|
'version' => 1,
|
|
]);
|
|
|
|
app(GdprDeletionService::class)->deleteUser($user, null);
|
|
|
|
$user = User::withTrashed()->find($user->id);
|
|
expect($user->deleted_at)->not->toBeNull();
|
|
expect($user->email)->not->toContain($originalEmail);
|
|
expect($user->email)->toStartWith('delete-');
|
|
expect($user->notes)->toBeNull();
|
|
expect($user->settings)->toBeNull();
|
|
expect($user->payment_methods)->toBeNull();
|
|
|
|
$account = UserAccount::withTrashed()->find($user->account_id);
|
|
expect($account->deleted_at)->not->toBeNull();
|
|
expect($account->first_name)->toBe(GdprDeletionService::ANONYMIZED_FIRSTNAME);
|
|
expect($account->last_name)->toBe(GdprDeletionService::ANONYMIZED_LASTNAME);
|
|
expect($account->address)->toBeNull();
|
|
expect($account->zipcode)->toBeNull();
|
|
expect($account->city)->toBeNull();
|
|
expect($account->phone)->toBeNull();
|
|
expect($account->mobil)->toBeNull();
|
|
expect($account->tax_number)->toBeNull();
|
|
expect($account->tax_identification_number)->toBeNull();
|
|
expect($account->birthday)->toBe('');
|
|
expect($account->bank_owner)->toBeNull();
|
|
expect($account->bank_iban)->toBeNull();
|
|
expect($account->bank_bic)->toBeNull();
|
|
expect($account->website)->toBeNull();
|
|
expect($account->facebook)->toBeNull();
|
|
|
|
expect(File::find($file->id))->toBeNull();
|
|
Storage::disk('user')->assertMissing('uploads/'.$user->id.'/ausweis.pdf');
|
|
|
|
expect(UserUpdateEmail::where('user_id', $user->id)->count())->toBe(0);
|
|
|
|
$abo = UserAbo::withTrashed()->find($abo->id);
|
|
expect($abo->email)->toBeNull();
|
|
expect($abo->carddata)->toBeNull();
|
|
expect((int) $abo->payone_userid)->toBe(0);
|
|
expect($abo->stripe_customer_id)->toBeNull();
|
|
expect($abo->stripe_payment_method_id)->toBeNull();
|
|
|
|
$business = UserBusiness::find($business->id);
|
|
expect($business->email)->toBeNull();
|
|
expect($business->first_name)->toBe(GdprDeletionService::ANONYMIZED_FIRSTNAME);
|
|
expect($business->user_birthday)->toBeNull();
|
|
expect($business->user_phone)->toBeNull();
|
|
});
|
|
|
|
it('hängt Downline und Kunden auf den nächsten aktiven Sponsor um', function () {
|
|
Mail::fake();
|
|
Storage::fake('user');
|
|
Storage::fake('public');
|
|
|
|
$sponsor = gdprConsultant(['payment_account' => now()->addYear()]);
|
|
$user = gdprConsultant(['m_sponsor' => $sponsor->id]);
|
|
$child = gdprConsultant(['m_sponsor' => $user->id]);
|
|
|
|
$customer = ShoppingUser::create([
|
|
'auth_user_id' => 0,
|
|
'member_id' => $user->id,
|
|
'billing_email' => 'kunde-'.uniqid('', true).'@example.com',
|
|
'billing_country_id' => gdprCountry()->id,
|
|
'shipping_country_id' => 1,
|
|
]);
|
|
|
|
app(GdprDeletionService::class)->deleteUser($user, null);
|
|
|
|
expect($child->fresh()->m_sponsor)->toBe($sponsor->id);
|
|
expect($customer->fresh()->member_id)->toBe($sponsor->id);
|
|
});
|
|
|
|
it('blockiert die Löschung bei aktivem Abo', function () {
|
|
Mail::fake();
|
|
|
|
$user = gdprConsultant();
|
|
UserAbo::create([
|
|
'user_id' => $user->id,
|
|
'shopping_user_id' => 0,
|
|
'is_for' => 'me',
|
|
'email' => $user->email,
|
|
'payone_userid' => 111,
|
|
'clearingtype' => 'cc',
|
|
'active' => true,
|
|
'status' => 2,
|
|
'abo_interval' => 1,
|
|
]);
|
|
|
|
app(GdprDeletionService::class)->deleteUser($user, null);
|
|
})->throws(GdprDeletionException::class);
|
|
|
|
it('blockiert die Löschung von Admin-Accounts', function () {
|
|
Mail::fake();
|
|
|
|
$user = gdprConsultant(['admin' => 2]);
|
|
|
|
app(GdprDeletionService::class)->deleteUser($user, null);
|
|
})->throws(GdprDeletionException::class);
|
|
|
|
it('anonymisiert einen Endkunden ohne Bestellungen vollständig, inkl. aller Einträge derselben Kundennummer', function () {
|
|
Mail::fake();
|
|
|
|
$country = gdprCountry();
|
|
$rowOne = ShoppingUser::create([
|
|
'auth_user_id' => 0,
|
|
'number' => 4711,
|
|
'billing_salutation' => 'Frau',
|
|
'billing_firstname' => 'Erika',
|
|
'billing_lastname' => 'Musterfrau',
|
|
'billing_address' => 'Beispielweg 2',
|
|
'billing_zipcode' => '54321',
|
|
'billing_city' => 'Beispielstadt',
|
|
'billing_phone' => '0123',
|
|
'billing_email' => 'erika@example.com',
|
|
'billing_country_id' => $country->id,
|
|
'shipping_country_id' => $country->id,
|
|
'remarks' => 'Bitte klingeln',
|
|
'subscribed' => true,
|
|
]);
|
|
$rowTwo = ShoppingUser::create([
|
|
'auth_user_id' => 0,
|
|
'number' => 4711,
|
|
'billing_firstname' => 'Erika',
|
|
'billing_lastname' => 'Musterfrau',
|
|
'billing_email' => 'erika@example.com',
|
|
'billing_country_id' => $country->id,
|
|
'shipping_country_id' => $country->id,
|
|
]);
|
|
|
|
app(GdprDeletionService::class)->deleteShoppingUser($rowOne, null);
|
|
|
|
foreach ([$rowOne, $rowTwo] as $row) {
|
|
$row = $row->fresh();
|
|
expect($row->billing_firstname)->toBe(GdprDeletionService::ANONYMIZED_FIRSTNAME);
|
|
expect($row->billing_lastname)->toBe(GdprDeletionService::ANONYMIZED_LASTNAME);
|
|
expect($row->billing_address)->toBeNull();
|
|
expect($row->billing_zipcode)->toBeNull();
|
|
expect($row->billing_city)->toBeNull();
|
|
expect($row->billing_phone)->toBeNull();
|
|
expect($row->billing_email)->not->toContain('erika@example.com');
|
|
expect($row->remarks)->toBe('');
|
|
expect($row->subscribed)->toBeFalse();
|
|
expect($row->faker_mail)->toBeTrue();
|
|
}
|
|
});
|
|
|
|
it('behält bei Endkunden mit Bestellung Name und Rechnungsadresse, entfernt aber alle Kontaktdaten', function () {
|
|
Mail::fake();
|
|
|
|
$country = gdprCountry();
|
|
|
|
$shopOwner = User::forceCreate([
|
|
'email' => 'shop-owner-'.uniqid('', true).'@example.com',
|
|
'password' => Hash::make('secret'),
|
|
'lang' => 'de',
|
|
]);
|
|
$userShop = UserShop::create([
|
|
'user_id' => $shopOwner->id,
|
|
'name' => 'TS'.substr(uniqid('', true), 0, 8),
|
|
'slug' => 'ts-'.uniqid(),
|
|
'active' => true,
|
|
]);
|
|
|
|
$customer = ShoppingUser::create([
|
|
'auth_user_id' => 0,
|
|
'number' => 4712,
|
|
'billing_firstname' => 'Erika',
|
|
'billing_lastname' => 'Musterfrau',
|
|
'billing_address' => 'Beispielweg 2',
|
|
'billing_zipcode' => '54321',
|
|
'billing_city' => 'Beispielstadt',
|
|
'billing_phone' => '0123',
|
|
'billing_email' => 'erika@example.com',
|
|
'billing_country_id' => $country->id,
|
|
'shipping_country_id' => $country->id,
|
|
'remarks' => 'Bitte klingeln',
|
|
'subscribed' => true,
|
|
]);
|
|
|
|
ShoppingOrder::create([
|
|
'shopping_user_id' => $customer->id,
|
|
'auth_user_id' => 0,
|
|
'country_id' => $country->id,
|
|
'user_shop_id' => $userShop->id,
|
|
'payment_for' => 6,
|
|
'total' => 100,
|
|
'subtotal' => 90,
|
|
]);
|
|
|
|
app(GdprDeletionService::class)->deleteShoppingUser($customer, null);
|
|
|
|
$customer = $customer->fresh();
|
|
expect($customer->billing_firstname)->toBe('Erika');
|
|
expect($customer->billing_lastname)->toBe('Musterfrau');
|
|
expect($customer->billing_address)->toBe('Beispielweg 2');
|
|
expect($customer->billing_city)->toBe('Beispielstadt');
|
|
expect($customer->billing_phone)->toBeNull();
|
|
expect($customer->billing_email)->not->toContain('erika@example.com');
|
|
expect($customer->remarks)->toBe('');
|
|
expect($customer->subscribed)->toBeFalse();
|
|
expect($customer->faker_mail)->toBeTrue();
|
|
});
|
|
|
|
it('lehnt die Endkunden-Löschung für Berater-Datensätze ab', function () {
|
|
Mail::fake();
|
|
|
|
$user = gdprConsultant();
|
|
$customer = ShoppingUser::create([
|
|
'auth_user_id' => $user->id,
|
|
'billing_email' => $user->email,
|
|
'billing_country_id' => gdprCountry()->id,
|
|
'shipping_country_id' => 1,
|
|
]);
|
|
|
|
app(GdprDeletionService::class)->deleteShoppingUser($customer, null);
|
|
})->throws(GdprDeletionException::class);
|
|
|
|
it('entfernt Zahlungs-Tokens der Abos eines Endkunden', function () {
|
|
Mail::fake();
|
|
|
|
$country = gdprCountry();
|
|
$customer = ShoppingUser::create([
|
|
'auth_user_id' => 0,
|
|
'number' => 4713,
|
|
'billing_firstname' => 'Erika',
|
|
'billing_lastname' => 'Musterfrau',
|
|
'billing_email' => 'erika-abo@example.com',
|
|
'billing_country_id' => $country->id,
|
|
'shipping_country_id' => $country->id,
|
|
]);
|
|
|
|
$abo = UserAbo::create([
|
|
'shopping_user_id' => $customer->id,
|
|
'is_for' => 'ot',
|
|
'email' => 'erika-abo@example.com',
|
|
'payone_userid' => 999,
|
|
'clearingtype' => 'cc',
|
|
'carddata' => ['brand' => 'visa', 'last4' => '4242'],
|
|
'active' => false,
|
|
'status' => 6,
|
|
'abo_interval' => 1,
|
|
]);
|
|
|
|
app(GdprDeletionService::class)->deleteShoppingUser($customer, null);
|
|
|
|
$abo = UserAbo::withTrashed()->find($abo->id);
|
|
expect($abo->email)->toBeNull();
|
|
expect($abo->carddata)->toBeNull();
|
|
expect((int) $abo->payone_userid)->toBe(0);
|
|
});
|