mivita/tests/Feature/GdprCustomerLockTest.php
Kevin 3d0864713b Kunden loeschen: DSGVO-geloeschte Kunden ausblenden und sperren; Checkout-Herkunft freiwillig
Kunden loeschen (Rueckmeldung "es wird nichts geloescht"):
- DSGVO-geloeschte Eintraege blieben mit Name in den Listen stehen; Kunde
  61009 wurde deshalb achtmal geloescht, bei Eintrag 45013 wurde die E-Mail
  danach ueber ein offenes Bearbeiten-Formular zurueckgeschrieben
- neue Spalte shopping_users.gdpr_deleted_at (Bestand per Migration markiert)
- Admin-Kundenliste blendet geloeschte Kunden aus (Filter: ausblenden /
  mit anzeigen / nur geloeschte), Berater-Kundenliste immer
- Bearbeiten/Speichern im Admin und beim Berater gesperrt; ShoppingUser
  verweigert das Zurueckschreiben von E-Mail und Telefon
- Modal und Erfolgsmeldung (de/en/es/fr) sagen klar, dass Name und
  Rechnungsadresse wegen der Aufbewahrungspflicht bleiben
- gdpr:repair-customers [--execute] zieht protokollierte Loeschungen nach
- DSGVO-Loeschung prueft Superadmin serverseitig; Berater duerfen eine
  Kunden-E-Mail nur beim eigenen Kunden nachtragen
- setFakerMail setzt faker_mail bei geloeschten Eintraegen nicht zurueck

Checkout: "Wie bist du auf uns aufmerksam geworden?" ist kein Pflichtfeld
mehr und wird farbig hervorgehoben.

Deploy: php artisan migrate && php artisan gdpr:repair-customers --execute

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:36:02 +00:00

214 lines
7.3 KiB
PHP

<?php
use App\Exceptions\GdprDeletionException;
use App\Models\Country;
use App\Models\Logger;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserShop;
use App\Services\GdprDeletionService;
use App\Services\ShoppingUserService;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
beforeEach(function () {
Mail::fake();
$this->country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
]);
$this->member = User::forceCreate([
'email' => 'gcl-member-'.uniqid('', true).'@example.com',
'password' => Hash::make('secret'),
'lang' => 'de',
]);
$this->userShop = UserShop::create([
'user_id' => $this->member->id,
'name' => 'GCL'.substr(uniqid('', true), 0, 8),
'slug' => 'gcl-'.uniqid(),
'active' => true,
]);
});
function gclCustomer(array $attributes = []): ShoppingUser
{
return ShoppingUser::create(array_merge([
'auth_user_id' => 0,
'member_id' => test()->member->id,
'number' => 5100,
'billing_firstname' => 'Susann',
'billing_lastname' => 'Voigt',
'billing_address' => 'Hauptstraße 1',
'billing_zipcode' => '19243',
'billing_city' => 'Wittenburg',
'billing_phone' => '0123',
'billing_email' => 'susann@example.com',
'billing_country_id' => test()->country->id,
'shipping_country_id' => test()->country->id,
], $attributes));
}
function gclOrderFor(ShoppingUser $customer): ShoppingOrder
{
return ShoppingOrder::create([
'shopping_user_id' => $customer->id,
'auth_user_id' => 0,
'country_id' => test()->country->id,
'user_shop_id' => test()->userShop->id,
'payment_for' => 6,
'total' => 100,
'subtotal' => 90,
]);
}
it('markiert alle Einträge bei der DSGVO-Löschung und meldet den Status im Modal', function () {
$first = gclCustomer();
$second = gclCustomer();
gclOrderFor($first);
$service = app(GdprDeletionService::class);
$before = $service->analyzeShoppingUser($first);
expect($before['all_gdpr_deleted'])->toBeFalse()
->and($before['gdpr_deleted_rows'])->toBe(0);
$service->deleteShoppingUser($first, null);
expect($first->fresh()->isGdprDeleted())->toBeTrue()
->and($second->fresh()->isGdprDeleted())->toBeTrue()
->and($first->fresh()->billing_lastname)->toBe('Voigt');
$after = $service->analyzeShoppingUser($first->fresh());
expect($after['all_gdpr_deleted'])->toBeTrue()
->and($after['gdpr_deleted_rows'])->toBe(2)
->and($after['gdpr_deleted_at'])->not->toBeNull();
});
it('blendet DSGVO-gelöschte Einträge über den Scope aus', function () {
$deleted = gclCustomer();
gclOrderFor($deleted);
app(GdprDeletionService::class)->deleteShoppingUser($deleted, null);
$other = gclCustomer(['number' => 5200, 'billing_email' => 'andere@example.com']);
$visibleIds = ShoppingUser::query()->withoutGdprDeleted()->pluck('id')->all();
expect($visibleIds)->toContain($other->id)
->not->toContain($deleted->id);
});
it('verhindert, dass Kontaktdaten in einen DSGVO-gelöschten Eintrag zurückgeschrieben werden', function (string $field, string $value) {
$customer = gclCustomer();
gclOrderFor($customer);
app(GdprDeletionService::class)->deleteShoppingUser($customer, null);
$customer = $customer->fresh();
$customer->{$field} = $value;
$customer->save();
})->with([
'E-Mail' => ['billing_email', 'susann@example.com'],
'Telefon' => ['billing_phone', '0123'],
'Liefer-E-Mail' => ['shipping_email', 'susann@example.com'],
'Liefer-Telefon' => ['shipping_phone', '0456'],
])->throws(GdprDeletionException::class);
it('erlaubt an DSGVO-gelöschten Einträgen weiterhin Änderungen ohne Kontaktdaten', function () {
$customer = gclCustomer();
gclOrderFor($customer);
app(GdprDeletionService::class)->deleteShoppingUser($customer, null);
$newMember = User::forceCreate([
'email' => 'gcl-new-member-'.uniqid('', true).'@example.com',
'password' => Hash::make('secret'),
'lang' => 'de',
]);
$customer = $customer->fresh();
$customer->member_id = $newMember->id;
$customer->save();
expect($customer->fresh()->member_id)->toBe($newMember->id);
});
it('lässt eine erneute DSGVO-Löschung eines bereits gelöschten Kunden zu', function () {
$customer = gclCustomer();
gclOrderFor($customer);
$service = app(GdprDeletionService::class);
$service->deleteShoppingUser($customer, null);
$firstDeletedAt = $customer->fresh()->gdpr_deleted_at;
$service->deleteShoppingUser($customer->fresh(), null);
expect($customer->fresh()->gdpr_deleted_at->equalTo($firstDeletedAt))->toBeTrue();
});
it('markiert auch komplett gelöschte Kunden ohne Bestellung', function () {
$customer = gclCustomer();
app(GdprDeletionService::class)->purgeShoppingUser($customer, null);
$row = ShoppingUser::withTrashed()->find($customer->id);
expect($row->trashed())->toBeTrue()
->and($row->isGdprDeleted())->toBeTrue();
});
it('setzt faker_mail bei DSGVO-gelöschten Einträgen nicht zurück', function () {
$customer = gclCustomer();
gclOrderFor($customer);
app(GdprDeletionService::class)->deleteShoppingUser($customer, null);
ShoppingUserService::setFakerMail($this->member);
expect($customer->fresh()->faker_mail)->toBeTrue();
});
it('zieht protokollierte Löschungen nach, deren Kontaktdaten wieder befüllt wurden', function () {
$restored = gclCustomer();
$sibling = gclCustomer();
gclOrderFor($restored);
gclOrderFor($sibling);
$service = app(GdprDeletionService::class);
$service->deleteShoppingUser($sibling, null);
// Zustand wie bei Kunde 45013: Kontaktdaten vor Einführung der Sperre wieder befüllt
ShoppingUser::whereKey($restored->id)->update([
'billing_email' => 'susann@example.com',
'billing_phone' => '0123',
'faker_mail' => false,
'gdpr_deleted_at' => null,
]);
$deletionLoggedAt = Logger::where('action', 'gdpr-delete-customer')->value('created_at');
$this->travelTo($deletionLoggedAt->copy()->addDay());
$lateCustomer = gclCustomer(['billing_email' => 'susann-neu@example.com']);
gclOrderFor($lateCustomer);
$preview = $service->repairLoggedShoppingUserDeletions(true);
expect(array_column($preview, 'id'))->toBe([$restored->id])
->and($restored->fresh()->billing_email)->toBe('susann@example.com');
$service->repairLoggedShoppingUserDeletions(false);
$restored = $restored->fresh();
expect($restored->billing_email)->toEndWith('@dsgvo.invalid')
->and($restored->billing_phone)->toBeNull()
->and($restored->isGdprDeleted())->toBeTrue()
->and($lateCustomer->fresh()->billing_email)->toBe('susann-neu@example.com')
->and($lateCustomer->fresh()->isGdprDeleted())->toBeFalse()
->and($service->repairLoggedShoppingUserDeletions(true))->toBe([]);
});