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:
Kevin 2026-10-06 12:36:02 +00:00
parent 083d643220
commit 3d0864713b
19 changed files with 635 additions and 90 deletions

View file

@ -10,6 +10,7 @@ use App\Services\GdprDeletionService;
use App\Services\HTMLHelper;
use App\Services\Shop;
use Auth;
use Illuminate\Http\RedirectResponse;
use Request;
use Validator;
@ -29,6 +30,7 @@ class CustomerController extends Controller
if (Request::get('reset') === 'filter') {
set_user_attr('filter_member_id', null);
set_user_attr('filter_customer_member', null);
set_user_attr('filter_gdpr_deleted', null);
return redirect(route('admin_customers'));
}
@ -82,6 +84,10 @@ class CustomerController extends Controller
{
$shopping_user = ShoppingUser::withTrashed()->findOrFail($id);
if (! Auth::user()->isSuperAdmin()) {
abort(403);
}
if (! Request::get('gdpr_delete_customer')) {
\Session()->flash('alert-error', __('msg.gdpr_confirm_checkbox_missing'));
@ -89,8 +95,8 @@ class CustomerController extends Controller
}
try {
app(GdprDeletionService::class)->deleteShoppingUser($shopping_user, Auth::id());
\Session()->flash('alert-success', __('msg.gdpr_delete_customer_success'));
$summary = app(GdprDeletionService::class)->deleteShoppingUser($shopping_user, Auth::id());
\Session()->flash('alert-success', __('msg.gdpr_delete_customer_success', ['count' => $summary['shopping_users_anonymized']]));
} catch (GdprDeletionException $e) {
\Session()->flash('alert-error', $e->getMessage());
@ -107,6 +113,9 @@ class CustomerController extends Controller
$shopping_user->id = 'new';
} else {
$shopping_user = ShoppingUser::findOrFail($id);
if ($shopping_user->isGdprDeleted()) {
return $this->redirectGdprLocked($shopping_user);
}
}
$data = [
'shopping_user' => $shopping_user,
@ -168,6 +177,9 @@ class CustomerController extends Controller
}
$shopping_user = ShoppingUser::findOrFail($id);
if ($shopping_user->isGdprDeleted()) {
return $this->redirectGdprLocked($shopping_user);
}
$data['language'] = isset($data['language']) ? $data['language'] : \App::getLocale();
$data['has_buyed'] = isset($data['has_buyed']) ? true : false;
@ -196,6 +208,13 @@ class CustomerController extends Controller
{
$query = ShoppingUser::select('shopping_users.*')->where('auth_user_id', '=', null);
set_user_attr('filter_gdpr_deleted', Request::get('filter_gdpr_deleted'));
if (Request::get('filter_gdpr_deleted') === 'only') {
$query->whereNotNull('gdpr_deleted_at');
} elseif (Request::get('filter_gdpr_deleted') !== 'all') {
$query->withoutGdprDeleted();
}
set_user_attr('filter_member_id', Request::get('filter_member_id'));
if (Request::get('filter_member_id') != '') {
$query->where('member_id', '=', Request::get('filter_member_id'));
@ -224,7 +243,8 @@ class CustomerController extends Controller
return $ShoppingUser->billing_country ? $ShoppingUser->billing_country->getLocated() : '';
})
->addColumn('isMember', function (ShoppingUser $ShoppingUser) {
return get_active_badge($ShoppingUser->auth_user_id).($ShoppingUser->mode === 'dev' ? ' <span class="badge badge-warning">dev</span>' : '');
return get_active_badge($ShoppingUser->auth_user_id).($ShoppingUser->mode === 'dev' ? ' <span class="badge badge-warning">dev</span>' : '')
.($ShoppingUser->isGdprDeleted() ? ' <span class="badge badge-secondary">DSGVO-gelöscht</span>' : '');
})
->addColumn('member_id', function (ShoppingUser $ShoppingUser) {
if ($ShoppingUser->is_like) {
@ -263,4 +283,11 @@ class CustomerController extends Controller
->rawColumns(['id', 'subscribed', 'isMember', 'member_id'])
->make(true);
}
protected function redirectGdprLocked(ShoppingUser $shopping_user): RedirectResponse
{
\Session()->flash('alert-error', __('msg.gdpr_customer_locked'));
return redirect(route('admin_customer_detail', [$shopping_user->id]));
}
}

View file

@ -11,6 +11,7 @@ use App\Services\HTMLHelper;
use App\Services\Shop;
use App\Services\ShoppingUserService;
use App\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Request;
use Validator;
@ -61,6 +62,9 @@ class CustomerController extends Controller
if ($shopping_user->member_id != \Auth::user()->id) {
abort(404);
}
if ($shopping_user->isGdprDeleted()) {
return $this->redirectGdprLocked($shopping_user);
}
$data = [
'shopping_user' => $shopping_user,
'isAdmin' => false,
@ -81,6 +85,9 @@ class CustomerController extends Controller
if ($shopping_user->member_id != \Auth::user()->id) {
abort(404);
}
if ($shopping_user->isGdprDeleted()) {
return $this->redirectGdprLocked($shopping_user);
}
}
$billing_email = null;
@ -142,6 +149,12 @@ class CustomerController extends Controller
if ($action === 'save') {
$shopping_user = ShoppingUser::findOrFail($id);
if ($shopping_user->member_id != \Auth::user()->id) {
abort(404);
}
if ($shopping_user->isGdprDeleted()) {
return $this->redirectGdprLocked($shopping_user);
}
$shopping_user->faker_mail = false;
$shopping_user->billing_email = Request::get($email);
$shopping_user->save();
@ -213,6 +226,9 @@ class CustomerController extends Controller
if ($shopping_user->member_id != \Auth::user()->id) {
abort(404);
}
if ($shopping_user->isGdprDeleted()) {
return $this->redirectGdprLocked($shopping_user);
}
CustomerPriority::checkChangeOne($shopping_user, $data, true);
$shopping_user->fill($data);
$shopping_user->save();
@ -261,7 +277,8 @@ class CustomerController extends Controller
$query->select(DB::raw('MAX(id)'))
->from('shopping_users')
->groupBy('billing_email');
})->where('shopping_users.member_id', '=', $user->id)->where('shopping_users.auth_user_id', '=', null);
})->where('shopping_users.member_id', '=', $user->id)->where('shopping_users.auth_user_id', '=', null)
->withoutGdprDeleted();
if (Request::get('isfor') === 'ot-member') { // Bestellung für Kunden
}
@ -327,4 +344,11 @@ class CustomerController extends Controller
->rawColumns(['send_to', 'id', 'subscribed', 'extras', 'status'])
->make(true);
}
protected function redirectGdprLocked(ShoppingUser $shopping_user): RedirectResponse
{
\Session()->flash('alert-error', __('msg.gdpr_customer_locked'));
return redirect(route('user_customer_detail', [$shopping_user->id]));
}
}

View file

@ -273,7 +273,7 @@ class CheckoutController extends Controller
];
if (Request::get('is_from') === 'shopping') {
$rules['customer_order_source_comment'] = 'required|string|max:500';
$rules['customer_order_source_comment'] = 'nullable|string|max:500';
}
if (Request::get('same_as_billing')) {
@ -287,9 +287,7 @@ class CheckoutController extends Controller
]);
}
return Validator::make(Request::all(), $rules, [
'customer_order_source_comment.required' => __('order.source_comment_required'),
]);
return Validator::make(Request::all(), $rules);
}
/**