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

21 KiB

Profile Deletion Grace Period

This document describes the grace period system for profile deletions, which allows profiles to be restored within a configurable timeframe after deletion.

Overview

When a profile is deleted (either manually by the user or automatically due to inactivity), the system implements a two-phase deletion process:

  1. Soft Deletion: Profile is marked as deleted but data remains intact
  2. Permanent Deletion: After the grace period expires, profile data is permanently anonymized and balances are handled

This gives users a safety window to restore their profiles if deleted accidentally or if they change their mind.

Configuration

The grace period is configured per platform in the timebank configuration files:

Location: config/timebank-*.php

'delete_profile' => [
    'grace_period_days' => 30,  // Number of days before permanent deletion
    // ... other deletion settings
],

Default Value: 30 days

Recommendation: Keep at least 14 days to give users adequate time to restore their profiles.

How It Works

Manual Deletion (User-Initiated)

  1. User initiates deletion from profile settings
  2. Profile is soft-deleted:
    • deleted_at timestamp is set to current time
    • Balance handling preferences are stored in Redis cache (key: balance_handling_{ModelClass}_{ID})
    • Cache TTL: grace period + 7 days buffer
    • comment field remains empty for manual deletions
    • User is logged out and redirected to goodbye page
  3. Confirmation email is sent with grace period information
  4. Profile can be restored during grace period
  5. After grace period expires, scheduled command permanently deletes the profile

Auto-Deletion (Inactivity-Based)

  1. System detects inactive profile (based on inactive_at timestamp)
  2. Warning emails are sent before deletion
  3. Profile is soft-deleted if user doesn't respond
  4. Profile is soft-deleted:
    • deleted_at timestamp is set to current time
    • Balance handling preferences are stored in Redis cache
    • comment field is set to: "Profile automatically deleted after X days of inactivity." (translated)
  5. Email notification sent about automatic deletion
  6. Grace period begins, allowing restoration
  7. After grace period expires, permanent deletion occurs

Permanent Deletion Process

After the grace period expires:

  1. Balance handling preferences are retrieved from Redis cache:

    • Cache key: balance_handling_{ModelClass}_{ID}
    • If cache found: Execute user's choice (donate or destroy currency)
    • FALLBACK: If cache lost, destroy currency (transfer to debit account)
    • Fallback triggers a warning log for monitoring
  2. Balance handling is executed:

    • If donated: transfers to selected organization account
    • If deleted (or fallback): balances transferred to debit account (currency removed from circulation)
  3. Profile data is anonymized:

    • Email: removed-{id}@remove.ed
    • Name: Removed {profiletype} {id}
    • Password: randomized
    • Personal data: cleared
    • Comment: cleared
  4. Profile photo deleted

  5. Related data cleaned up (stars, bookmarks, etc.)

  6. Cache is cleared after successful completion

Artisan Commands

Restore Deleted Profile

Restore a profile that was deleted within the grace period.

Command Signature

php artisan profiles:restore {username?} {--list} {--type=}

Parameters

  • {username?} - Optional. Username of the profile to restore
  • --list - Display all profiles eligible for restoration
  • --type= - Filter by profile type (user, organization, bank, admin)

Usage Examples

List all restorable profiles:

php artisan profiles:restore --list

List restorable profiles by type:

php artisan profiles:restore --list --type=user
php artisan profiles:restore --list --type=organization

Restore specific profile:

php artisan profiles:restore john_doe

Interactive mode (prompts for username):

php artisan profiles:restore

Output Example

Available profiles for restoration:

Users:
┌──────────┬───────────────┬──────────────┬─────────────┐
│ Username │ Email         │ Deleted      │ Time Left   │
├──────────┼───────────────┼──────────────┼─────────────┤
│ john_doe │ john@test.com │ 2 days ago   │ 28 days     │
│ jane_smith│ jane@test.com │ 5 hours ago  │ 29 days     │
└──────────┴───────────────┴──────────────┴─────────────┘

Organizations:
┌──────────┬───────────────┬──────────────┬─────────────┐
│ Username │ Email         │ Deleted      │ Time Left   │
├──────────┼───────────────┼──────────────┼─────────────┤
│ org_test │ org@test.com  │ 1 day ago    │ 29 days     │
└──────────┴───────────────┴──────────────┴─────────────┘

Total: 3 profile(s) can be restored

Restoration Process

The command will:

  1. Verify the profile exists and is within grace period
  2. Check that the profile hasn't been anonymized
  3. Clear the deleted_at timestamp
  4. Clear stored balance handling preferences from comment field
  5. Log the restoration action
  6. Display success message with profile details

Error Handling

The command handles various error scenarios:

  • Profile not found: "Profile 'username' not found"
  • Already active: "Profile 'username' is not deleted"
  • Grace period expired: "Profile 'username' cannot be restored (grace period expired)"
  • Already anonymized: "Profile 'username' has already been permanently deleted"

Permanent Deletion Command (Scheduled)

This command runs automatically via Laravel scheduler to permanently delete expired profiles.

Command Signature

php artisan profiles:permanently-delete-expired

Schedule Configuration

Location: app/Console/Kernel.php

$schedule->command('profiles:permanently-delete-expired')
    ->daily()
    ->at('02:30')
    ->withoutOverlapping()
    ->appendOutputTo(storage_path('logs/permanent-deletions.log'));

Default Schedule: Daily at 02:30 AM

What It Does

  1. Finds all profiles where:
    • deleted_at is set
    • deleted_at + grace_period_days < now
    • Email is not already anonymized (not deleted_user_*)
  2. For each expired profile:
    • Retrieves balance handling preferences from comment field
    • Calls DeleteUser::permanentlyDelete() method
    • Handles balances according to stored preferences
    • Anonymizes profile data
  3. Logs all actions to storage/logs/permanent-deletions.log

Manual Execution

You can manually trigger permanent deletion of expired profiles:

php artisan profiles:permanently-delete-expired

Warning: This command is destructive and cannot be undone. Only run manually for testing or if you need to immediately process expired profiles.

Email Notifications

User-Deleted Email

Sent immediately when a profile is deleted.

Template: resources/views/emails/administration/user-deleted.blade.php

Contains:

  • Account details (username, email, deletion time)
  • Balance handling information (if applicable)
  • Grace period notification
  • Data handling information

Key Message:

"This action will become permanent after {days} days."

Auto-Deleted Email

Sent when a profile is automatically deleted due to inactivity.

Template: Same as user-deleted, with additional fields:

  • autoDeleted: true
  • Deletion reason
  • Days of inactivity

Technical Implementation

Data Storage During Grace Period

Profile Models (User, Organization, Bank, Admin):

  • deleted_at (datetime): Timestamp when profile was soft-deleted
  • comment (text):
    • Auto-deletion: Human-readable message (e.g., "Profile automatically deleted after 120 days of inactivity.")
    • Manual deletion: Empty (no comment)

Balance Handling Cache Storage (Redis):

Why Cache?

  • Balance handling is stored in cache (not database) to keep the comment field human-readable for administrators
  • User's balance handling choice (donate vs delete) needs to persist through the grace period
  • Balances are handled during permanent deletion (after grace period expires), not during soft delete
  • Cache automatically expires after grace period + buffer, self-cleaning the data
  • Transactions table provides permanent audit trail of actual balance transfers

Cache Details:

  • Key: balance_handling_{ModelClass}_{ProfileID}
  • TTL: Grace period days + 7 days buffer
  • Structure:
    [
        'option' => 'donate|delete',
        'donation_account_id' => 123,  // null if option is 'delete'
        'stored_at' => '2025-12-19 10:30:00'
    ]
    

Cache Fallback:

  • If cache is lost during permanent deletion, the system defaults to destroying currency (transfer to debit account)
  • A warning is logged: Balance handling cache lost for profile deletion
  • This ensures no orphaned balances remain in the system
  • Transaction table will show the fallback action for audit purposes

Translation Keys

Auto-deletion comment (resources/lang/*.json):

  • English: "Profile automatically deleted after :days days of inactivity."
  • Dutch: "Profiel automatisch verwijderd na :days dagen inactiviteit."
  • French: "Profil automatiquement supprimé après :days jours d'inactivité."
  • Spanish: "Perfil eliminado automáticamente después de :days días de inactividad."
  • German: "Profil automatisch nach :days Tagen Inaktivität gelöscht."

Key Classes and Files

Actions

app/Actions/Jetstream/DeleteUser.php

  • delete(): Soft deletes profile, stores balance preferences in Redis cache
  • permanentlyDelete(): Retrieves balance preferences from cache, handles balances, anonymizes profile, clears cache

app/Actions/Jetstream/RestoreProfile.php

  • restore(): Restores a soft-deleted profile within grace period

Commands

app/Console/Commands/RestoreDeletedProfile.php

  • Interactive CLI for restoring profiles
  • Lists eligible profiles
  • Handles restoration process

app/Console/Commands/PermanentlyDeleteExpiredProfiles.php

  • Scheduled task for permanent deletion
  • Processes expired profiles
  • Logs all actions

Livewire Components

app/Http/Livewire/Profile/DeleteUserForm.php

  • deleteUser(): Handles user-initiated deletion
  • Stores balance handling options
  • Manages logout and session
  • Redirects to goodbye page

Views

resources/views/goodbye-deleted-user.blade.php

  • Displayed after manual deletion
  • Shows deletion confirmation and grace period info

Routes

Route: /goodbye-deleted-user (Named: goodbye-deleted-user)

  • Displays goodbye page after deletion
  • Requires session('result') data

Testing

Test Manual Deletion

  1. Set grace period to 1 day in config for testing:

    'grace_period_days' => 1,
    
  2. Delete a test profile from profile settings

  3. Verify:

    • Goodbye page displays
    • Email received with grace period info
    • Profile listed in php artisan profiles:restore --list
  4. Restore the profile:

    php artisan profiles:restore test_user
    
  5. Verify profile is active and can log in

Test Automatic Deletion

  1. Run the permanent deletion command after grace period:

    php artisan profiles:permanently-delete-expired
    
  2. Verify:

    • Profile is permanently deleted
    • Data is anonymized
    • Balances handled according to preferences
    • Logged in storage/logs/permanent-deletions.log

Test Edge Cases

Grace period expired:

php artisan profiles:restore expired_user
# Should show: "Profile cannot be restored (grace period expired)"

Already anonymized:

php artisan profiles:restore permanently_deleted_user
# Should show: "Profile has already been permanently deleted"

Non-existent profile:

php artisan profiles:restore nonexistent
# Should show: "Profile 'nonexistent' not found"

Best Practices

For Administrators

  1. Monitor grace period activity:

    • Check storage/logs/permanent-deletions.log regularly
    • Review restoration requests
    • Adjust grace period based on user feedback
  2. Backup before major changes:

    • Database backup before reducing grace period
    • Test restoration process in staging first
  3. User communication:

    • Clearly communicate grace period in deletion UI
    • Include grace period in Terms of Service
    • Send reminder emails before permanent deletion (optional enhancement)

For Developers

  1. Never bypass grace period:

    • Always use DeleteUser@delete() for deletions
    • Never directly set deleted_at without storing balance preferences
    • Use DeleteUser@permanentlyDelete() only after grace period
  2. Preserve balance preferences:

    • Always store in comment field as JSON
    • Include stored_at timestamp
    • Validate before permanent deletion
  3. Testing:

    • Use short grace period (1 day) in development
    • Test both restoration and permanent deletion
    • Verify email notifications
  4. Cache management:

    • Clear account balance cache when restoring
    • Invalidate session data appropriately
    • Refresh Elasticsearch indices if needed

Troubleshooting

Profile shows as deleted but can't be restored

Possible causes:

  1. Grace period already expired
  2. Profile already permanently deleted (email starts with deleted_user_)
  3. Database inconsistency

Solution:

# Check profile status in database
php artisan tinker
>>> $user = App\Models\User::withTrashed()->where('name', 'username')->first();
>>> $user->deleted_at
>>> $user->email
>>> Carbon\Carbon::parse($user->deleted_at)->addDays(config('timebank_cc.delete_profile.grace_period_days'))

Balance handling preferences lost

Possible causes:

  1. comment field was cleared manually
  2. Profile updated after deletion

Solution:

  • Check database for comment field content
  • If lost, you'll need to manually decide balance handling during restoration

Permanent deletion not running

Possible causes:

  1. Laravel scheduler not configured
  2. Command has errors
  3. No expired profiles

Solution:

# Check scheduler is running
php artisan schedule:list

# Run manually to see errors
php artisan profiles:permanently-delete-expired

# Check logs
tail -f storage/logs/laravel.log
tail -f storage/logs/permanent-deletions.log

Restored profile can't log in

Possible causes:

  1. Cache not cleared
  2. Session data corrupted
  3. Password was changed during soft deletion

Solution:

# Clear all caches
php artisan cache:clear
php artisan view:clear
php artisan config:clear

# Reset password if needed
php artisan tinker
>>> $user = App\Models\User::where('name', 'username')->first();
>>> $user->password = Hash::make('newpassword');
>>> $user->save();

Future Enhancements

Potential improvements to the grace period system:

  1. Reminder emails:

    • Send email 7 days before permanent deletion
    • Send final warning 24 hours before deletion
  2. Self-service restoration:

    • Allow users to restore their own profiles via email link
    • Magic link authentication for deleted accounts
  3. Extended grace for specific cases:

    • Longer grace period for profiles with high balance
    • Longer grace for organization/bank profiles
  4. Audit trail:

    • Track who restored profiles and when
    • Log reason for restoration
    • Activity log entries for all grace period actions
  5. Dashboard for admins:

    • View all profiles in grace period
    • Bulk restore operations
    • Analytics on deletion patterns

WireChat Message Handling

All WireChat messages are eventually deleted to prevent orphaned data. When a profile is permanently deleted, their sent messages are released for cleanup.

Configuration

Location: config/timebank-*.php

'wirechat' => [
    'disappearing_messages' => [
        'allow_users_to_keep' => true,      // Allow marking messages as "kept"
        'duration' => 30,                    // Days before regular messages deleted
        'kept_messages_duration' => 90,      // Days before kept messages deleted
        'cleanup_schedule' => 'everyFiveMinutes',
    ],
    'profile_deletion' => [
        'release_kept_messages' => true,     // MUST be true to prevent orphans
    ],
],

Note: Disappearing messages are ALWAYS enabled. There is no 'enabled' flag - messages are always cleaned up to prevent orphaned data from deleted profiles.

Behavior

All messages are eventually deleted:

  • Regular messages: Deleted after duration days (default: 30 days)
  • Kept messages: Deleted after kept_messages_duration days (default: 90 days)
  • Messages from deleted profiles: Released and then deleted after duration days

When profile is permanently deleted:

  1. Profile enters grace period (default: 30 days)
  2. Grace period expires → permanent deletion runs
  3. All kept messages sent by that profile: kept_at = null (released)
  4. Messages become regular messages, deleted after duration days
  5. This prevents orphaned messages with invalid sendable_id

Timeline Example

With default settings:

  • duration = 30 days
  • kept_messages_duration = 90 days
  • release_kept_messages = true
Scenario 1: Normal message lifecycle
Day 0: User sends message
Day 30: Message automatically deleted by DeleteExpiredWireChatMessagesJob

Scenario 2: Kept message lifecycle
Day 0: User sends message, recipient marks as "kept"
Day 90: Kept message automatically deleted by cleanup job

Scenario 3: Sender deletes profile
Day 0: User sends message, recipient marks as "kept"
Day 365: User deletes profile → grace period starts
Day 395: Grace period expires → permanent deletion
  → kept_at set to null (message "released")
  → Message now subject to normal 30-day duration
Day 425: Message deleted by cleanup job (30 days after release)

Why This Prevents Orphaned Messages

Without release_kept_messages:

  • Deleted profile has sendable_id = 123, sendable_type = 'App\Models\User'
  • Profile anonymized: name becomes "Removed user abc123ef"
  • Kept messages still reference old sendable_id pointing to anonymized profile
  • Messages orphaned forever with no way to trace original sender
  • Database accumulates orphaned data indefinitely

With release_kept_messages = true:

  • Kept messages released when profile permanently deleted
  • Normal cleanup job deletes them after duration days
  • No orphaned data remains in database
  • Clean data retention without privacy concerns
  • Conversation history maintained for recipients until cleanup

Configuration Notes

  • release_kept_messages should ALWAYS be true (default and recommended)
  • Setting it to false will cause orphaned messages to accumulate
  • All durations specified in DAYS (converted to seconds internally)
  • Cleanup job runs based on cleanup_schedule (default: every 5 minutes)
  • User control is hardcoded to false (users cannot change message duration)
  • Disappearing messages are hardcoded to enabled (always active)
  • Profile Auto-Delete: references/PROFILE_AUTO_DELETE.md - Automatic deletion based on inactivity
  • Profile Inactive Config: references/PROFILE_INACTIVE_CONFIG.md - Configuration for inactive profile detection
  • Email Testing: references/EMAIL-TESTING-GUIDE.md - Testing email notifications

Changelog

Version 1.1 (2025-12-19)

  • BREAKING CHANGE: Balance handling data moved from comment field to Redis cache
  • Added cache fallback: automatically destroys currency if cache is lost
  • Comment field now human-readable:
    • Auto-deletion: Shows translated message "Profile automatically deleted after X days of inactivity"
    • Manual deletion: Empty (no comment)
  • Added translation keys for auto-deletion comments in 5 languages (en, nl, fr, de, es)
  • Enhanced logging: Warning logged when cache fallback is triggered
  • Cache automatically expires after grace period + 7 days buffer
  • Transaction table provides complete audit trail of balance handling

Version 1.0 (2025-12-19)

  • Initial implementation of grace period system
  • Two-phase deletion process (soft delete + permanent delete)
  • Restore command with interactive and list modes
  • Scheduled permanent deletion command
  • Email notifications with grace period information
  • Balance handling deferred until permanent deletion