im profil kann ein nutzer nun seine daten exportieren. man kann seinen account löschen. nach 2 jahren werden inaktive accounts gelöscht, 1 monat vorher wird eine email geschickt. Hilfetexte und Legal Pages in der Guest PWA korrigiert und vom layout her optimiert (dark mode).
This commit is contained in:
109
app/Console/Commands/ProcessTenantRetention.php
Normal file
109
app/Console/Commands/ProcessTenantRetention.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\AnonymizeAccount;
|
||||
use App\Models\Tenant;
|
||||
use App\Notifications\InactiveTenantDeletionWarning;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
class ProcessTenantRetention extends Command
|
||||
{
|
||||
protected $signature = 'tenants:retention-scan';
|
||||
|
||||
protected $description = 'Scans tenants for Inaktivität, verschickt Warnungen und startet Anonymisierungen.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$warningThreshold = now()->subMonths(23);
|
||||
$deletionThreshold = now()->subMonths(24);
|
||||
|
||||
Tenant::query()
|
||||
->whereNull('anonymized_at')
|
||||
->with(['user'])
|
||||
->withMax('events as last_event_activity', 'updated_at')
|
||||
->withMax('purchases as last_purchase_activity', 'purchased_at')
|
||||
->withMax('photos as last_photo_activity', 'created_at')
|
||||
->chunkById(100, function ($tenants) use ($warningThreshold, $deletionThreshold) {
|
||||
foreach ($tenants as $tenant) {
|
||||
$lastActivity = $this->determineLastActivity($tenant);
|
||||
|
||||
if (! $lastActivity) {
|
||||
$lastActivity = $tenant->created_at ?? now();
|
||||
}
|
||||
|
||||
if ($this->hasActiveSubscription($tenant)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($lastActivity->lte($deletionThreshold)) {
|
||||
$this->dispatchAnonymization($tenant);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($lastActivity->lte($warningThreshold) && $tenant->deletion_warning_sent_at === null) {
|
||||
$this->sendWarning($tenant, $lastActivity->copy()->addMonths(24));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->info(__('profile.retention.scan_complete'));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
protected function determineLastActivity(Tenant $tenant): ?Carbon
|
||||
{
|
||||
$candidates = collect([
|
||||
$tenant->last_activity_at,
|
||||
$tenant->last_event_activity ? Carbon::parse($tenant->last_event_activity) : null,
|
||||
$tenant->last_purchase_activity ? Carbon::parse($tenant->last_purchase_activity) : null,
|
||||
$tenant->last_photo_activity ? Carbon::parse($tenant->last_photo_activity) : null,
|
||||
])->filter();
|
||||
|
||||
if ($candidates->isEmpty()) {
|
||||
return $tenant->created_at ? $tenant->created_at->copy() : null;
|
||||
}
|
||||
|
||||
return $candidates->sort()->last();
|
||||
}
|
||||
|
||||
protected function hasActiveSubscription(Tenant $tenant): bool
|
||||
{
|
||||
return $tenant->tenantPackages()
|
||||
->where('active', true)
|
||||
->whereHas('package', fn ($query) => $query->where('type', 'reseller'))
|
||||
->exists();
|
||||
}
|
||||
|
||||
protected function sendWarning(Tenant $tenant, Carbon $plannedDeletion): void
|
||||
{
|
||||
$email = $tenant->contact_email
|
||||
?? $tenant->email
|
||||
?? $tenant->user?->email;
|
||||
|
||||
if (! $email) {
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::route('mail', $email)
|
||||
->notify(new InactiveTenantDeletionWarning($tenant, $plannedDeletion));
|
||||
|
||||
$tenant->forceFill([
|
||||
'deletion_warning_sent_at' => now(),
|
||||
'pending_deletion_at' => $plannedDeletion,
|
||||
])->save();
|
||||
}
|
||||
|
||||
protected function dispatchAnonymization(Tenant $tenant): void
|
||||
{
|
||||
if ($tenant->anonymized_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
AnonymizeAccount::dispatch($tenant->user?->id, $tenant->id);
|
||||
}
|
||||
}
|
||||
41
app/Console/Commands/PurgeExpiredDataExports.php
Normal file
41
app/Console/Commands/PurgeExpiredDataExports.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\DataExport;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class PurgeExpiredDataExports extends Command
|
||||
{
|
||||
protected $signature = 'exports:purge';
|
||||
|
||||
protected $description = 'Delete abgelaufene Datenexporte und entfernen die entsprechenden Dateien.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$expired = DataExport::query()
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<', now())
|
||||
->get();
|
||||
|
||||
if ($expired->isEmpty()) {
|
||||
$this->info(__('profile.export.purge.none'));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($expired as $export) {
|
||||
if ($export->path && Storage::disk('local')->exists($export->path)) {
|
||||
Storage::disk('local')->delete($export->path);
|
||||
}
|
||||
$export->delete();
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->info(trans_choice('profile.export.purge.deleted', $count, ['count' => $count]));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user