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); } }