34 KiB
Profile Auto-Delete System Reference
This document provides comprehensive documentation on the profile auto-delete system, including configuration, commands, workflows, and testing procedures.
Table of Contents
- Overview
- System Architecture
- Configuration Structure
- Artisan Commands
- Workflow Process
- Scheduled Execution
- Testing
- Production Configuration
- Troubleshooting
- Related Systems
Overview
The profile auto-delete system automatically manages inactive profiles through a two-step process:
- Mark Inactive: Profiles that haven't logged in for a configured period are automatically marked as inactive
- Process Inactive: Inactive profiles receive progressive warning emails before final deletion
Purpose
- Maintain platform health by removing abandoned profiles
- Protect user privacy by deleting old personal data
- Send warning emails giving users a chance to prevent deletion
- Comply with data retention policies and GDPR requirements
- Keep the active user base current and engaged
Key Features
- Two-step process: Separate inactive marking from deletion
- Progressive warnings: 3 escalating warning emails before deletion
- Grace period: Deleted profiles can be restored within configurable timeframe (default: 30 days)
- Configurable thresholds: All timeframes configurable per platform
- Balance protection: Prevents deletion of profiles with negative balances
- Transaction anonymization: Past transactions preserved but anonymized
- Balance handling: Multiple options for account balance disposal
- Automatic scheduling: Runs daily via Laravel scheduler
- Comprehensive logging: All actions logged for audit trail
System Architecture
Two-Step Process
Step 1: Mark Inactive (Daily at 01:30)
┌─────────────────────────────────────────────────────────────┐
│ profiles:mark-inactive │
├─────────────────────────────────────────────────────────────┤
│ • Finds profiles where last_login_at > days_not_logged_in │
│ • Sets inactive_at = now() │
│ • Profile becomes hidden/labeled (per config) │
│ • Logs action to mark-inactive-profiles.log │
└─────────────────────────────────────────────────────────────┘
Step 2: Process Inactive (Daily at 02:00)
┌─────────────────────────────────────────────────────────────┐
│ profiles:process-inactive │
├─────────────────────────────────────────────────────────────┤
│ • Finds profiles where inactive_at is set │
│ • Calculates days since inactive_at │
│ • Sends appropriate warning based on thresholds: │
│ - Warning 1: X days after inactive │
│ - Warning 2: Y days after inactive │
│ - Final Warning: Z days after inactive │
│ - Delete: N days after inactive │
│ • Logs all actions to inactive-profiles.log │
└─────────────────────────────────────────────────────────────┘
Database Schema
Users/Organizations Table:
last_login_at(timestamp, nullable) - Last successful logininactive_at(timestamp, nullable) - When profile was marked inactivedeleted_at(timestamp, nullable) - Soft delete timestamp
Field States:
inactive_at = NULL→ Profile is activeinactive_at = [past date]→ Profile is inactive, awaiting deletiondeleted_at = [date]→ Profile is soft-deleted
Configuration Structure
Location
Configuration is defined in:
config/timebank-default.php(lines 1405-1420+)config/timebank_cc.php(lines 1405-1420+)- Active config loaded via
TIMEBANK_CONFIGenvironment variable
Complete Configuration
// Step 1: Mark profiles as inactive
'profile_inactive' => [
'days_not_logged_in' => 350, // Profile marked inactive after this many days
're-activate_at_login' => true, // Auto-reactivate when user logs in
'messenger_hidden' => true, // Hide from messenger search
'profile_search_hidden' => true, // Hide from main search
'profile_hidden' => true, // Hide profile page from non-admins
'profile_labeled' => true, // Show "Inactive" label
],
// Step 2: Process inactive profiles (send warnings and delete)
'delete_profile' => [
// NOTE: These are days AFTER the profile is marked inactive
// Total time = days_not_logged_in + days_after_inactive
'days_after_inactive' => [
'warning_1' => 7, // First warning
'warning_2' => 10, // Second warning
'warning_final' => 14, // Final warning
'run_delete' => 15, // Delete profile
],
'account_balances' => [
// Handle account balances on deletion (elsif logic):
'donate_balances_to_organization_account_specified' => true,
'transfer_balances_to_bank_client' => false,
'transfer_balances_to_account_id' => null,
'transfer_balances_to_debit_account' => true,
],
],
Configuration Timeline Example
With the configuration above:
- Day 0: User last logs in
- Day 350: Profile marked inactive (
profiles:mark-inactive) - Day 357: First warning email sent (350 + 7)
- Day 360: Second warning email sent (350 + 10)
- Day 364: Final warning email sent (350 + 14)
- Day 365: Profile deleted (350 + 15)
Total: 1 year from last login to deletion
Artisan Commands
1. Mark Inactive Profiles
Command: php artisan profiles:mark-inactive
Purpose: Automatically mark profiles as inactive based on login inactivity.
What It Does:
- Finds all User and Organization profiles where:
last_login_atis older than configureddays_not_logged_ininactive_atis NULL (not already marked inactive)
- Sets
inactive_atto current timestamp - Logs action to
storage/logs/mark-inactive-profiles.log
Usage:
# Mark all eligible profiles as inactive
php artisan profiles:mark-inactive
Output Example:
Checking profiles for inactivity...
[User] Marked inactive: John Doe (1095 days)
[User] Marked inactive: Jane Smith (1200 days)
[Organization] Marked inactive: Community Center (800 days)
Processing complete: 3 profiles marked as inactive
Log File: storage/logs/mark-inactive-profiles.log
[2025-11-14 01:30:15] === Starting profile inactivity check ===
[2025-11-14 01:30:16] [User] Marked INACTIVE: John Doe (ID: 123) - Not logged in for 1095 days (last login: 2022-01-01)
[2025-11-14 01:30:17] [User] Marked INACTIVE: Jane Smith (ID: 456) - Not logged in for 1200 days (last login: 2021-09-15)
[2025-11-14 01:30:18] === Completed: 3 profiles marked inactive ===
Schedule: Runs daily at 01:30 (via app/Console/Kernel.php)
2. Process Inactive Profiles
Command: php artisan profiles:process-inactive
Purpose: Send warning emails and delete profiles based on inactivity duration.
What It Does:
- Finds all User and Organization profiles where:
inactive_atis set (marked as inactive)deleted_atis NULL (not already deleted)
- Calculates days since
inactive_at - Determines action based on thresholds:
- Send Warning 1 email
- Send Warning 2 email
- Send Final Warning email
- Delete profile
- Emails are queued (not sent immediately)
- Logs all actions to
storage/logs/inactive-profiles.log
Usage:
# Process all inactive profiles
php artisan profiles:process-inactive
Output Example:
Processing inactive profiles...
[User] warning_1: John Doe (7 days remaining)
[User] warning_2: Jane Smith (5 days remaining)
[Organization] final: Community Center (1 day remaining)
[User] Deleted: Old User
Processing complete: 3 warnings sent, 1 profiles deleted
Log File: storage/logs/inactive-profiles.log
[2025-11-14 02:00:15] === Starting inactive profile processing ===
[2025-11-14 02:00:16] [User] warning_1 sent to John Doe (ID: 123) - Inactive for 7.5 days, 7.5 days remaining
[2025-11-14 02:00:17] [User] warning_2 sent to Jane Smith (ID: 456) - Inactive for 10.2 days, 4.8 days remaining
[2025-11-14 02:00:18] [Organization] final sent to Community Center (ID: 789) - Inactive for 14.1 days, 0.9 days remaining
[2025-11-14 02:00:19] [User] DELETED Old User (ID: 321) - Inactive for 15.5 days - Confirmation email sent to olduser@example.com
[2025-11-14 02:00:20] === Completed: 3 warnings, 1 deletions ===
Schedule: Runs daily at 02:00 (via app/Console/Kernel.php)
Important Notes:
- Emails are queued, not sent immediately (requires queue worker)
- Profiles with negative balances are skipped and logged
- Each profile receives only ONE email per day (based on current threshold)
- Soft-deleted profiles are automatically excluded
- Deletion confirmation emails are sent automatically after successful deletion
Workflow Process
Full Lifecycle Example
Timeline: Production Configuration
Day 0 (Jan 1, 2024):
├─ User logs in for last time
└─ last_login_at = 2024-01-01 10:00:00
Day 350 (Dec 16, 2024) at 01:30:
├─ profiles:mark-inactive runs
├─ Finds user (350 days since last login)
├─ Sets inactive_at = 2024-12-16 01:30:00
├─ Profile becomes hidden/labeled
└─ Log: "Marked INACTIVE: User (350 days)"
Day 350 (Dec 16, 2024) at 02:00:
├─ profiles:process-inactive runs
├─ User inactive for 0 days (just marked)
└─ No action (threshold not reached)
Day 357 (Dec 23, 2024) at 02:00:
├─ profiles:process-inactive runs
├─ User inactive for 7 days
├─ Threshold: warning_1 (7 days)
├─ Queues Warning 1 email
└─ Log: "warning_1 sent, 8 days remaining"
Email Content (Warning 1):
├─ Subject: "Warning: Your profile will be deleted soon"
├─ Header: "Inactive profile warning"
├─ Message: "Your profile has been inactive for 357 days..."
├─ Shows: Account balances, time remaining
└─ Button: "Activate profile"
Day 360 (Dec 26, 2024) at 02:00:
├─ profiles:process-inactive runs
├─ User inactive for 10 days
├─ Threshold: warning_2 (10 days)
├─ Queues Warning 2 email
└─ Log: "warning_2 sent, 5 days remaining"
Day 364 (Dec 30, 2024) at 02:00:
├─ profiles:process-inactive runs
├─ User inactive for 14 days
├─ Threshold: warning_final (14 days)
├─ Queues Final Warning email
└─ Log: "final sent, 1 day remaining"
Day 365 (Dec 31, 2024) at 02:00:
├─ profiles:process-inactive runs
├─ User inactive for 15 days
├─ Threshold: run_delete (15 days)
├─ Checks for negative balances → None
├─ Executes DeleteUser action (soft delete)
├─ Sets deleted_at = 2024-12-31 02:00:00
├─ Balance handling preferences stored in comment field
├─ Profile enters grace period (30 days by default)
├─ User can restore profile during grace period
├─ Deletion confirmation email sent
└─ Log: "DELETED User (365 days total)"
Day 395 (Jan 30, 2025) at 02:30:
├─ profiles:permanently-delete-expired runs
├─ Grace period expired (30 days since deletion)
├─ Executes permanent deletion
├─ Personal data anonymized
├─ Transactions anonymized
├─ Account balances handled per stored preferences
└─ Profile cannot be restored after this point
User Reactivation Scenarios
Scenario 1: User Logs In During Warning Period
Day 357: Warning 1 sent
Day 358: User logs in
├─ Auto-reactivation enabled (re-activate_at_login = true)
├─ Sets inactive_at = NULL
├─ Profile becomes active again
├─ User sees "Welcome back!" message
└─ Process stops (profile no longer inactive)
Scenario 2: User Clicks "Activate Profile" Button
Day 359: User clicks button in warning email
├─ Redirected to login page with intended route
├─ User logs in successfully
├─ Auto-reactivation triggers
├─ Sets inactive_at = NULL
└─ Profile active, deletion prevented
Scenario 3: User Does Nothing
Day 357: Warning 1 sent → User ignores
Day 360: Warning 2 sent → User ignores
Day 364: Final Warning → User ignores
Day 365: Profile soft-deleted → Grace period begins (30 days)
Day 395: Grace period expires → Profile permanently deleted
Scenario 4: Profile Restoration During Grace Period
Day 365: Profile soft-deleted (automatic deletion after inactivity)
├─ Deletion confirmation email sent
├─ Profile enters 30-day grace period
└─ Profile can be restored by admin
Day 370: Admin restores profile (5 days after deletion)
├─ Command: php artisan profiles:restore username
├─ Sets deleted_at = NULL
├─ Clears stored balance handling preferences
├─ Profile becomes active again
└─ User can log in normally
Scheduled Execution
Scheduler Configuration
Location: app/Console/Kernel.php (lines 51-63)
// Mark profiles as inactive when they haven't logged in for configured days
$schedule->command('profiles:mark-inactive')
->daily()
->at('01:30')
->withoutOverlapping()
->appendOutputTo(storage_path('logs/mark-inactive-profiles.log'));
// Process inactive profiles daily (send warnings and soft-delete profiles)
$schedule->command('profiles:process-inactive')
->daily()
->at('02:00')
->withoutOverlapping()
->appendOutputTo(storage_path('logs/inactive-profiles.log'));
// Permanently delete profiles after grace period expires
$schedule->command('profiles:permanently-delete-expired')
->daily()
->at('02:30')
->withoutOverlapping()
->appendOutputTo(storage_path('logs/permanent-deletions.log'));
Execution Times
- 01:30: Mark inactive profiles (Step 1)
- 02:00: Process inactive profiles (Step 2 - soft delete)
- 02:30: Permanently delete expired profiles (after grace period)
30-minute gap ensures profiles marked at 01:30 won't be immediately processed.
Enabling the Scheduler
Crontab Entry:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Verify Schedule:
# List all scheduled tasks
php artisan schedule:list
# Test scheduler without waiting
php artisan schedule:test
Testing
Testing Configuration
For rapid testing, use minute-based values:
'profile_inactive' => [
'days_not_logged_in' => 0.000694, // ~1 minute
],
'delete_profile' => [
'days_after_inactive' => [
'warning_1' => 0.001389, // ~2 minutes after marked inactive
'warning_2' => 0.00208, // ~3 minutes after marked inactive
'warning_final' => 0.00278, // ~4 minutes after marked inactive
'run_delete' => 0.00347, // ~5 minutes after marked inactive
],
],
Conversion Formula: days = minutes / 1440
Test Script: All 3 Warnings
File: test-all-warnings.sh
#!/bin/bash
USER_ID=${1:-102}
echo "=== Testing All 3 Warning Emails for User ID: $USER_ID ==="
# Warning 1: Set inactive_at to 2 minutes ago
echo "=== Test 1: Warning 1 (inactive for 2 minutes) ==="
php artisan tinker --execute="
\$user = App\Models\User::find($USER_ID);
\$user->inactive_at = now()->subMinutes(2);
\$user->save();
exit;
"
php artisan profiles:process-inactive
sleep 2
# Warning 2: Set inactive_at to 3 minutes ago
echo "=== Test 2: Warning 2 (inactive for 3 minutes) ==="
php artisan tinker --execute="
\$user = App\Models\User::find($USER_ID);
\$user->inactive_at = now()->subMinutes(3);
\$user->save();
exit;
"
php artisan profiles:process-inactive
sleep 2
# Warning Final: Set inactive_at to 4 minutes ago
echo "=== Test 3: Warning Final (inactive for 4 minutes) ==="
php artisan tinker --execute="
\$user = App\Models\User::find($USER_ID);
\$user->inactive_at = now()->subMinutes(4);
\$user->save();
exit;
"
php artisan profiles:process-inactive
echo "=== All 3 warning emails sent! ==="
php artisan queue:work --stop-when-empty --timeout=30
Usage:
chmod +x test-all-warnings.sh
./test-all-warnings.sh 102
Manual Testing Steps
Test 1: Mark Inactive
# Setup test user
php artisan tinker --execute="
\$user = App\Models\User::find(102);
\$user->last_login_at = now()->subMinutes(2);
\$user->inactive_at = null;
\$user->save();
echo 'User setup: last login 2 minutes ago';
exit;
"
# Run mark-inactive command
php artisan profiles:mark-inactive
# Verify user is now inactive
php artisan tinker --execute="
\$user = App\Models\User::find(102);
echo 'inactive_at: ' . \$user->inactive_at . PHP_EOL;
echo 'Is active: ' . (\$user->isActive() ? 'Yes' : 'No');
exit;
"
Test 2: Warning Emails
# Setup user for Warning 1
php artisan tinker --execute="
\$user = App\Models\User::find(102);
\$user->inactive_at = now()->subMinutes(2);
\$user->save();
exit;
"
# Process inactive profiles
php artisan profiles:process-inactive
# Process email queue
php artisan queue:work --stop-when-empty
# Check Mailpit for email
Test 3: Deletion
# Setup user for deletion
php artisan tinker --execute="
\$user = App\Models\User::find(102);
\$user->inactive_at = now()->subMinutes(6);
\$user->save();
exit;
"
# Process inactive profiles (will delete)
php artisan profiles:process-inactive
# Verify deletion
php artisan tinker --execute="
\$user = App\Models\User::withTrashed()->find(102);
echo 'deleted_at: ' . \$user->deleted_at . PHP_EOL;
exit;
"
Verification Checklist
- Command logs to correct file
- Profiles marked inactive at right threshold
- Warning 1 email received
- Warning 2 email received
- Final warning email received
- Profile deleted at correct threshold
- Profile with negative balance skipped
- Already deleted profiles not reprocessed
- Email queue processed successfully
- Transactions anonymized after deletion
Production Configuration
Recommended Production Settings
'profile_inactive' => [
'days_not_logged_in' => 350, // ~11.5 months
're-activate_at_login' => true,
'messenger_hidden' => true,
'profile_search_hidden' => true,
'profile_hidden' => true,
'profile_labeled' => true,
],
'delete_profile' => [
'days_after_inactive' => [
'warning_1' => 7, // 357 days total (~11.75 months)
'warning_2' => 10, // 360 days total (~11.83 months)
'warning_final' => 14, // 364 days total (~12 months)
'run_delete' => 15, // 365 days total (1 year)
],
],
Total Timeline: 1 year from last login to deletion
Alternative: Strict 3-Year Retention
'profile_inactive' => [
'days_not_logged_in' => 1080, // ~3 years
],
'delete_profile' => [
'days_after_inactive' => [
'warning_1' => 7, // 1087 days total
'warning_2' => 10, // 1090 days total
'warning_final' => 14, // 1094 days total
'run_delete' => 15, // 1095 days total (3 years)
],
],
Balance Handling Options
Option 1: Donate to Organization (Recommended)
'account_balances' => [
'donate_balances_to_organization_account_specified' => true,
'transfer_balances_to_bank_client' => false,
'transfer_balances_to_account_id' => null,
'transfer_balances_to_debit_account' => true,
],
Users can select an organization to receive their balance during deletion flow.
Option 2: Remove Currency (Debit Account)
'account_balances' => [
'donate_balances_to_organization_account_specified' => false,
'transfer_balances_to_bank_client' => false,
'transfer_balances_to_account_id' => null,
'transfer_balances_to_debit_account' => true, // Removes from circulation
],
Balance transferred to debit account (removes dead currency).
Option 3: Transfer to Specific Account
'account_balances' => [
'donate_balances_to_organization_account_specified' => false,
'transfer_balances_to_bank_client' => false,
'transfer_balances_to_account_id' => 123, // Specific account ID
'transfer_balances_to_debit_account' => false,
],
All balances go to a designated account (e.g., community fund).
Troubleshooting
Issue: Commands Not Running Automatically
Symptoms:
- Profiles not being marked inactive
- No warning emails sent
- Logs not updating
Diagnosis:
# Check if scheduler is configured
crontab -l | grep artisan
# Verify schedule:run works
php artisan schedule:run
# List scheduled tasks
php artisan schedule:list
Solution:
# Add to crontab
crontab -e
# Add this line:
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
# Verify
php artisan schedule:test
Issue: Emails Not Sending
Symptoms:
- Log shows "warning sent" but no email received
- Queue contains jobs but they're not processing
Diagnosis:
# Check queue status
php artisan queue:failed
# Check mail configuration
php artisan tinker --execute="dd(config('mail'));"
# Verify queue worker is running
ps aux | grep "queue:work"
Solution:
# Start queue worker
php artisan queue:work
# Or use supervisor for persistent queue worker
# See: Laravel Queue documentation
Issue: Already Deleted Profiles Being Processed
Symptoms:
- Log shows deletions for profiles that were already deleted
- "DELETED" entries for same profile multiple times
Fixed In: Version with whereNull('deleted_at') filter (app/Console/Commands/ProcessInactiveProfiles.php:50, 61)
Verification:
# Check ProcessInactiveProfiles.php contains:
grep "whereNull('deleted_at')" app/Console/Commands/ProcessInactiveProfiles.php
# Should see:
# ->whereNull('deleted_at') // Exclude already deleted profiles
Issue: Profiles with Negative Balances Not Skipped
Symptoms:
- Error when trying to delete profile with negative balance
- Deletion process crashes
Expected Behavior:
- Profiles with negative balances should be skipped
- Log entry: "SKIPPED deletion - Has negative balance"
Verification:
# Check logs for skip messages
grep "SKIPPED" storage/logs/inactive-profiles.log
# Manually test
php artisan tinker --execute="
\$user = App\Models\User::find(102);
\$accounts = \$user->accounts()->active()->notRemoved()->get();
foreach (\$accounts as \$account) {
echo \$account->name . ': ' . \$account->balance . PHP_EOL;
}
exit;
"
Issue: Wrong Warning Level Sent
Symptoms:
- User receives Warning 2 but should get Warning 1
- Only one warning email instead of three
Understanding: The system sends ONE warning per day based on CURRENT inactive duration:
- If user is 7 days inactive → Warning 1
- If user is 10 days inactive → Warning 2
- If user is 14 days inactive → Final Warning
This is by design. Users naturally progress through warnings as time passes.
Not a Bug: Each profile gets the appropriate warning for their current state, not all warnings at once.
Issue: Configuration Not Taking Effect
Symptoms:
- Changed config values but behavior unchanged
- Still using old thresholds
Solution:
# Clear config cache
php artisan config:clear
# Rebuild cache
php artisan config:cache
# Verify config loaded
php artisan tinker --execute="
echo 'days_not_logged_in: ' . timebank_config('profile_inactive.days_not_logged_in') . PHP_EOL;
echo 'warning_1: ' . timebank_config('delete_profile.days_after_inactive.warning_1') . PHP_EOL;
exit;
"
Related Systems
1. Profile Inactive System
Documentation: PROFILE_INACTIVE_CONFIG.md
The auto-delete system builds on the profile inactive system:
- Uses same
inactive_atfield - Respects same visibility/hiding rules
- Triggered by same
isActive()method
Integration:
MarkInactiveProfilessetsinactive_at- Profile becomes hidden (per inactive config)
ProcessInactiveProfilesprocesses these inactive profiles- Progressive warnings sent before deletion
2. User Deletion Action
Location: app/Actions/Jetstream/DeleteUser.php
The auto-delete system uses the existing DeleteUser action:
$deleteUser = new DeleteUser();
$deleteUser->delete($profile, 'delete', null);
What It Does:
- Anonymizes profile data
- Preserves transactions (anonymized)
- Handles account balances (per config)
- Soft deletes profile
- Note: Does NOT send deletion email (handled by ProcessInactiveProfiles command)
3. Email System
Warning Emails
Mailable Classes:
app/Mail/InactiveProfileWarning1Mail.phpapp/Mail/InactiveProfileWarning2Mail.phpapp/Mail/InactiveProfileWarningFinalMail.php
Email Templates:
resources/views/emails/inactive-profiles/{locale}/warning-1.blade.phpresources/views/emails/inactive-profiles/{locale}/warning-2.blade.phpresources/views/emails/inactive-profiles/{locale}/warning-final.blade.php
Languages Supported: en, nl, de, es, fr
Deletion Confirmation Email
Mailable Class:
app/Mail/UserDeletedMail.php
Email Template:
resources/views/emails/administration/{locale}/user-deleted.blade.php
Sent by: ProcessInactiveProfiles command (app/Console/Commands/ProcessInactiveProfiles.php:194)
When: Immediately after successful profile deletion
Content:
- Account details (username, full name, email)
- Deletion timestamp
- Balance handling information (total balance, donation details if applicable)
- Confirmation that data has been permanently deleted and anonymized
- Note that action is irreversible
Variables:
[
'time' => '18 November 2025, 22:26',
'deletedUser' => (object)[
'name' => 'ProfileName',
'full_name' => 'Full Profile Name',
'lang_preference' => 'en',
],
'mail' => 'user@example.com',
'balanceHandlingOption' => 'delete',
'totalBalance' => 120, // in minutes
'donationAccountId' => null,
'donationAccountName' => null,
'donationOrganizationName' => null,
]
Languages Supported: en, nl, de, es, fr
4. Queue System
All emails (warnings and deletion confirmations) are queued for asynchronous delivery:
- Uses Laravel queue system
- Requires queue worker running
- Prevents command timeout for large batches
- Emails are sent in the user's preferred language (
lang_preference)
Queue Configuration: config/queue.php
Email Types Queued:
- Warning emails (InactiveProfileWarning1Mail, InactiveProfileWarning2Mail, InactiveProfileWarningFinalMail)
- Deletion confirmation emails (UserDeletedMail)
5. Profile Deletion Grace Period
Documentation: PROFILE_DELETION_GRACE_PERIOD.md
When profiles are deleted (either automatically or manually), they enter a grace period before permanent deletion:
How It Works:
- Profile is soft-deleted (
deleted_attimestamp set) - Balance handling preferences stored in
commentfield as JSON - Profile remains in grace period for configurable days (default: 30)
- Profile can be restored during grace period using
php artisan profiles:restore - After grace period expires,
profiles:permanently-delete-expiredcommand:- Anonymizes profile data permanently
- Handles account balances per stored preferences
- Makes restoration impossible
Configuration:
'delete_profile' => [
'grace_period_days' => 30, // Days before permanent deletion
],
Restoration Command:
# List all restorable profiles
php artisan profiles:restore --list
# Restore specific profile
php artisan profiles:restore username
Integration with Auto-Delete:
- Auto-deleted profiles enter grace period automatically
- Deletion confirmation email mentions grace period
- Admin can restore accidentally deleted profiles
- Permanent deletion happens 30 days after initial deletion
Monitoring
Log Files
Mark Inactive Log: storage/logs/mark-inactive-profiles.log
# View recent marks
tail -n 50 storage/logs/mark-inactive-profiles.log
# Count marked today
grep "$(date +%Y-%m-%d)" storage/logs/mark-inactive-profiles.log | wc -l
Process Inactive Log: storage/logs/inactive-profiles.log
# View recent activity
tail -n 100 storage/logs/inactive-profiles.log
# Count deletions today
grep "DELETED" storage/logs/inactive-profiles.log | grep "$(date +%Y-%m-%d)" | wc -l
# Count warnings sent
grep "warning_1 sent" storage/logs/inactive-profiles.log | grep "$(date +%Y-%m-%d)" | wc -l
Metrics to Track
Daily:
- Number of profiles marked inactive
- Number of warnings sent (by level)
- Number of profiles deleted
- Number of profiles skipped (negative balance)
Weekly:
- Total inactive profiles
- Inactive profiles by stage (warning 1, 2, final, pending deletion)
- Average time to deletion
- Reactivation rate (users who logged back in)
Monthly:
- Data retention compliance
- Account balance disposal (by method)
- User notification effectiveness
- Error rates
Best Practices
1. Communication Strategy
Before Implementation:
- Announce new auto-delete policy to all users
- Update Terms of Service
- Add FAQ explaining the process
- Give 60-day grace period before first deletions
During Operation:
- Clear warning emails with specific dates
- Multiple warnings with escalating urgency
- Easy reactivation process (just login)
- Contact information for questions
2. Data Protection
GDPR Compliance:
- Auto-delete supports "right to be forgotten"
- Users can delete account anytime (no waiting)
- Clear retention policy stated upfront
- Audit trail via log files
Balance Protection:
- Never delete profiles with negative balances
- Allow balance donation before deletion
- Clear communication about balance disposal
- Option to withdraw balance before inactivity
3. Testing Strategy
Before Production:
- Test full cycle with minute-based config
- Verify all 3 warning emails send correctly
- Test deletion with various balance states
- Confirm transactions anonymize properly
- Verify scheduler runs correctly
- Test queue worker processes emails
- Check logs for errors
- Test reactivation flow
In Production:
- Monitor logs daily for first month
- Review deletion counts weekly
- Track reactivation rates
- Collect user feedback on warnings
- Adjust timing if needed
4. Gradual Rollout
Phase 1: Warnings Only (Month 1)
'days_after_inactive' => [
'warning_1' => 7,
'warning_2' => 10,
'warning_final' => 14,
'run_delete' => 99999, // Never delete (testing)
],
Send warnings but don't delete. Monitor feedback.
Phase 2: Long Timeline (Month 2-3)
'days_after_inactive' => [
'warning_1' => 30,
'warning_2' => 45,
'warning_final' => 60,
'run_delete' => 90, // 3 months from marked inactive
],
Very generous timeline while users adjust.
Phase 3: Normal Timeline (Month 4+)
'days_after_inactive' => [
'warning_1' => 7,
'warning_2' => 10,
'warning_final' => 14,
'run_delete' => 15,
],
Standard production timeline.
See Also
- PROFILE_DELETION_GRACE_PERIOD.md - Grace period and profile restoration
- PROFILE_INACTIVE_CONFIG.md - Profile inactive system documentation
- EMAIL-TESTING-GUIDE.md - Email testing procedures
app/Console/Commands/MarkInactiveProfiles.php- Mark inactive commandapp/Console/Commands/ProcessInactiveProfiles.php- Process inactive commandapp/Console/Commands/PermanentlyDeleteExpiredProfiles.php- Permanent deletion commandapp/Console/Commands/RestoreDeletedProfile.php- Profile restoration commandapp/Actions/Jetstream/DeleteUser.php- User deletion actionapp/Actions/Jetstream/RestoreProfile.php- Profile restoration actionapp/Mail/UserDeletedMail.php- Deletion confirmation email mailableapp/Http/Livewire/Profile/DeleteUserForm.php- Manual deletion form (uses same email)resources/views/emails/administration/{locale}/user-deleted.blade.php- Deletion email templateconfig/timebank-default.php- Full configuration reference
Changelog
2025-12-19 - Grace Period Implementation
- Added 30-day grace period for deleted profiles
- Profiles now soft-deleted first, permanently deleted after grace period
- Created
profiles:permanently-delete-expiredscheduled command - Created
profiles:restorecommand for profile restoration - Balance handling deferred until permanent deletion
- Updated deletion emails to mention grace period
- Added comprehensive grace period documentation
2025-11-18 - Deletion Confirmation Emails
- Added automatic deletion confirmation emails
- Integrated
UserDeletedMailintoProcessInactiveProfilescommand - Email sent immediately after successful profile deletion
- Updated log messages to include "Confirmation email sent to {email}"
- All deletion emails queued for asynchronous delivery
2025-11-14 - Initial Implementation
- Created two-step auto-delete system
- Implemented
MarkInactiveProfilescommand - Refactored
ProcessInactiveProfilesto useinactive_at - Added scheduler configuration
- Created comprehensive documentation
- Fixed soft-delete exclusion bug
- Added production and testing configurations
Last Updated: 2025-12-19
Maintainer: Development Team
Status: PRODUCTION READY
Commands: profiles:mark-inactive, profiles:process-inactive, profiles:permanently-delete-expired, profiles:restore