mivita/tests/Feature/GdprDeletionTest.php
Kevin 60f9985313 Kunden loeschen: Modal mit Komplett- oder DSGVO-Loeschung ueber alle Dubletten
Ticket dev/_tickets/2026-09-08.md #2:
- "Kunde loeschen" oeffnet ein Modal statt direkt per GET zu loeschen. Es
  zeigt alle Eintraege desselben Kunden (Kundennummer oder E-Mail, ohne
  Platzhalter-Adressen) mit Anzahl Bestellungen.
- Ohne Bestellungen: Komplett-Loeschung (anonymisiert und soft-geloescht,
  Portal-Logins und Stripe-Kunden entfernt, Zahlungs-Tokens inaktiver Abos
  geleert). Mit Bestellungen nur DSGVO-Loeschung (Superadmin); die arbeitet
  jetzt ebenfalls auf der E-Mail-Gruppe und entfernt Portal-Logins.
- Bei aktivem Abo oder Berater-Datensatz wird abgelehnt.
- Die alte "Loeschung" (CustomerRepository::deleteCustomer) entfaellt.
- Fehlermeldungen nutzen alert-error; alert-danger zeigte kein Layout an.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:43:35 +00:00

421 lines
14 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\Services\Payment\Stripe\StripeCustomerService;
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);
});
it('löscht die Stripe-Customer-Objekte des Endkunden (SE1.6)', function () {
Mail::fake();
$country = gdprCountry();
$customer = ShoppingUser::create([
'auth_user_id' => 0,
'number' => 4714,
'billing_firstname' => 'Erika',
'billing_lastname' => 'Musterfrau',
'billing_email' => 'erika-stripe@example.com',
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'stripe_customer_id' => 'cus_shop_1',
]);
UserAbo::create([
'shopping_user_id' => $customer->id,
'is_for' => 'ot',
'email' => 'erika-stripe@example.com',
'payone_userid' => 0,
'payment_provider' => 'stripe',
'stripe_customer_id' => 'cus_abo_1',
'stripe_payment_method_id' => 'pm_abo_1',
'clearingtype' => 'stripe',
'active' => false,
'status' => 6,
'abo_interval' => 1,
]);
$deleted = [];
$stripeCustomers = Mockery::mock(StripeCustomerService::class);
$stripeCustomers->shouldReceive('deleteCustomer')
->twice()
->andReturnUsing(function (string $customerId) use (&$deleted): bool {
$deleted[] = $customerId;
return true;
});
app()->instance(StripeCustomerService::class, $stripeCustomers);
$summary = app(GdprDeletionService::class)->deleteShoppingUser($customer, null);
sort($deleted);
expect($deleted)->toBe(['cus_abo_1', 'cus_shop_1'])
->and($summary['stripe_customers_deleted'])->toBe(2)
->and($customer->fresh()->stripe_customer_id)->toBeNull();
});