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>
214 lines
7.3 KiB
PHP
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([]);
|
|
});
|