Files
timebank-cc-public/references/PROFILE_AUTO_DELETE.md
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

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

  1. Overview
  2. System Architecture
  3. Configuration Structure
  4. Artisan Commands
  5. Workflow Process
  6. Scheduled Execution
  7. Testing
  8. Production Configuration
  9. Troubleshooting
  10. Related Systems

Overview

The profile auto-delete system automatically manages inactive profiles through a two-step process:

  1. Mark Inactive: Profiles that haven't logged in for a configured period are automatically marked as inactive
  2. 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 login
  • inactive_at (timestamp, nullable) - When profile was marked inactive
  • deleted_at (timestamp, nullable) - Soft delete timestamp

Field States:

  • inactive_at = NULL → Profile is active
  • inactive_at = [past date] → Profile is inactive, awaiting deletion
  • deleted_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_CONFIG environment 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:

  1. Finds all User and Organization profiles where:
    • last_login_at is older than configured days_not_logged_in
    • inactive_at is NULL (not already marked inactive)
  2. Sets inactive_at to current timestamp
  3. 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:

  1. Finds all User and Organization profiles where:
    • inactive_at is set (marked as inactive)
    • deleted_at is NULL (not already deleted)
  2. Calculates days since inactive_at
  3. Determines action based on thresholds:
    • Send Warning 1 email
    • Send Warning 2 email
    • Send Final Warning email
    • Delete profile
  4. Emails are queued (not sent immediately)
  5. 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

'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;
"

1. Profile Inactive System

Documentation: PROFILE_INACTIVE_CONFIG.md

The auto-delete system builds on the profile inactive system:

  • Uses same inactive_at field
  • Respects same visibility/hiding rules
  • Triggered by same isActive() method

Integration:

  • MarkInactiveProfiles sets inactive_at
  • Profile becomes hidden (per inactive config)
  • ProcessInactiveProfiles processes 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.php
  • app/Mail/InactiveProfileWarning2Mail.php
  • app/Mail/InactiveProfileWarningFinalMail.php

Email Templates:

  • resources/views/emails/inactive-profiles/{locale}/warning-1.blade.php
  • resources/views/emails/inactive-profiles/{locale}/warning-2.blade.php
  • resources/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:

  1. Warning emails (InactiveProfileWarning1Mail, InactiveProfileWarning2Mail, InactiveProfileWarningFinalMail)
  2. 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_at timestamp set)
  • Balance handling preferences stored in comment field 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-expired command:
    • 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 command
  • app/Console/Commands/ProcessInactiveProfiles.php - Process inactive command
  • app/Console/Commands/PermanentlyDeleteExpiredProfiles.php - Permanent deletion command
  • app/Console/Commands/RestoreDeletedProfile.php - Profile restoration command
  • app/Actions/Jetstream/DeleteUser.php - User deletion action
  • app/Actions/Jetstream/RestoreProfile.php - Profile restoration action
  • app/Mail/UserDeletedMail.php - Deletion confirmation email mailable
  • app/Http/Livewire/Profile/DeleteUserForm.php - Manual deletion form (uses same email)
  • resources/views/emails/administration/{locale}/user-deleted.blade.php - Deletion email template
  • config/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-expired scheduled command
  • Created profiles:restore command 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 UserDeletedMail into ProcessInactiveProfiles command
  • 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 MarkInactiveProfiles command
  • Refactored ProcessInactiveProfiles to use inactive_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