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>
This commit is contained in:
parent
657d4bd5b7
commit
ac3bd6e8ea
136 changed files with 7610 additions and 2688 deletions
371
tests/Feature/GdprDeletionTest.php
Normal file
371
tests/Feature/GdprDeletionTest.php
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
<?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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue