Files
timebank-cc-public/app/Console/Commands/ProcessInactiveProfiles.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

312 lines
13 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Models\Organization;
use App\Mail\InactiveProfileWarning1Mail;
use App\Mail\InactiveProfileWarning2Mail;
use App\Mail\InactiveProfileWarningFinalMail;
use App\Mail\UserDeletedMail;
use App\Actions\Jetstream\DeleteUser;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;
class ProcessInactiveProfiles extends Command
{
protected $signature = 'profiles:process-inactive';
protected $description = 'Process inactive profiles - send warnings and delete profiles that exceed inactivity thresholds';
protected $thresholds = [];
protected $logFile;
public function __construct()
{
parent::__construct();
// Convert configured days to seconds for precise comparison
$this->thresholds = [
'warning_1' => timebank_config('delete_profile.days_after_inactive.warning_1') * 86400,
'warning_2' => timebank_config('delete_profile.days_after_inactive.warning_2') * 86400,
'warning_final' => timebank_config('delete_profile.days_after_inactive.warning_final') * 86400,
'run_delete' => timebank_config('delete_profile.days_after_inactive.run_delete') * 86400,
];
$this->logFile = storage_path('logs/inactive-profiles.log');
}
public function handle()
{
$this->info('Processing inactive profiles...');
$this->logMessage('=== Starting inactive profile processing ===');
$totalWarnings = 0;
$totalDeletions = 0;
// Process Users
$users = User::whereNotNull('inactive_at') // Only process profiles marked as inactive
->whereNull('deleted_at') // Exclude already deleted profiles
->get();
foreach ($users as $user) {
$result = $this->processProfile($user, 'User');
if ($result === 'warning') $totalWarnings++;
if ($result === 'deleted') $totalDeletions++;
}
// Process Organizations
$organizations = Organization::whereNotNull('inactive_at') // Only process profiles marked as inactive
->whereNull('deleted_at') // Exclude already deleted profiles
->get();
foreach ($organizations as $organization) {
$result = $this->processProfile($organization, 'Organization');
if ($result === 'warning') $totalWarnings++;
if ($result === 'deleted') $totalDeletions++;
}
$this->info("Processing complete: {$totalWarnings} warnings sent, {$totalDeletions} profiles deleted");
$this->logMessage("=== Completed: {$totalWarnings} warnings, {$totalDeletions} deletions ===\n");
return 0;
}
protected function processProfile($profile, $profileType)
{
if (!$profile->inactive_at) {
return null;
}
$secondsSinceInactive = now()->diffInSeconds($profile->inactive_at);
$secondsRemaining = $this->thresholds['run_delete'] - $secondsSinceInactive;
// Determine action based on thresholds
if ($secondsSinceInactive >= $this->thresholds['run_delete']) {
// Delete profile
return $this->deleteProfile($profile, $profileType, $secondsSinceInactive);
} elseif ($secondsSinceInactive >= $this->thresholds['warning_final'] && $secondsSinceInactive < $this->thresholds['run_delete']) {
// Send final warning
return $this->sendWarning($profile, $profileType, 'final', $secondsRemaining, $secondsSinceInactive);
} elseif ($secondsSinceInactive >= $this->thresholds['warning_2'] && $secondsSinceInactive < $this->thresholds['warning_final']) {
// Send warning 2
return $this->sendWarning($profile, $profileType, 'warning_2', $secondsRemaining, $secondsSinceInactive);
} elseif ($secondsSinceInactive >= $this->thresholds['warning_1'] && $secondsSinceInactive < $this->thresholds['warning_2']) {
// Send warning 1
return $this->sendWarning($profile, $profileType, 'warning_1', $secondsRemaining, $secondsSinceInactive);
}
return null;
}
protected function sendWarning($profile, $profileType, $warningLevel, $secondsRemaining, $secondsSinceInactive)
{
$accountsData = $this->getAccountsData($profile);
$totalBalance = $this->getTotalBalance($profile);
$timeRemaining = $this->formatTimeRemaining($secondsRemaining);
$daysSinceInactive = round($secondsSinceInactive / 86400, 2);
$mailClass = match($warningLevel) {
'warning_1' => InactiveProfileWarning1Mail::class,
'warning_2' => InactiveProfileWarning2Mail::class,
'final' => InactiveProfileWarningFinalMail::class,
};
// Get recipients
$recipients = $this->getRecipients($profile, $profileType);
// Send email to all recipients
foreach ($recipients as $recipient) {
Mail::to($recipient['email'])
->queue(new $mailClass(
$profile,
$profileType,
$timeRemaining,
$secondsRemaining / 86400, // days remaining
$accountsData,
$totalBalance,
$daysSinceInactive
));
}
$this->logMessage("[{$profileType}] {$warningLevel} sent to {$profile->name} (ID: {$profile->id}) - Inactive for {$daysSinceInactive} days, {$timeRemaining} remaining");
$this->info("[{$profileType}] {$warningLevel}: {$profile->name} ({$timeRemaining} remaining)");
return 'warning';
}
protected function deleteProfile($profile, $profileType, $secondsSinceInactive)
{
$daysSinceInactive = round($secondsSinceInactive / 86400, 2);
try {
// Check for negative balances
$accountsData = $this->getAccountsData($profile);
foreach ($accountsData as $account) {
if ($account['balance'] < 0) {
$this->logMessage("[{$profileType}] SKIPPED deletion of {$profile->name} (ID: {$profile->id}) - Has negative balance");
$this->warn("[{$profileType}] Skipped: {$profile->name} - negative balance");
return null;
}
}
// Store profile data before deletion (needed for email)
$totalBalance = $this->getTotalBalance($profile);
$profileEmail = $profile->email;
$profileName = $profile->name;
$profileFullName = $profile->full_name ?? $profile->name;
// Get the profile's updated_at timestamp
$profileTable = $profile->getTable();
$time = DB::table($profileTable)
->where('id', $profile->id)
->pluck('updated_at')
->first();
$time = Carbon::parse($time);
// Execute soft deletion (sets deleted_at, handles balances, but doesn't anonymize)
// Balance handling: skip donation option, use config elsif logic
$deleteUser = new DeleteUser();
$result = $deleteUser->delete($profile, 'delete', null, true); // true = isAutoDeleted
// Check if soft deletion was successful
if ($result['status'] === 'success') {
// Get auto-delete and grace period configuration
$daysNotLoggedIn = timebank_config('profile_inactive.days_not_logged_in');
$daysAfterInactive = timebank_config('delete_profile.days_after_inactive.run_delete');
$totalDays = $daysNotLoggedIn + $daysAfterInactive;
$gracePeriodDays = timebank_config('delete_profile.grace_period_days', 30);
// Prepare email data (similar to DeleteUserForm.php)
$emailData = [
'time' => $time->translatedFormat('j F Y, H:i'),
'deletedUser' => (object)[
'name' => $profileName,
'full_name' => $profileFullName,
'lang_preference' => $profile->lang_preference ?? config('app.locale', 'en'),
],
'mail' => $profileEmail,
'balanceHandlingOption' => 'delete', // Auto-delete always uses 'delete' option
'totalBalance' => $totalBalance,
'donationAccountId' => null,
'donationAccountName' => null,
'donationOrganizationName' => null,
'autoDeleted' => true, // Flag to indicate this was an auto-deletion
'daysNotLoggedIn' => $daysNotLoggedIn,
'daysAfterInactive' => $daysAfterInactive,
'totalDaysToDelete' => $totalDays,
'gracePeriodDays' => $gracePeriodDays, // Days to restore profile
];
// Send deletion confirmation email
Mail::to($profileEmail)->queue(new UserDeletedMail($emailData));
$this->logMessage("[{$profileType}] SOFT DELETED {$profileName} (ID: {$profile->id}) - Inactive for {$daysSinceInactive} days - Can be restored within {$gracePeriodDays} days - Email sent to {$profileEmail}");
$this->info("[{$profileType}] Soft deleted: {$profileName} (restorable for {$gracePeriodDays} days)");
return 'deleted';
} else {
$this->logMessage("[{$profileType}] ERROR deleting {$profileName} (ID: {$profile->id}): {$result['message']}");
$this->error("[{$profileType}] Error deleting {$profileName}: {$result['message']}");
return null;
}
} catch (\Exception $e) {
$this->logMessage("[{$profileType}] ERROR deleting {$profile->name} (ID: {$profile->id}): {$e->getMessage()}");
$this->error("[{$profileType}] Error deleting {$profile->name}: {$e->getMessage()}");
return null;
}
}
protected function getRecipients($profile, $profileType)
{
$recipients = [];
if ($profileType === 'User') {
$recipients[] = [
'email' => $profile->email,
'name' => $profile->name,
];
} elseif ($profileType === 'Organization') {
// Add organization email
$recipients[] = [
'email' => $profile->email,
'name' => $profile->name,
];
// Add all manager emails
$managers = $profile->managers()->get();
foreach ($managers as $manager) {
$recipients[] = [
'email' => $manager->email,
'name' => $manager->name,
];
}
}
return $recipients;
}
protected function getAccountsData($profile)
{
$accounts = [];
$profileAccounts = $profile->accounts()->active()->notRemoved()->get();
foreach ($profileAccounts as $account) {
// Clear cache to get fresh balance
\Cache::forget("account_balance_{$account->id}");
$accounts[] = [
'id' => $account->id,
'name' => $account->name,
'balance' => $account->balance, // in minutes
'balanceFormatted' => tbFormat($account->balance),
];
}
return $accounts;
}
protected function getTotalBalance($profile)
{
$total = 0;
$accountsData = $this->getAccountsData($profile);
foreach ($accountsData as $account) {
$total += $account['balance'];
}
return $total;
}
protected function formatTimeRemaining($seconds)
{
$days = $seconds / 86400;
if ($days >= 7) {
$weeks = round($days / 7);
return trans_choice('weeks_remaining', $weeks, ['count' => $weeks]);
} elseif ($days >= 1) {
$daysRounded = round($days);
return trans_choice('days_remaining', $daysRounded, ['count' => $daysRounded]);
} elseif ($seconds >= 3600) {
$hours = round($seconds / 3600);
return trans_choice('hours_remaining', $hours, ['count' => $hours]);
} else {
$minutes = max(1, round($seconds / 60));
return trans_choice('minutes_remaining', $minutes, ['count' => $minutes]);
}
}
protected function logMessage($message)
{
$timestamp = now()->format('Y-m-d H:i:s');
$logEntry = "[{$timestamp}] {$message}\n";
file_put_contents($this->logFile, $logEntry, FILE_APPEND);
Log::info($message);
}
}