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>
This commit is contained in:
parent
083d643220
commit
3d0864713b
19 changed files with 635 additions and 90 deletions
|
|
@ -4,6 +4,7 @@ namespace App\Services;
|
|||
|
||||
use App\Exceptions\GdprDeletionException;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Logger;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
|
|
@ -13,6 +14,7 @@ use App\Models\UserShop;
|
|||
use App\Models\UserUpdateEmail;
|
||||
use App\Services\Payment\Stripe\StripeCustomerService;
|
||||
use App\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -220,7 +222,10 @@ class GdprDeletionService
|
|||
* orders_count: int,
|
||||
* orders_by_row: array<int, int>,
|
||||
* active_abos: int,
|
||||
* can_purge: bool
|
||||
* can_purge: bool,
|
||||
* gdpr_deleted_rows: int,
|
||||
* gdpr_deleted_at: Carbon|null,
|
||||
* all_gdpr_deleted: bool
|
||||
* }
|
||||
*/
|
||||
public function analyzeShoppingUser(ShoppingUser $shoppingUser): array
|
||||
|
|
@ -239,6 +244,7 @@ class GdprDeletionService
|
|||
$isMember = $rows->contains(fn (ShoppingUser $row) => (int) $row->auth_user_id > 0);
|
||||
$ordersCount = array_sum($ordersByRow);
|
||||
$activeAbos = UserAbo::whereIn('shopping_user_id', $rowIds)->where('active', true)->count();
|
||||
$gdprDeletedRows = $rows->filter(fn (ShoppingUser $row) => $row->isGdprDeleted());
|
||||
|
||||
return [
|
||||
'rows' => $rows,
|
||||
|
|
@ -248,9 +254,76 @@ class GdprDeletionService
|
|||
'orders_by_row' => $ordersByRow,
|
||||
'active_abos' => $activeAbos,
|
||||
'can_purge' => ! $isMember && $ordersCount === 0 && $activeAbos === 0,
|
||||
'gdpr_deleted_rows' => $gdprDeletedRows->count(),
|
||||
'gdpr_deleted_at' => $gdprDeletedRows->max('gdpr_deleted_at'),
|
||||
'all_gdpr_deleted' => $rows->isNotEmpty() && $gdprDeletedRows->count() === $rows->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Protokollierte DSGVO-Löschungen von Endkunden nachziehen: Einträge, die vor
|
||||
* der Löschung angelegt waren und deren Kontaktdaten danach wieder befüllt
|
||||
* wurden (z. B. durch ein offenes Bearbeiten-Formular), werden erneut
|
||||
* anonymisiert und markiert. Später angelegte Einträge (neue Bestellung nach
|
||||
* der Löschung) bleiben unberührt.
|
||||
*
|
||||
* @return array<int, array{id: int, number: int|null, deletion_logged_at: string}>
|
||||
*/
|
||||
public function repairLoggedShoppingUserDeletions(bool $dryRun = true): array
|
||||
{
|
||||
$deletions = Logger::query()
|
||||
->whereIn('action', ['gdpr-delete-customer', 'gdpr-purge-customer'])
|
||||
->where('model', ShoppingUser::class)
|
||||
->selectRaw('model_id, MAX(created_at) as deleted_at')
|
||||
->groupBy('model_id')
|
||||
->get();
|
||||
|
||||
$repaired = [];
|
||||
foreach ($deletions as $deletion) {
|
||||
$target = ShoppingUser::withTrashed()->find($deletion->model_id);
|
||||
if (! $target || (int) $target->auth_user_id > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows = ShoppingUser::withTrashed()
|
||||
->where(function ($query) use ($target) {
|
||||
$query->where('id', $target->id);
|
||||
if ($target->number > 0) {
|
||||
$query->orWhere('number', $target->number);
|
||||
}
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->whereNull('auth_user_id')->orWhere('auth_user_id', 0);
|
||||
})
|
||||
->where('created_at', '<=', $deletion->deleted_at)
|
||||
->get()
|
||||
->filter(fn (ShoppingUser $row) => ! $row->isGdprDeleted() || ! str_ends_with((string) $row->billing_email, '@dsgvo.invalid'))
|
||||
->reject(fn (ShoppingUser $row) => isset($repaired[$row->id]));
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$repaired[$row->id] = [
|
||||
'id' => $row->id,
|
||||
'number' => $row->number,
|
||||
'deletion_logged_at' => (string) $deletion->deleted_at,
|
||||
];
|
||||
}
|
||||
|
||||
if (! $dryRun) {
|
||||
DB::transaction(fn () => $this->anonymizeShoppingUserRows($rows));
|
||||
$this->logDeletion('gdpr-repair-customer', $target->id, ShoppingUser::class, null, null, [
|
||||
'shopping_user_id' => $target->id,
|
||||
'shopping_users_anonymized' => $rows->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($repaired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Datensätze eines Endkunden, inkl. soft-deleted: der Eintrag selbst,
|
||||
* alle Einträge mit derselben Kundennummer und alle Endkunden-Einträge mit
|
||||
|
|
@ -494,6 +567,7 @@ class GdprDeletionService
|
|||
$row->notice = [];
|
||||
$row->subscribed = false;
|
||||
$row->faker_mail = true;
|
||||
$row->gdpr_deleted_at ??= now();
|
||||
$row->save();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue