Initial commit
This commit is contained in:
311
app/Console/Commands/ProcessInactiveProfiles.php
Normal file
311
app/Console/Commands/ProcessInactiveProfiles.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user