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
50
app/Console/Commands/GdprRepairCustomers.php
Normal file
50
app/Console/Commands/GdprRepairCustomers.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\GdprDeletionService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class GdprRepairCustomers extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'gdpr:repair-customers
|
||||
{--execute : Änderungen wirklich ausführen (ohne Option nur Vorschau)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Zieht protokollierte DSGVO-Löschungen von Endkunden nach: Einträge, deren Kontaktdaten nach der Löschung wieder befüllt wurden, werden erneut anonymisiert und gesperrt.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(GdprDeletionService $gdprDeletionService): int
|
||||
{
|
||||
$execute = (bool) $this->option('execute');
|
||||
$rows = $gdprDeletionService->repairLoggedShoppingUserDeletions(! $execute);
|
||||
|
||||
if ($rows === []) {
|
||||
$this->info('Keine Einträge zu reparieren.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->table(['ShoppingUser-ID', 'Kd-Nr.', 'Löschung protokolliert am'], array_map(
|
||||
fn (array $row) => [$row['id'], $row['number'] ?: '-', $row['deletion_logged_at']],
|
||||
$rows
|
||||
));
|
||||
|
||||
$this->info($execute
|
||||
? count($rows).' Eintrag/Einträge erneut anonymisiert und gesperrt.'
|
||||
: count($rows).' Eintrag/Einträge betroffen. Vorschau – mit --execute ausführen.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\GdprDeletionException;
|
||||
use App\Services\ShoppingUserService;
|
||||
use App\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
|
@ -122,6 +124,7 @@ use Illuminate\Support\Carbon;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\ShoppingUser whereMode($value)
|
||||
*
|
||||
* @property bool|null $faker_mail
|
||||
* @property Carbon|null $gdpr_deleted_at
|
||||
* @property string|null $shipping_email
|
||||
* @property string|null $is_for
|
||||
* @property string|null $is_from
|
||||
|
|
@ -215,8 +218,57 @@ class ShoppingUser extends Model
|
|||
'has_buyed' => 'bool',
|
||||
'subscribed' => 'bool',
|
||||
'wp_order_number' => 'int',
|
||||
'gdpr_deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Kontaktfelder, die nach einer DSGVO-Löschung nicht wieder befüllt werden dürfen.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public const GDPR_LOCKED_CONTACT_FIELDS = ['billing_email', 'billing_phone', 'shipping_email', 'shipping_phone'];
|
||||
|
||||
/**
|
||||
* Schutz gegen das Zurückschreiben von Kontaktdaten in DSGVO-gelöschte Einträge,
|
||||
* z. B. durch ein noch offenes Bearbeiten-Formular (dev/_tickets/2026-10-06).
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updating(function (ShoppingUser $shoppingUser) {
|
||||
if ($shoppingUser->getOriginal('gdpr_deleted_at') === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (self::GDPR_LOCKED_CONTACT_FIELDS as $field) {
|
||||
if ($shoppingUser->isDirty($field) && ! self::isGdprPlaceholderValue($field, $shoppingUser->{$field})) {
|
||||
throw new GdprDeletionException(__('msg.gdpr_customer_locked'));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static function isGdprPlaceholderValue(string $field, mixed $value): bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $field === 'billing_email' && str_ends_with((string) $value, '@dsgvo.invalid');
|
||||
}
|
||||
|
||||
public function isGdprDeleted(): bool
|
||||
{
|
||||
return $this->gdpr_deleted_at !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<ShoppingUser> $query
|
||||
*/
|
||||
public function scopeWithoutGdprDeleted($query): void
|
||||
{
|
||||
$query->whereNull($query->qualifyColumn('gdpr_deleted_at'));
|
||||
}
|
||||
|
||||
// can null
|
||||
public function member()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,78 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\User;
|
||||
use Yard;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ShoppingUserService
|
||||
{
|
||||
|
||||
/**
|
||||
* Gibt alle Bestellungen eines Mitglieds zurück
|
||||
*
|
||||
* @param ShoppingUser $shopping_user Das Mitglied, dessen Bestellungen zurückgegeben werden sollen
|
||||
*
|
||||
* @param ShoppingUser $shopping_user Das Mitglied, dessen Bestellungen zurückgegeben werden sollen
|
||||
* @return Collection Die Bestellungen des Mitglieds
|
||||
*/
|
||||
public static function getAllOrdersByMember($shopping_user){
|
||||
public static function getAllOrdersByMember($shopping_user)
|
||||
{
|
||||
$users = ShoppingUser::where('billing_email', '=', $shopping_user->billing_email)->where('member_id', '=', $shopping_user->member_id)->get();
|
||||
return $users->flatMap(function($user) {
|
||||
|
||||
return $users->flatMap(function ($user) {
|
||||
return $user->shopping_orders;
|
||||
})->sortByDesc('created_at');
|
||||
})->sortByDesc('created_at');
|
||||
}
|
||||
|
||||
public static function syncOrdersByEmail($user)
|
||||
{
|
||||
$maxAttempts = 3;
|
||||
$attempt = 1;
|
||||
|
||||
|
||||
while ($attempt <= $maxAttempts) {
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
|
||||
$shopping_user = ShoppingUser::where('member_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$shopping_user) {
|
||||
if (! $shopping_user) {
|
||||
DB::commit();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$shopping_users = ShoppingUser::where('billing_email', '=', $shopping_user->billing_email)
|
||||
->whereHas('shopping_order', function($q) {
|
||||
->whereHas('shopping_order', function ($q) {
|
||||
$q->where('txaction', 'paid')
|
||||
->orWhere('txaction', 'appointed')
|
||||
->orWhere('txaction', 'extern')
|
||||
->orWhere('txaction', 'invoice_open')
|
||||
->orWhere('txaction', 'invoice_paid');
|
||||
->orWhere('txaction', 'appointed')
|
||||
->orWhere('txaction', 'extern')
|
||||
->orWhere('txaction', 'invoice_open')
|
||||
->orWhere('txaction', 'invoice_paid');
|
||||
})
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
|
||||
$order_count = $shopping_users->count();
|
||||
|
||||
foreach($shopping_users as $user) {
|
||||
foreach ($shopping_users as $user) {
|
||||
$user->orders = $order_count;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return; // Erfolgreicher Fall
|
||||
|
||||
} catch (QueryException $e) {
|
||||
DB::rollBack();
|
||||
|
||||
|
||||
// Wenn es ein Deadlock ist, versuchen wir es erneut
|
||||
if ($e->getCode() == 40001 && $attempt < $maxAttempts) {
|
||||
$sleepTime = pow(2, $attempt) * 100000; // Exponentielles Backoff in Mikrosekunden
|
||||
usleep($sleepTime);
|
||||
$attempt++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
throw $e; // Andere Fehler oder zu viele Versuche
|
||||
}
|
||||
}
|
||||
|
|
@ -91,52 +95,55 @@ class ShoppingUserService
|
|||
}*/
|
||||
/**
|
||||
* Synchronisiert die Bestellungen eines ShoppingOrders mit den Bestellungen des zugehörigen ShoppingUsers
|
||||
*
|
||||
* @param ShoppingOrder $shopping_order Der zu synchronisierende ShoppingOrder
|
||||
*/
|
||||
public static function snycOrdersByShoppingOrder(ShoppingOrder $shopping_order) {
|
||||
*
|
||||
* @param ShoppingOrder $shopping_order Der zu synchronisierende ShoppingOrder
|
||||
*/
|
||||
public static function snycOrdersByShoppingOrder(ShoppingOrder $shopping_order)
|
||||
{
|
||||
$shopping_user = $shopping_order->shopping_user ? $shopping_order->shopping_user : null;
|
||||
if($shopping_user){
|
||||
if ($shopping_user) {
|
||||
self::snycOrdersByShoppingUser($shopping_user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronisiert die Bestellungen eines ShoppingUsers mit den Bestellungen des zugehörigen Benutzers
|
||||
*
|
||||
* @param ShoppingUser $shopping_user Der zu synchronisierende ShoppingUser
|
||||
*/
|
||||
public static function snycOrdersByShoppingUser(ShoppingUser $shopping_user) {
|
||||
*
|
||||
* @param ShoppingUser $shopping_user Der zu synchronisierende ShoppingUser
|
||||
*/
|
||||
public static function snycOrdersByShoppingUser(ShoppingUser $shopping_user)
|
||||
{
|
||||
|
||||
// Aktualisiere alle Benutzer mit einer einzigen Datenbankabfrage
|
||||
ShoppingUser::where('billing_email', $shopping_user->billing_email)
|
||||
->update([
|
||||
'orders' => ShoppingUser::where('billing_email', $shopping_user->billing_email)
|
||||
->whereHas('shopping_order', function($q) {
|
||||
$q->whereIn('txaction', ['paid', 'appointed', 'extern', 'invoice_open', 'invoice_paid']);
|
||||
})
|
||||
->count()
|
||||
]);
|
||||
->update([
|
||||
'orders' => ShoppingUser::where('billing_email', $shopping_user->billing_email)
|
||||
->whereHas('shopping_order', function ($q) {
|
||||
$q->whereIn('txaction', ['paid', 'appointed', 'extern', 'invoice_open', 'invoice_paid']);
|
||||
})
|
||||
->count(),
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public static function snycOrdersByNumber($number){
|
||||
if($number <= 0) {
|
||||
|
||||
public static function snycOrdersByNumber($number)
|
||||
{
|
||||
if ($number <= 0) {
|
||||
return;
|
||||
}
|
||||
// Aktualisiere alle Benutzer mit einer einzigen Datenbankabfrage
|
||||
|
||||
ShoppingUser::where('number', '=', $number)
|
||||
->update([
|
||||
'orders' => ShoppingUser::where('number', '=', $number)
|
||||
->whereHas('shopping_order', function($q) {
|
||||
$q->whereIn('txaction', ['paid', 'appointed', 'extern', 'invoice_open', 'invoice_paid']);
|
||||
})
|
||||
->count()
|
||||
]);
|
||||
->update([
|
||||
'orders' => ShoppingUser::where('number', '=', $number)
|
||||
->whereHas('shopping_order', function ($q) {
|
||||
$q->whereIn('txaction', ['paid', 'appointed', 'extern', 'invoice_open', 'invoice_paid']);
|
||||
})
|
||||
->count(),
|
||||
]);
|
||||
// Setze orders auf NULL für alle anderen
|
||||
ShoppingUser::where('number', '=', $number)
|
||||
->whereDoesntHave('shopping_order', function($q) {
|
||||
->whereDoesntHave('shopping_order', function ($q) {
|
||||
$q->whereIn('txaction', ['paid', 'appointed', 'extern', 'extern_paid']);
|
||||
})
|
||||
->update(['orders' => null]);
|
||||
|
|
@ -144,22 +151,23 @@ class ShoppingUserService
|
|||
|
||||
/**
|
||||
* Synchronisiert die Nummern aller ShoppingUser eines Benutzers anhand der E-Mails
|
||||
*
|
||||
* @param User $user Der Benutzer, dessen ShoppingUser synchronisiert werden sollen
|
||||
*/
|
||||
public static function syncNumbersByEmail($user) {
|
||||
*
|
||||
* @param User $user Der Benutzer, dessen ShoppingUser synchronisiert werden sollen
|
||||
*/
|
||||
public static function syncNumbersByEmail($user)
|
||||
{
|
||||
// Hole alle ShoppingUser mit E-Mail gruppiert
|
||||
$emailGroups = ShoppingUser::where('member_id', $user->id)
|
||||
->whereNotNull('billing_email')
|
||||
->where('billing_email', '!=', '')
|
||||
->get()
|
||||
->groupBy('billing_email');
|
||||
|
||||
foreach($emailGroups as $email => $users) {
|
||||
|
||||
foreach ($emailGroups as $email => $users) {
|
||||
// Suche ob es bereits eine Nummer für diese E-Mail gibt
|
||||
$existingNumber = ($users->whereNotNull('number')->first()) ? $users->whereNotNull('number')->first()->number : null;
|
||||
|
||||
if($existingNumber) {
|
||||
|
||||
if ($existingNumber) {
|
||||
// Wenn eine Nummer existiert, nutze diese für alle User mit der E-Mail
|
||||
ShoppingUser::where('member_id', $user->id)
|
||||
->where('billing_email', $email)
|
||||
|
|
@ -169,20 +177,21 @@ class ShoppingUserService
|
|||
// Wenn keine Nummer existiert, generiere eine neue
|
||||
$maxNumber = ShoppingUser::max('number') ?: 1000;
|
||||
$newNumber = $maxNumber + 1;
|
||||
|
||||
|
||||
ShoppingUser::where('member_id', $user->id)
|
||||
->where('billing_email', $email)
|
||||
->update(['number' => $newNumber]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setzt alle Faker-E-Mails für einen Benutzer auf 1
|
||||
*
|
||||
* @param User $user Der Benutzer, dessen Faker-E-Mails gesetzt werden sollen
|
||||
*/
|
||||
public static function setFakerMail($user){
|
||||
*
|
||||
* @param User $user Der Benutzer, dessen Faker-E-Mails gesetzt werden sollen
|
||||
*/
|
||||
public static function setFakerMail($user)
|
||||
{
|
||||
// Aktualisiere alle Faker-E-Mails für den Benutzer in einem einzigen Update
|
||||
// Schneller da nur ein einzelner SQL Query ausgeführt wird
|
||||
ShoppingUser::where('member_id', $user->id)
|
||||
|
|
@ -193,6 +202,7 @@ class ShoppingUserService
|
|||
// Setze alle anderen auf NULL zurück
|
||||
ShoppingUser::where('member_id', $user->id)
|
||||
->where('billing_email', 'NOT LIKE', '%faker@mivita.care')
|
||||
->whereNull('gdpr_deleted_at')
|
||||
->update(['faker_mail' => null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue