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>
This commit is contained in:
parent
30bbfa32a0
commit
60f9985313
12 changed files with 631 additions and 88 deletions
|
|
@ -45,6 +45,7 @@ class CustomerController extends Controller
|
|||
$shopping_user = ShoppingUser::findOrFail($id);
|
||||
$data = [
|
||||
'shopping_user' => $shopping_user,
|
||||
'deletion' => app(GdprDeletionService::class)->analyzeShoppingUser($shopping_user),
|
||||
'isAdmin' => true,
|
||||
'isView' => 'customer',
|
||||
];
|
||||
|
|
@ -52,16 +53,26 @@ class CustomerController extends Controller
|
|||
return view('admin.customer.detail', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Komplett-Löschung (nur ohne Bestellungen), dev/_tickets/2026-09-08.md #2.
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$shopping_user = ShoppingUser::findOrFail($id);
|
||||
$result = $this->customerRepository->deleteCustomer($shopping_user);
|
||||
if (! $result) {
|
||||
return back()->with('alert-error', 'Kunde hat Bestellungen. Löschen nicht möglich.');
|
||||
} else {
|
||||
\Session()->flash('alert-success', 'Kunde wurde gelöscht');
|
||||
$shopping_user = ShoppingUser::withTrashed()->findOrFail($id);
|
||||
|
||||
return redirect(route('admin_customers'));
|
||||
if (! Request::get('delete_customer')) {
|
||||
\Session()->flash('alert-error', __('msg.delete_confirm_checkbox_missing'));
|
||||
|
||||
return redirect(route('admin_customer_detail', [$shopping_user->id]));
|
||||
}
|
||||
|
||||
try {
|
||||
$summary = app(GdprDeletionService::class)->purgeShoppingUser($shopping_user, Auth::id());
|
||||
\Session()->flash('alert-success', __('msg.gdpr_purge_customer_success', ['count' => $summary['shopping_users_purged']]));
|
||||
} catch (GdprDeletionException $e) {
|
||||
\Session()->flash('alert-error', $e->getMessage());
|
||||
|
||||
return redirect(route('admin_customer_detail', [$shopping_user->id]));
|
||||
}
|
||||
|
||||
return redirect(route('admin_customers'));
|
||||
|
|
@ -72,7 +83,7 @@ class CustomerController extends Controller
|
|||
$shopping_user = ShoppingUser::withTrashed()->findOrFail($id);
|
||||
|
||||
if (! Request::get('gdpr_delete_customer')) {
|
||||
\Session()->flash('alert-danger', __('msg.gdpr_confirm_checkbox_missing'));
|
||||
\Session()->flash('alert-error', __('msg.gdpr_confirm_checkbox_missing'));
|
||||
|
||||
return redirect(route('admin_customer_detail', [$shopping_user->id]));
|
||||
}
|
||||
|
|
@ -81,7 +92,7 @@ class CustomerController extends Controller
|
|||
app(GdprDeletionService::class)->deleteShoppingUser($shopping_user, Auth::id());
|
||||
\Session()->flash('alert-success', __('msg.gdpr_delete_customer_success'));
|
||||
} catch (GdprDeletionException $e) {
|
||||
\Session()->flash('alert-danger', $e->getMessage());
|
||||
\Session()->flash('alert-error', $e->getMessage());
|
||||
|
||||
return redirect(route('admin_customer_detail', [$shopping_user->id]));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\ShoppingUser;
|
||||
|
||||
class CustomerRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
|
|
@ -11,44 +9,6 @@ class CustomerRepository extends BaseRepository
|
|||
// $this->model = $model;
|
||||
}
|
||||
|
||||
public function deleteCustomer(ShoppingUser $shopping_user)
|
||||
{
|
||||
if ($shopping_user->shopping_orders->count() > 0) {
|
||||
return false;
|
||||
}
|
||||
$shopping_user->auth_user_id = null;
|
||||
$shopping_user->member_id = null;
|
||||
$shopping_user->billing_salutation = null;
|
||||
$shopping_user->billing_company = null;
|
||||
$shopping_user->billing_firstname = 'Deleted';
|
||||
$shopping_user->billing_lastname = 'User';
|
||||
$shopping_user->billing_address = 'Deleted';
|
||||
$shopping_user->billing_address_2 = null;
|
||||
$shopping_user->billing_zipcode = '00000';
|
||||
$shopping_user->billing_city = 'Deleted';
|
||||
$shopping_user->billing_phone = null;
|
||||
$shopping_user->billing_email = 'deleted_'.$shopping_user->id.'@deleted.com';
|
||||
|
||||
$shopping_user->shipping_salutation = null;
|
||||
$shopping_user->shipping_company = null;
|
||||
$shopping_user->shipping_firstname = 'Deleted';
|
||||
$shopping_user->shipping_lastname = 'User';
|
||||
$shopping_user->shipping_address = 'Deleted';
|
||||
$shopping_user->shipping_address_2 = null;
|
||||
$shopping_user->shipping_zipcode = '00000';
|
||||
$shopping_user->shipping_city = 'Deleted';
|
||||
$shopping_user->shipping_phone = null;
|
||||
$shopping_user->shipping_email = 'deleted_'.$shopping_user->id.'@deleted.com';
|
||||
|
||||
$shopping_user->remarks = null;
|
||||
$shopping_user->notice = null;
|
||||
$shopping_user->faker_mail = true;
|
||||
|
||||
$shopping_user->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function update($data)
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\GdprDeletionException;
|
||||
use App\Models\Customer;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAccount;
|
||||
|
|
@ -30,6 +32,12 @@ use Illuminate\Support\Facades\Storage;
|
|||
* denormalisierten Personendaten werden anonymisiert.
|
||||
* - Stripe-Customer-Objekte werden gelöscht (Phase SE1.6, dev/2026-08-26-stripe/);
|
||||
* die Charges bleiben bei Stripe erhalten — sie sind Buchungsbelege.
|
||||
*
|
||||
* Endkunden werden pro Bestellung neu angelegt (mehrere shopping_users-Zeilen).
|
||||
* Alle Kunden-Operationen arbeiten deshalb auf der Gruppe aller Einträge mit
|
||||
* derselben Kundennummer ODER derselben E-Mail-Adresse (dev/_tickets/2026-09-08.md, #2):
|
||||
* - deleteShoppingUser(): DSGVO-Anonymisierung (Pflicht, sobald Bestellungen vorliegen)
|
||||
* - purgeShoppingUser(): Komplett-Löschung, nur solange kein Eintrag eine Bestellung hat
|
||||
*/
|
||||
class GdprDeletionService
|
||||
{
|
||||
|
|
@ -128,13 +136,14 @@ class GdprDeletionService
|
|||
|
||||
$stripeCustomerIds = $this->collectStripeCustomerIds(null, $rows);
|
||||
|
||||
$summary = DB::transaction(function () use ($shoppingUser, $rows) {
|
||||
$summary = DB::transaction(function () use ($shoppingUser, $rows, $originalEmail) {
|
||||
$summary = ['shopping_user_id' => $shoppingUser->id];
|
||||
$summary['abos_anonymized'] = 0;
|
||||
foreach ($rows as $row) {
|
||||
$summary['abos_anonymized'] += $this->anonymizeUserAbos('shopping_user_id', $row->id);
|
||||
}
|
||||
$summary['shopping_users_anonymized'] = $this->anonymizeShoppingUserRows($rows);
|
||||
$summary['portal_customers_deleted'] = $this->deletePortalCustomers($rows, $originalEmail);
|
||||
|
||||
return $summary;
|
||||
});
|
||||
|
|
@ -147,17 +156,170 @@ class GdprDeletionService
|
|||
}
|
||||
|
||||
/**
|
||||
* Alle Datensätze eines Endkunden (gleiche Kundennummer), inkl. soft-deleted.
|
||||
* Endkunden (ShoppingUser) komplett löschen — nur solange keiner seiner
|
||||
* Einträge eine Bestellung hat (dann gibt es keine Aufbewahrungspflicht).
|
||||
*
|
||||
* Alle Einträge der Gruppe werden vollständig anonymisiert und soft-deleted,
|
||||
* zugehörige (inaktive) Abos verlieren Kontakt- und Zahlungsdaten, die
|
||||
* Portal-Logins (customers) werden entfernt. Ein Hard-Delete ist bewusst nicht
|
||||
* vorgesehen: Fremdschlüssel (user_abos, checkout_funnel_events, Logs) zeigen
|
||||
* auf shopping_users, und der Lösch-Nachweis bleibt so erhalten.
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*
|
||||
* @throws GdprDeletionException wenn der Datensatz zu einem Berater gehört, Bestellungen oder aktive Abos bestehen
|
||||
*/
|
||||
public function purgeShoppingUser(ShoppingUser $shoppingUser, ?int $executedByUserId = null): array
|
||||
{
|
||||
$analysis = $this->analyzeShoppingUser($shoppingUser);
|
||||
|
||||
if ($analysis['is_member']) {
|
||||
throw new GdprDeletionException(__('msg.gdpr_error_customer_is_member'));
|
||||
}
|
||||
if ($analysis['orders_count'] > 0) {
|
||||
throw new GdprDeletionException(__('msg.gdpr_error_customer_has_orders'));
|
||||
}
|
||||
if ($analysis['active_abos'] > 0) {
|
||||
throw new GdprDeletionException(__('msg.gdpr_error_active_abo'));
|
||||
}
|
||||
|
||||
$rows = $analysis['rows'];
|
||||
$originalEmail = $shoppingUser->billing_email;
|
||||
$stripeCustomerIds = $this->collectStripeCustomerIds(null, $rows);
|
||||
|
||||
$summary = DB::transaction(function () use ($shoppingUser, $rows, $originalEmail) {
|
||||
$summary = ['shopping_user_id' => $shoppingUser->id];
|
||||
$summary['abos_anonymized'] = 0;
|
||||
$summary['shopping_users_purged'] = 0;
|
||||
foreach ($rows as $row) {
|
||||
$summary['abos_anonymized'] += $this->anonymizeUserAbos('shopping_user_id', $row->id);
|
||||
$this->anonymizeShoppingUserComplete($row);
|
||||
$row->delete();
|
||||
$summary['shopping_users_purged']++;
|
||||
}
|
||||
$summary['portal_customers_deleted'] = $this->deletePortalCustomers($rows, $originalEmail);
|
||||
|
||||
return $summary;
|
||||
});
|
||||
|
||||
$summary['stripe_customers_deleted'] = $this->deleteStripeCustomers($stripeCustomerIds);
|
||||
|
||||
$this->logDeletion('gdpr-purge-customer', $shoppingUser->id, ShoppingUser::class, $executedByUserId, $originalEmail, $summary);
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse für das Lösch-Modal: Wie oft ist der Kunde angelegt, welche Einträge
|
||||
* haben Bestellungen, und welche Löschart ist möglich?
|
||||
*
|
||||
* @return array{
|
||||
* rows: \Illuminate\Database\Eloquent\Collection<int, ShoppingUser>,
|
||||
* email: string|null,
|
||||
* is_member: bool,
|
||||
* orders_count: int,
|
||||
* orders_by_row: array<int, int>,
|
||||
* active_abos: int,
|
||||
* can_purge: bool
|
||||
* }
|
||||
*/
|
||||
public function analyzeShoppingUser(ShoppingUser $shoppingUser): array
|
||||
{
|
||||
$rows = $this->getCustomerRows($shoppingUser);
|
||||
$rowIds = $rows->pluck('id')->all();
|
||||
|
||||
$ordersByRow = ShoppingOrder::withTrashed()
|
||||
->whereIn('shopping_user_id', $rowIds)
|
||||
->selectRaw('shopping_user_id, count(*) as orders_count')
|
||||
->groupBy('shopping_user_id')
|
||||
->pluck('orders_count', 'shopping_user_id')
|
||||
->map(fn ($count) => (int) $count)
|
||||
->all();
|
||||
|
||||
$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();
|
||||
|
||||
return [
|
||||
'rows' => $rows,
|
||||
'email' => $this->groupingEmail($shoppingUser),
|
||||
'is_member' => $isMember,
|
||||
'orders_count' => $ordersCount,
|
||||
'orders_by_row' => $ordersByRow,
|
||||
'active_abos' => $activeAbos,
|
||||
'can_purge' => ! $isMember && $ordersCount === 0 && $activeAbos === 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Datensätze eines Endkunden, inkl. soft-deleted: der Eintrag selbst,
|
||||
* alle Einträge mit derselben Kundennummer und alle Endkunden-Einträge mit
|
||||
* derselben E-Mail-Adresse (Kunden werden pro Bestellung neu angelegt und
|
||||
* bekommen nicht immer dieselbe Kundennummer).
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, ShoppingUser>
|
||||
*/
|
||||
public function getCustomerRows(ShoppingUser $shoppingUser)
|
||||
{
|
||||
if ($shoppingUser->number > 0) {
|
||||
return ShoppingUser::withTrashed()->where('number', $shoppingUser->number)->get();
|
||||
$email = $this->groupingEmail($shoppingUser);
|
||||
|
||||
return ShoppingUser::withTrashed()
|
||||
->where(function ($query) use ($shoppingUser, $email) {
|
||||
$query->where('id', $shoppingUser->id);
|
||||
if ($shoppingUser->number > 0) {
|
||||
$query->orWhere('number', $shoppingUser->number);
|
||||
}
|
||||
if ($email !== null) {
|
||||
$query->orWhere(function ($emailQuery) use ($email) {
|
||||
$emailQuery->whereRaw('LOWER(billing_email) = ?', [$email])
|
||||
->where(function ($endCustomer) {
|
||||
$endCustomer->whereNull('auth_user_id')->orWhere('auth_user_id', 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
->orderBy('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* E-Mail-Adresse, über die Mehrfach-Anlagen eines Kunden erkannt werden.
|
||||
* Platzhalter-Adressen (Faker, bereits anonymisiert) taugen nicht zur
|
||||
* Gruppierung — sie würden fremde Kunden zusammenziehen.
|
||||
*/
|
||||
public function groupingEmail(ShoppingUser $shoppingUser): ?string
|
||||
{
|
||||
$email = mb_strtolower(trim((string) $shoppingUser->billing_email));
|
||||
if ($email === '' || $shoppingUser->faker_mail) {
|
||||
return null;
|
||||
}
|
||||
foreach (['faker@mivita.care', '@dsgvo.invalid', '@deleted.com'] as $placeholder) {
|
||||
if (str_ends_with($email, $placeholder)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return ShoppingUser::withTrashed()->where('id', $shoppingUser->id)->get();
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal-Logins (customers) des Kunden entfernen: Sie enthalten Name und
|
||||
* E-Mail und sind reine Zugangsdaten ohne Aufbewahrungspflicht.
|
||||
*
|
||||
* @param Collection<int, ShoppingUser>|\Illuminate\Database\Eloquent\Collection<int, ShoppingUser> $rows
|
||||
*/
|
||||
protected function deletePortalCustomers($rows, ?string $email): int
|
||||
{
|
||||
$email = mb_strtolower(trim((string) $email));
|
||||
|
||||
return Customer::query()
|
||||
->where(function ($query) use ($rows, $email) {
|
||||
$query->whereIn('shopping_user_id', collect($rows)->pluck('id')->all());
|
||||
if ($email !== '') {
|
||||
$query->orWhereRaw('LOWER(email) = ?', [$email]);
|
||||
}
|
||||
})
|
||||
->delete();
|
||||
}
|
||||
|
||||
protected function anonymizeUserRow(User $user): void
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ return [
|
|||
'gdpr_error_is_admin' => 'DSGVO-Löschung nicht möglich: Der Account hat Admin-Rechte. Bitte entziehe zuerst die Admin-Rechte.',
|
||||
'gdpr_error_active_abo' => 'DSGVO-Löschung nicht möglich: Es besteht noch ein aktives Abo. Bitte kündige das Abo zuerst.',
|
||||
'gdpr_error_customer_is_member' => 'DSGVO-Löschung nicht möglich: Dieser Kundendatensatz gehört zu einem Berater-Account. Bitte nutze die DSGVO-Löschung in der User-Verwaltung.',
|
||||
'delete_confirm_checkbox_missing' => 'Bitte bestätige die Löschung über die Checkbox.',
|
||||
'gdpr_purge_customer_button' => 'Kunde komplett löschen',
|
||||
'gdpr_purge_customer_success' => 'Kunde wurde komplett gelöscht: :count Kundeneintrag/-einträge entfernt.',
|
||||
'gdpr_error_customer_has_orders' => 'Komplett löschen nicht möglich: Der Kunde hat bereits Bestellungen. Es ist nur eine DSGVO-Löschung möglich.',
|
||||
];
|
||||
/*
|
||||
{{ __('msg.') }}
|
||||
|
|
|
|||
|
|
@ -50,4 +50,8 @@ return [
|
|||
'gdpr_error_is_admin' => 'GDPR deletion not possible: The account has admin rights. Please remove the admin rights first.',
|
||||
'gdpr_error_active_abo' => 'GDPR deletion not possible: There is still an active subscription. Please cancel the subscription first.',
|
||||
'gdpr_error_customer_is_member' => 'GDPR deletion not possible: This customer record belongs to a consultant account. Please use the GDPR deletion in the user administration.',
|
||||
'delete_confirm_checkbox_missing' => 'Please confirm the deletion via the checkbox.',
|
||||
'gdpr_purge_customer_button' => 'Delete customer completely',
|
||||
'gdpr_purge_customer_success' => 'Customer deleted completely: :count customer record(s) removed.',
|
||||
'gdpr_error_customer_has_orders' => 'Complete deletion not possible: The customer already has orders. Only a GDPR deletion is possible.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -50,4 +50,8 @@ return [
|
|||
'gdpr_error_is_admin' => 'Eliminación RGPD no posible: La cuenta tiene derechos de administrador. Por favor retira primero los derechos de administrador.',
|
||||
'gdpr_error_active_abo' => 'Eliminación RGPD no posible: Todavía existe una suscripción activa. Por favor cancela primero la suscripción.',
|
||||
'gdpr_error_customer_is_member' => 'Eliminación RGPD no posible: Este registro de cliente pertenece a una cuenta de asesor. Por favor utiliza la eliminación RGPD en la administración de usuarios.',
|
||||
'delete_confirm_checkbox_missing' => 'Confirma la eliminación mediante la casilla.',
|
||||
'gdpr_purge_customer_button' => 'Eliminar cliente por completo',
|
||||
'gdpr_purge_customer_success' => 'Cliente eliminado por completo: :count registro(s) de cliente eliminado(s).',
|
||||
'gdpr_error_customer_has_orders' => 'Eliminación completa no posible: el cliente ya tiene pedidos. Solo es posible una eliminación RGPD.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -48,4 +48,8 @@ return [
|
|||
'gdpr_error_is_admin' => "Suppression RGPD impossible : Le compte dispose de droits d'administrateur. Veuillez d'abord retirer les droits d'administrateur.",
|
||||
'gdpr_error_active_abo' => "Suppression RGPD impossible : Un abonnement actif existe encore. Veuillez d'abord résilier l'abonnement.",
|
||||
'gdpr_error_customer_is_member' => "Suppression RGPD impossible : Cette fiche client appartient à un compte de conseiller. Veuillez utiliser la suppression RGPD dans l'administration des utilisateurs.",
|
||||
'delete_confirm_checkbox_missing' => 'Veuillez confirmer la suppression via la case à cocher.',
|
||||
'gdpr_purge_customer_button' => 'Supprimer complètement le client',
|
||||
'gdpr_purge_customer_success' => 'Client supprimé complètement : :count enregistrement(s) client supprimé(s).',
|
||||
'gdpr_error_customer_has_orders' => 'Suppression complète impossible : le client a déjà des commandes. Seule une suppression RGPD est possible.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a class="btn btn-sm btn-secondary mt-2" href="{{route('admin_customer_edit', [$shopping_user->id])}}">{{ __('customer.edit_customer_data') }}</a>
|
||||
<a class="btn btn-sm btn-danger mt-2" href="{{route('admin_customer_delete', [$shopping_user->id])}}" onclick="return confirm('{{ __('customer.delete_customer_confirm') }}')">{{ __('customer.delete_customer') }}</a>
|
||||
<button type="button" class="btn btn-sm btn-danger mt-2" data-toggle="modal" data-target="#modals-customer-delete">{{ __('customer.delete_customer') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
|
|
|
|||
|
|
@ -9,40 +9,102 @@
|
|||
@include('admin.customer._detail')
|
||||
<a href="{{route('admin_customers')}}" class="btn btn-sm btn-default mt-2 float-right">{{ __('back') }}</a>
|
||||
|
||||
@if (Auth::user()->isSuperAdmin() && !$shopping_user->auth_user_id)
|
||||
<button type="button" class="btn btn-sm btn-danger mt-2" data-toggle="modal"
|
||||
data-target="#modals-customer-gdpr-delete">{{ __('msg.gdpr_delete_customer_button') }}</button>
|
||||
{{-- Kunden löschen: ein Modal für beide Wege (dev/_tickets/2026-09-08.md, Ticket #2) --}}
|
||||
@php($canGdprDelete = Auth::user()->isSuperAdmin())
|
||||
<div class="modal fade" id="modals-customer-delete">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('customer.delete_customer') }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if ($deletion['is_member'])
|
||||
<div class="alert alert-warning mb-0">{{ __('msg.gdpr_error_customer_is_member') }}</div>
|
||||
@else
|
||||
<p>
|
||||
Der Kunde ist <b>{{ $deletion['rows']->count() }}-mal</b> angelegt
|
||||
@if ($deletion['email'])
|
||||
(E-Mail <b>{{ $deletion['email'] }}</b>, Kundennummer {{ $shopping_user->number ?: '-' }}).
|
||||
@else
|
||||
(Kundennummer {{ $shopping_user->number ?: '-' }}; keine gültige E-Mail zur Duplikat-Suche).
|
||||
@endif
|
||||
Alle folgenden Einträge werden gemeinsam behandelt:
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered" id="customer-delete-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Kd-Nr.</th>
|
||||
<th>Name</th>
|
||||
<th>Berater</th>
|
||||
<th>Bestellungen</th>
|
||||
<th>Angelegt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($deletion['rows'] as $row)
|
||||
<tr class="{{ $row->id === $shopping_user->id ? 'font-weight-bold' : '' }}">
|
||||
<td>{{ $row->id }}</td>
|
||||
<td>{{ $row->number ?: '-' }}</td>
|
||||
<td>{{ trim($row->billing_firstname.' '.$row->billing_lastname) }}
|
||||
@if ($row->deleted_at) <span class="badge badge-secondary">gelöscht</span> @endif
|
||||
</td>
|
||||
<td>{{ $row->member->email ?? '-' }}</td>
|
||||
<td>{{ $deletion['orders_by_row'][$row->id] ?? 0 }}</td>
|
||||
<td>{{ $row->created_at ? $row->created_at->format('d.m.Y') : '-' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Modal DSGVO-Löschung -->
|
||||
<div class="modal fade" id="modals-customer-gdpr-delete">
|
||||
<div class="modal-dialog">
|
||||
<form class="modal-content" action="{{ route('admin_customer_gdpr_delete', [$shopping_user->id]) }}"
|
||||
method="post">
|
||||
@csrf
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('msg.gdpr_delete_customer_button') }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Alle personenbezogenen Daten dieses Kunden werden <b>unwiderruflich anonymisiert</b>
|
||||
(alle Einträge mit der Kundennummer {{ $shopping_user->number }}).
|
||||
Bei Bestellungen mit Rechnung bleiben Name und Rechnungsadresse wegen der
|
||||
gesetzlichen Aufbewahrungspflichten erhalten; alle Kontaktdaten
|
||||
(E-Mail, Telefon, Notizen, Newsletter) werden entfernt.
|
||||
Aktive Abos müssen vorher gekündigt werden.</p>
|
||||
<label class="custom-control custom-checkbox m-0">
|
||||
<input type="checkbox" class="custom-control-input" name="gdpr_delete_customer" value="1">
|
||||
<span class="custom-control-label">DSGVO-Löschung unwiderruflich ausführen?</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ __('close') }}</button>
|
||||
<button type="submit"
|
||||
class="btn btn-danger">{{ __('msg.gdpr_delete_customer_button') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
@if ($deletion['orders_count'] > 0)
|
||||
<div class="alert alert-warning">
|
||||
Der Kunde hat schon <b>{{ $deletion['orders_count'] }} Bestellung(en)</b>. Es ist nur <b>DSGVO löschen</b> möglich:
|
||||
Alle personenbezogenen Daten werden unwiderruflich anonymisiert. Bei Einträgen mit Bestellung bleiben Name und
|
||||
Rechnungsadresse wegen der gesetzlichen Aufbewahrungspflichten erhalten; alle Kontaktdaten (E-Mail, Telefon,
|
||||
Notizen, Newsletter) sowie die Portal-Logins werden entfernt.
|
||||
@if ($deletion['active_abos'] > 0)
|
||||
<br><b>{{ __('msg.gdpr_error_active_abo') }}</b>
|
||||
@endif
|
||||
</div>
|
||||
@if ($canGdprDelete)
|
||||
<form action="{{ route('admin_customer_gdpr_delete', [$shopping_user->id]) }}" method="post" id="form-customer-gdpr-delete">
|
||||
@csrf
|
||||
<label class="custom-control custom-checkbox mb-3">
|
||||
<input type="checkbox" class="custom-control-input" name="gdpr_delete_customer" value="1">
|
||||
<span class="custom-control-label">DSGVO-Löschung unwiderruflich ausführen?</span>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-danger" {{ $deletion['active_abos'] > 0 ? 'disabled' : '' }}>{{ __('msg.gdpr_delete_customer_button') }}</button>
|
||||
</form>
|
||||
@else
|
||||
<p class="text-muted mb-0">Die DSGVO-Löschung kann nur von einem Superadmin ausgeführt werden.</p>
|
||||
@endif
|
||||
@elseif ($deletion['active_abos'] > 0)
|
||||
<div class="alert alert-warning mb-0">{{ __('msg.gdpr_error_active_abo') }}</div>
|
||||
@else
|
||||
<div class="alert alert-info">
|
||||
Der Kunde hat <b>keine Bestellungen</b> und kann <b>komplett gelöscht</b> werden: Alle oben aufgeführten Einträge
|
||||
werden vollständig anonymisiert und entfernt, inklusive Portal-Login und inaktiver Abos.
|
||||
</div>
|
||||
<form action="{{ route('admin_customer_delete', [$shopping_user->id]) }}" method="post" id="form-customer-delete">
|
||||
@csrf
|
||||
<label class="custom-control custom-checkbox mb-3">
|
||||
<input type="checkbox" class="custom-control-input" name="delete_customer" value="1">
|
||||
<span class="custom-control-label">Kunden mit allen {{ $deletion['rows']->count() }} Einträgen unwiderruflich löschen?</span>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-danger">{{ __('msg.gdpr_purge_customer_button') }}</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ __('close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
|
@ -337,7 +337,7 @@ Route::domain(config('app.pre_url_crm').config('app.domain').config('app.tld_car
|
|||
// customers
|
||||
Route::get('/admin/customers', 'CustomerController@index')->name('admin_customers');
|
||||
Route::get('/admin/customer/detail/{id}', 'CustomerController@detail')->name('admin_customer_detail');
|
||||
Route::get('/admin/customer/delete/{id}', 'CustomerController@delete')->name('admin_customer_delete');
|
||||
Route::post('/admin/customer/delete/{id}', 'CustomerController@delete')->name('admin_customer_delete');
|
||||
Route::post('/admin/customer/gdpr-delete/{id}', 'CustomerController@gdprDelete')->name('admin_customer_gdpr_delete');
|
||||
Route::get('/admin/customer/edit/{id}', 'CustomerController@edit')->name('admin_customer_edit');
|
||||
Route::post('/admin/customer/edit/{id}', 'CustomerController@store')->name('admin_customer_edit');
|
||||
|
|
|
|||
282
tests/Feature/CustomerDeleteTest.php
Normal file
282
tests/Feature/CustomerDeleteTest.php
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
<?php
|
||||
|
||||
use App\Exceptions\GdprDeletionException;
|
||||
use App\Models\Country;
|
||||
use App\Models\Customer;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserShop;
|
||||
use App\Services\GdprDeletionService;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
/**
|
||||
* Ticket dev/_tickets/2026-09-08.md #2: Kunden löschen (komplett vs. DSGVO),
|
||||
* Mehrfach-Anlagen werden über die E-Mail-Adresse zusammengefasst.
|
||||
*
|
||||
* Die Admin-Routen werden host-abhängig geladen, daher App-Neustart mit CRM-Host.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
$this->previousHttpHost = $_SERVER['HTTP_HOST'] ?? null;
|
||||
Mail::fake();
|
||||
$_SERVER['HTTP_HOST'] = config('domains.domains.crm.host');
|
||||
$this->refreshApplication();
|
||||
$this->artisan('migrate');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Host wieder zurücksetzen, sonst booten nachfolgende Tests mit den CRM-Routen.
|
||||
if ($this->previousHttpHost === null) {
|
||||
unset($_SERVER['HTTP_HOST']);
|
||||
} else {
|
||||
$_SERVER['HTTP_HOST'] = $this->previousHttpHost;
|
||||
}
|
||||
});
|
||||
|
||||
function deleteTestUser(array $attributes = []): User
|
||||
{
|
||||
return User::forceCreate(array_merge([
|
||||
'email' => 'user-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'active' => 1,
|
||||
'wizard' => 100,
|
||||
'payment_account' => now()->addYear(),
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
function deleteTestCustomer(User $member, string $email, array $attributes = []): ShoppingUser
|
||||
{
|
||||
$country = Country::first() ?? Country::create([
|
||||
'code' => 'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland',
|
||||
'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
|
||||
return ShoppingUser::create(array_merge([
|
||||
'auth_user_id' => 0,
|
||||
'member_id' => $member->id,
|
||||
'number' => random_int(1000, 99999),
|
||||
'billing_firstname' => 'Erika',
|
||||
'billing_lastname' => 'Muster',
|
||||
'billing_address' => 'Musterweg 1',
|
||||
'billing_zipcode' => '12345',
|
||||
'billing_city' => 'Musterstadt',
|
||||
'billing_phone' => '0123',
|
||||
'billing_email' => $email,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
function deleteTestOrder(ShoppingUser $customer): ShoppingOrder
|
||||
{
|
||||
$shop = UserShop::first() ?? UserShop::create(['user_id' => $customer->member_id, 'name' => 'Shop', 'slug' => 'shop-'.uniqid()]);
|
||||
|
||||
return ShoppingOrder::create([
|
||||
'shopping_user_id' => $customer->id,
|
||||
'auth_user_id' => 0,
|
||||
'member_id' => $customer->member_id,
|
||||
'user_shop_id' => $shop->id,
|
||||
'country_id' => $customer->billing_country_id,
|
||||
'total' => 10,
|
||||
'txaction' => 'appointed',
|
||||
'mode' => 'test',
|
||||
]);
|
||||
}
|
||||
|
||||
it('fasst alle Einträge eines Kunden über Kundennummer und E-Mail zusammen', function () {
|
||||
$member = deleteTestUser();
|
||||
$first = deleteTestCustomer($member, 'erika@example.com', ['number' => 61009]);
|
||||
$sameNumber = deleteTestCustomer($member, 'anders@example.com', ['number' => 61009]);
|
||||
$sameEmailOtherNumber = deleteTestCustomer($member, 'Erika@Example.com', ['number' => 61010]);
|
||||
$consultantRow = deleteTestCustomer($member, 'erika@example.com', ['number' => 99, 'auth_user_id' => $member->id]);
|
||||
$other = deleteTestCustomer($member, 'jemand@example.com', ['number' => 61011]);
|
||||
|
||||
$ids = app(GdprDeletionService::class)->getCustomerRows($first)->pluck('id')->all();
|
||||
|
||||
expect($ids)->toBe([$first->id, $sameNumber->id, $sameEmailOtherNumber->id])
|
||||
->and($ids)->not->toContain($consultantRow->id)
|
||||
->and($ids)->not->toContain($other->id);
|
||||
});
|
||||
|
||||
it('gruppiert nicht über Platzhalter-Adressen', function () {
|
||||
$member = deleteTestUser();
|
||||
$faker = deleteTestCustomer($member, 'abc-faker@mivita.care', ['number' => 1, 'faker_mail' => true]);
|
||||
$otherFaker = deleteTestCustomer($member, 'abc-faker@mivita.care', ['number' => 2, 'faker_mail' => true]);
|
||||
|
||||
$service = app(GdprDeletionService::class);
|
||||
|
||||
expect($service->groupingEmail($faker))->toBeNull()
|
||||
->and($service->getCustomerRows($faker)->pluck('id')->all())->toBe([$faker->id])
|
||||
->and($service->getCustomerRows($otherFaker)->pluck('id')->all())->toBe([$otherFaker->id]);
|
||||
});
|
||||
|
||||
it('löscht einen Kunden ohne Bestellungen komplett, inklusive Mehrfach-Anlagen, Portal-Login und Abo-Daten', function () {
|
||||
$member = deleteTestUser();
|
||||
$first = deleteTestCustomer($member, 'erika@example.com', ['number' => 61009]);
|
||||
$duplicate = deleteTestCustomer($member, 'erika@example.com', ['number' => 61010]);
|
||||
$abo = UserAbo::create([
|
||||
'user_id' => null,
|
||||
'member_id' => $member->id,
|
||||
'shopping_user_id' => $duplicate->id,
|
||||
'is_for' => 'ot',
|
||||
'email' => 'erika@example.com',
|
||||
'payone_userid' => 4711,
|
||||
'clearingtype' => 'cc',
|
||||
'carddata' => ['last4' => '4242'],
|
||||
'active' => false,
|
||||
'status' => 4,
|
||||
'abo_interval' => 1,
|
||||
]);
|
||||
$portal = Customer::create(['email' => 'erika@example.com', 'shopping_user_id' => $first->id, 'member_id' => $member->id, 'number' => 61009]);
|
||||
|
||||
$summary = app(GdprDeletionService::class)->purgeShoppingUser($first, $member->id);
|
||||
|
||||
expect($summary['shopping_users_purged'])->toBe(2)
|
||||
->and($summary['portal_customers_deleted'])->toBe(1)
|
||||
->and($summary['abos_anonymized'])->toBe(1);
|
||||
|
||||
foreach ([$first->id, $duplicate->id] as $id) {
|
||||
expect(ShoppingUser::find($id))->toBeNull();
|
||||
$row = ShoppingUser::withTrashed()->find($id);
|
||||
expect($row->deleted_at)->not->toBeNull()
|
||||
->and($row->billing_firstname)->toBe(GdprDeletionService::ANONYMIZED_FIRSTNAME)
|
||||
->and($row->billing_lastname)->toBe(GdprDeletionService::ANONYMIZED_LASTNAME)
|
||||
->and($row->billing_address)->toBeNull()
|
||||
->and($row->billing_phone)->toBeNull()
|
||||
->and($row->billing_email)->not->toContain('erika');
|
||||
}
|
||||
|
||||
expect(Customer::find($portal->id))->toBeNull();
|
||||
$abo = UserAbo::withTrashed()->find($abo->id);
|
||||
expect($abo->email)->toBeNull()
|
||||
->and($abo->carddata)->toBeNull()
|
||||
->and((int) $abo->payone_userid)->toBe(0);
|
||||
});
|
||||
|
||||
it('verweigert die Komplett-Löschung, sobald eine Mehrfach-Anlage eine Bestellung hat', function () {
|
||||
$member = deleteTestUser();
|
||||
$first = deleteTestCustomer($member, 'erika@example.com', ['number' => 61009]);
|
||||
$duplicate = deleteTestCustomer($member, 'erika@example.com', ['number' => 61010]);
|
||||
deleteTestOrder($duplicate);
|
||||
|
||||
expect(fn () => app(GdprDeletionService::class)->purgeShoppingUser($first, $member->id))
|
||||
->toThrow(GdprDeletionException::class, __('msg.gdpr_error_customer_has_orders'));
|
||||
|
||||
expect($first->fresh()->billing_email)->toBe('erika@example.com')
|
||||
->and($duplicate->fresh()->deleted_at)->toBeNull();
|
||||
});
|
||||
|
||||
it('verweigert die Komplett-Löschung bei Berater-Datensätzen und aktiven Abos', function () {
|
||||
$member = deleteTestUser();
|
||||
$consultantRow = deleteTestCustomer($member, 'berater@example.com', ['auth_user_id' => $member->id]);
|
||||
|
||||
expect(fn () => app(GdprDeletionService::class)->purgeShoppingUser($consultantRow, $member->id))
|
||||
->toThrow(GdprDeletionException::class, __('msg.gdpr_error_customer_is_member'));
|
||||
|
||||
$customer = deleteTestCustomer($member, 'abo@example.com');
|
||||
UserAbo::create([
|
||||
'user_id' => null,
|
||||
'member_id' => $member->id,
|
||||
'shopping_user_id' => $customer->id,
|
||||
'is_for' => 'ot',
|
||||
'email' => 'abo@example.com',
|
||||
'payone_userid' => 1,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => true,
|
||||
'status' => 2,
|
||||
'abo_interval' => 1,
|
||||
]);
|
||||
|
||||
expect(fn () => app(GdprDeletionService::class)->purgeShoppingUser($customer, $member->id))
|
||||
->toThrow(GdprDeletionException::class, __('msg.gdpr_error_active_abo'));
|
||||
});
|
||||
|
||||
it('anonymisiert bei der DSGVO-Löschung auch Mehrfach-Anlagen mit anderer Kundennummer und entfernt den Portal-Login', function () {
|
||||
$member = deleteTestUser();
|
||||
$first = deleteTestCustomer($member, 'erika@example.com', ['number' => 61009]);
|
||||
deleteTestOrder($first);
|
||||
$duplicate = deleteTestCustomer($member, 'erika@example.com', ['number' => 61010]);
|
||||
Customer::create(['email' => 'erika@example.com', 'shopping_user_id' => $duplicate->id, 'member_id' => $member->id]);
|
||||
|
||||
$summary = app(GdprDeletionService::class)->deleteShoppingUser($first, $member->id);
|
||||
|
||||
expect($summary['shopping_users_anonymized'])->toBe(2)
|
||||
->and($summary['portal_customers_deleted'])->toBe(1);
|
||||
|
||||
$first = $first->fresh();
|
||||
expect($first->billing_firstname)->toBe('Erika')
|
||||
->and($first->billing_email)->toBe('geloescht_'.$first->id.'@dsgvo.invalid');
|
||||
|
||||
$duplicate = $duplicate->fresh();
|
||||
expect($duplicate->billing_firstname)->toBe(GdprDeletionService::ANONYMIZED_FIRSTNAME)
|
||||
->and($duplicate->billing_email)->not->toContain('erika');
|
||||
|
||||
expect(Customer::where('email', 'erika@example.com')->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('bietet im Modal die Komplett-Löschung an, wenn keine Bestellung vorliegt, und führt sie nach Bestätigung aus', function () {
|
||||
$admin = deleteTestUser(['admin' => 2]);
|
||||
$member = deleteTestUser();
|
||||
$first = deleteTestCustomer($member, 'erika@example.com', ['number' => 61009]);
|
||||
$duplicate = deleteTestCustomer($member, 'erika@example.com', ['number' => 61010]);
|
||||
|
||||
$this->actingAs($admin)->get(route('admin_customer_detail', $first->id))
|
||||
->assertOk()
|
||||
->assertSee('id="modals-customer-delete"', false)
|
||||
->assertSee('2-mal')
|
||||
->assertSee(__('msg.gdpr_purge_customer_button'))
|
||||
->assertDontSee(__('msg.gdpr_delete_customer_button'));
|
||||
|
||||
$this->actingAs($admin)->post(route('admin_customer_delete', $first->id))
|
||||
->assertRedirect(route('admin_customer_detail', $first->id))
|
||||
->assertSessionHas('alert-error', __('msg.delete_confirm_checkbox_missing'));
|
||||
expect($first->fresh())->not->toBeNull();
|
||||
|
||||
$this->actingAs($admin)->post(route('admin_customer_delete', $first->id), ['delete_customer' => 1])
|
||||
->assertRedirect(route('admin_customers'))
|
||||
->assertSessionHas('alert-success');
|
||||
expect(ShoppingUser::find($first->id))->toBeNull()
|
||||
->and(ShoppingUser::find($duplicate->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('bietet im Modal nur die DSGVO-Löschung an, wenn eine Bestellung vorliegt', function () {
|
||||
$superadmin = deleteTestUser(['admin' => 3]);
|
||||
$member = deleteTestUser();
|
||||
$first = deleteTestCustomer($member, 'erika@example.com', ['number' => 61009]);
|
||||
$duplicate = deleteTestCustomer($member, 'erika@example.com', ['number' => 61010]);
|
||||
deleteTestOrder($duplicate);
|
||||
|
||||
$this->actingAs($superadmin)->get(route('admin_customer_detail', $first->id))
|
||||
->assertOk()
|
||||
->assertSee('Es ist nur')
|
||||
->assertSee(__('msg.gdpr_delete_customer_button'))
|
||||
->assertDontSee(__('msg.gdpr_purge_customer_button'));
|
||||
|
||||
$this->actingAs($superadmin)->post(route('admin_customer_delete', $first->id), ['delete_customer' => 1])
|
||||
->assertRedirect(route('admin_customer_detail', $first->id))
|
||||
->assertSessionHas('alert-error', __('msg.gdpr_error_customer_has_orders'));
|
||||
expect($first->fresh()->billing_email)->toBe('erika@example.com');
|
||||
|
||||
$this->actingAs($superadmin)->post(route('admin_customer_gdpr_delete', $first->id), ['gdpr_delete_customer' => 1])
|
||||
->assertRedirect(route('admin_customers'))
|
||||
->assertSessionHas('alert-success');
|
||||
expect($duplicate->fresh()->billing_email)->toBe('geloescht_'.$duplicate->id.'@dsgvo.invalid')
|
||||
->and($first->fresh()->billing_firstname)->toBe(GdprDeletionService::ANONYMIZED_FIRSTNAME);
|
||||
});
|
||||
|
||||
it('zeigt normalen Admins bei Bestellungen den Hinweis auf den Superadmin', function () {
|
||||
$admin = deleteTestUser(['admin' => 2]);
|
||||
$member = deleteTestUser();
|
||||
$customer = deleteTestCustomer($member, 'erika@example.com');
|
||||
deleteTestOrder($customer);
|
||||
|
||||
$this->actingAs($admin)->get(route('admin_customer_detail', $customer->id))
|
||||
->assertOk()
|
||||
->assertSee('nur von einem Superadmin')
|
||||
->assertDontSee('id="form-customer-gdpr-delete"', false);
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@ 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;
|
||||
|
|
@ -369,3 +370,52 @@ it('entfernt Zahlungs-Tokens der Abos eines Endkunden', function () {
|
|||
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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue