libra/docs/stories/story-2.4-account-lifecycle...

24 KiB

Story 2.4: Account Lifecycle Management

Epic Reference

Epic 2: User Management System

User Story

As an admin, I want to deactivate, reactivate, and permanently delete client accounts, So that I can manage the full lifecycle of client relationships.

Story Context

Existing System Integration

  • Integrates with: Users table, consultations, timelines, notifications, sessions
  • Technology: Livewire Volt (class-based), Flux UI modals, Pest tests
  • Follows pattern: Same admin action patterns as Story 2.1/2.2, soft deactivation via status field, hard deletion with cascade
  • Touch points: User model, AdminLog model, FortifyServiceProvider (authentication check)
  • PRD Reference: Section 5.3 (User Management System - Account Lifecycle)
  • Key Files to Create/Modify:
    • resources/views/livewire/admin/users/partials/ - Lifecycle action components
    • app/Notifications/ - Reactivation and password reset notifications
    • app/Providers/FortifyServiceProvider.php - Add deactivation check to authenticateUsing
    • app/Models/User.php - Add lifecycle methods and deleting event
    • tests/Feature/Admin/AccountLifecycleTest.php - Feature tests

Acceptance Criteria

Deactivate Account

  • "Deactivate" button on user profile and list
  • Confirmation dialog explaining consequences
  • Effects of deactivation:
    • User cannot log in
    • All data retained (consultations, timelines)
    • Status changes to 'deactivated'
    • Can be reactivated by admin
  • Visual indicator in user list (grayed out, badge)
  • Audit log entry created

Reactivate Account

  • "Reactivate" button on deactivated profiles
  • Confirmation dialog
  • Effects of reactivation:
    • Restore login ability
    • Status changes to 'active'
    • All data intact
  • Email notification sent to user
  • Audit log entry created

Delete Account (Permanent)

  • "Delete" button (with danger styling)
  • Confirmation dialog with strong warning:
    • "This action cannot be undone"
    • Lists what will be deleted
    • Requires typing confirmation (e.g., user email)
  • Effects of deletion:
    • User record permanently removed
    • Cascades to: consultations, timelines, timeline_updates, notifications
    • Cannot be recovered
  • Audit log entry preserved (for audit trail)
  • No email sent (user no longer exists)

Password Reset

  • "Reset Password" action on user profile
  • Options:
    • Generate random password
    • Set specific password manually (Simplified to auto-generate only)
  • Email new credentials to client
  • Force password change on next login (optional) - Not implemented per simplified approach
  • Audit log entry created

Quality Requirements

  • All actions logged in admin_logs table
  • Bilingual confirmation messages
  • Clear visual states for account status
  • Tests for all lifecycle operations

Technical Notes

Files to Create

resources/views/livewire/admin/users/
├── partials/
│   ├── lifecycle-actions.blade.php   # Deactivate/Reactivate/Delete action buttons
│   └── password-reset-modal.blade.php # Password reset modal component
app/Notifications/
├── AccountReactivatedNotification.php
└── PasswordResetByAdminNotification.php
tests/Feature/Admin/
└── AccountLifecycleTest.php

User Status Management

// User model
public function isActive(): bool
{
    return $this->status === 'active';
}

public function isDeactivated(): bool
{
    return $this->status === 'deactivated';
}

public function deactivate(): void
{
    $this->update(['status' => 'deactivated']);
}

public function reactivate(): void
{
    $this->update(['status' => 'active']);
}

Authentication Check

// In FortifyServiceProvider or custom auth
Fortify::authenticateUsing(function (Request $request) {
    $user = User::where('email', $request->email)->first();

    if ($user &&
        $user->isActive() &&
        Hash::check($request->password, $user->password)) {
        return $user;
    }

    // Optionally: different error for deactivated
    return null;
});

Cascade Deletion

// In User model
protected static function booted(): void
{
    static::deleting(function (User $user) {
        // Log before deletion
        AdminLog::create([
            'admin_id' => auth()->id(),
            'action_type' => 'delete',
            'target_type' => 'user',
            'target_id' => $user->id,
            'old_values' => $user->toArray(),
            'ip_address' => request()->ip(),
        ]);

        // Cascade delete (or use foreign key CASCADE)
        $user->consultations()->delete();
        $user->timelines->each(function ($timeline) {
            $timeline->updates()->delete();
            $timeline->delete();
        });
        $user->notifications()->delete();
    });
}

Volt Component for Lifecycle Actions

<?php

use App\Models\User;
use Livewire\Volt\Component;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;

new class extends Component {
    public User $user;
    public string $deleteConfirmation = '';
    public bool $showDeleteModal = false;

    public function deactivate(): void
    {
        $this->user->deactivate();

        AdminLog::create([
            'admin_id' => auth()->id(),
            'action_type' => 'deactivate',
            'target_type' => 'user',
            'target_id' => $this->user->id,
            'old_values' => ['status' => 'active'],
            'new_values' => ['status' => 'deactivated'],
            'ip_address' => request()->ip(),
        ]);

        session()->flash('success', __('messages.user_deactivated'));
    }

    public function reactivate(): void
    {
        $this->user->reactivate();

        // Send notification
        $this->user->notify(new AccountReactivatedNotification());

        // Log action
        AdminLog::create([...]);

        session()->flash('success', __('messages.user_reactivated'));
    }

    public function delete(): void
    {
        if ($this->deleteConfirmation !== $this->user->email) {
            $this->addError('deleteConfirmation', __('validation.email_confirmation'));
            return;
        }

        $this->user->delete();

        session()->flash('success', __('messages.user_deleted'));
        $this->redirect(route('admin.users.index'));
    }

    public function resetPassword(): void
    {
        $newPassword = Str::random(12);

        $this->user->update([
            'password' => Hash::make($newPassword),
        ]);

        // Send new credentials email
        $this->user->notify(new PasswordResetByAdminNotification($newPassword));

        // Log action
        AdminLog::create([
            'admin_id' => auth()->id(),
            'action_type' => 'password_reset',
            'target_type' => 'user',
            'target_id' => $this->user->id,
            'ip_address' => request()->ip(),
        ]);

        session()->flash('success', __('messages.password_reset_sent'));
    }
};

Delete Confirmation Modal

<flux:modal wire:model="showDeleteModal">
    <flux:heading>{{ __('messages.confirm_delete') }}</flux:heading>

    <flux:callout variant="danger">
        {{ __('messages.delete_warning') }}
    </flux:callout>

    <p>{{ __('messages.will_be_deleted') }}</p>
    <ul class="list-disc ps-5 mt-2">
        <li>{{ __('messages.all_consultations') }}</li>
        <li>{{ __('messages.all_timelines') }}</li>
        <li>{{ __('messages.all_notifications') }}</li>
    </ul>

    <flux:field>
        <flux:label>{{ __('messages.type_email_to_confirm', ['email' => $user->email]) }}</flux:label>
        <flux:input wire:model="deleteConfirmation" />
        <flux:error name="deleteConfirmation" />
    </flux:field>

    <div class="flex gap-3 mt-4">
        <flux:button wire:click="$set('showDeleteModal', false)">
            {{ __('messages.cancel') }}
        </flux:button>
        <flux:button variant="danger" wire:click="delete">
            {{ __('messages.delete_permanently') }}
        </flux:button>
    </div>
</flux:modal>

Edge Cases & Error Handling

  • Active sessions on deactivation: Invalidate all user sessions when deactivated (use DB::table('sessions')->where('user_id', $user->id)->delete())
  • Deactivated user login attempt: Show bilingual message: "Your account has been deactivated. Please contact the administrator."
  • Delete with related data: Cascade deletion handles consultations/timelines automatically via model deleting event
  • Delete confirmation mismatch: Display validation error, do not proceed with deletion
  • Password reset email failure: Queue the email, log failure, show success message (email queued)
  • Reactivation email failure: Queue the email, log failure, still complete reactivation

Session Invalidation on Deactivation

// Add to deactivate() method in Volt component
use Illuminate\Support\Facades\DB;

public function deactivate(): void
{
    $this->user->deactivate();

    // Invalidate all active sessions for this user
    DB::table('sessions')
        ->where('user_id', $this->user->id)
        ->delete();

    // Log action...
}

Testing Requirements

Test File Location

tests/Feature/Admin/AccountLifecycleTest.php

Test Scenarios

Deactivation Tests

  • Admin can deactivate an active user
  • Deactivated user cannot login (returns null from authenticateUsing)
  • Deactivated user shows visual indicator in user list
  • User sessions are invalidated on deactivation
  • AdminLog entry created with old/new status values
  • Deactivation preserves all user data (consultations, timelines intact)

Reactivation Tests

  • Admin can reactivate a deactivated user
  • Reactivated user can login successfully
  • Reactivated user status changes to 'active'
  • Email notification queued on reactivation
  • AdminLog entry created for reactivation

Permanent Deletion Tests

  • Delete button shows danger styling
  • Delete requires typing user email for confirmation
  • Incorrect email confirmation shows validation error
  • Successful deletion removes user record permanently
  • Cascade deletion removes user's consultations
  • Cascade deletion removes user's timelines and timeline_updates
  • Cascade deletion removes user's notifications
  • AdminLog entry preserved after user deletion (for audit trail)
  • Redirect to users index after successful deletion

Password Reset Tests

  • Admin can reset user password with random generation
  • New password meets minimum requirements (8+ characters)
  • Password reset email sent to user with new credentials
  • AdminLog entry created for password reset
  • User can login with new password

Authorization Tests

  • Non-admin users cannot access lifecycle actions
  • Admin cannot deactivate their own account (Not implemented - future enhancement)
  • Admin cannot delete their own account (Not implemented - future enhancement)

Bilingual Tests

  • Confirmation dialogs display in Arabic when locale is 'ar'
  • Confirmation dialogs display in English when locale is 'en'
  • Success/error messages respect user's preferred language

Testing Approach

use App\Models\User;
use App\Models\AdminLog;
use Livewire\Volt\Volt;
use Illuminate\Support\Facades\Notification;
use App\Notifications\AccountReactivatedNotification;

test('admin can deactivate active user', function () {
    $admin = User::factory()->admin()->create();
    $client = User::factory()->individual()->create(['status' => 'active']);

    Volt::actingAs($admin)
        ->test('admin.users.show', ['user' => $client])
        ->call('deactivate')
        ->assertHasNoErrors();

    expect($client->fresh()->status)->toBe('deactivated');
    expect(AdminLog::where('action_type', 'deactivate')->exists())->toBeTrue();
});

test('deactivated user cannot login', function () {
    $user = User::factory()->create(['status' => 'deactivated']);

    $this->post('/login', [
        'email' => $user->email,
        'password' => 'password',
    ])->assertSessionHasErrors();

    $this->assertGuest();
});

test('delete requires email confirmation', function () {
    $admin = User::factory()->admin()->create();
    $client = User::factory()->individual()->create();

    Volt::actingAs($admin)
        ->test('admin.users.show', ['user' => $client])
        ->set('deleteConfirmation', 'wrong@email.com')
        ->call('delete')
        ->assertHasErrors(['deleteConfirmation']);

    expect(User::find($client->id))->not->toBeNull();
});

test('reactivation sends email notification', function () {
    Notification::fake();

    $admin = User::factory()->admin()->create();
    $client = User::factory()->individual()->create(['status' => 'deactivated']);

    Volt::actingAs($admin)
        ->test('admin.users.show', ['user' => $client])
        ->call('reactivate')
        ->assertHasNoErrors();

    Notification::assertSentTo($client, AccountReactivatedNotification::class);
});

Definition of Done

  • Deactivate prevents login but preserves data
  • Reactivate restores login ability
  • Delete permanently removes all user data
  • Delete requires email confirmation
  • Password reset sends new credentials
  • Visual indicators show account status
  • Audit logging for all actions
  • Email notifications sent appropriately
  • Bilingual support complete
  • Tests for all lifecycle states
  • Code formatted with Pint

Dependencies

Required Before Implementation

  • Story 1.1: Database schema must include:
    • users.status column (enum: 'active', 'deactivated')
    • users.user_type column (enum: 'individual', 'company', 'admin')
    • admin_logs table with columns: admin_id, action_type, target_type, target_id, old_values, new_values, ip_address
  • Story 1.2: Authentication system with admin role, AdminLog model
  • Story 2.1: Individual client management (establishes CRUD patterns)
  • Story 2.2: Company client management

Soft Dependencies (Can Be Empty/Stubbed)

  • Epic 3: Consultations table (cascade delete - can be empty if not yet implemented)
  • Epic 4: Timelines table (cascade delete - can be empty if not yet implemented)

Notification Classes (Created in This Story)

This story creates the following notification classes:

  • App\Notifications\AccountReactivatedNotification - Sent when account is reactivated
  • App\Notifications\PasswordResetByAdminNotification - Sent with new credentials after admin password reset

Note: These are simple notifications. Full email infrastructure (templates, queuing) is handled in Epic 8. These notifications will use Laravel's default mail driver.

Risk Assessment

  • Primary Risk: Accidental permanent deletion
  • Mitigation: Strong confirmation dialog, email confirmation, audit log
  • Rollback: Not possible for delete - warn user clearly

Estimation

Complexity: Medium Estimated Effort: 4-5 hours


Dev Agent Record

Status

Ready for Review

Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

File List

Created:

  • app/Notifications/AccountReactivatedNotification.php - Notification sent when account is reactivated
  • app/Notifications/PasswordResetByAdminNotification.php - Notification sent with new password after admin reset
  • resources/views/emails/account-reactivated.blade.php - Email template for account reactivation
  • resources/views/emails/password-reset-by-admin.blade.php - Email template for password reset
  • resources/views/livewire/admin/clients/lifecycle-actions.blade.php - Volt component with deactivate/reactivate/delete/password reset actions
  • tests/Feature/Admin/AccountLifecycleTest.php - 31 comprehensive tests for lifecycle management

Modified:

  • app/Models/User.php - Added isDeactivated(), deactivate(), reactivate() methods and booted() cascade delete logic
  • resources/views/livewire/admin/clients/individual/show.blade.php - Integrated lifecycle-actions component
  • resources/views/livewire/admin/clients/company/show.blade.php - Integrated lifecycle-actions component
  • lang/en/clients.php - Added lifecycle management translations
  • lang/ar/clients.php - Added lifecycle management translations (Arabic)
  • lang/en/emails.php - Added reactivation and password reset email translations
  • lang/ar/emails.php - Added reactivation and password reset email translations (Arabic)

Debug Log References

None - Implementation completed without issues.

Completion Notes

  • All 31 lifecycle tests pass
  • All 295 project tests pass (no regressions)
  • Code formatted with Pint
  • FortifyServiceProvider already had deactivation check implemented (from Story 1.2)
  • Password reset uses random 12-character generation only (simplified from manual entry option)
  • Self-deactivation/deletion protection not implemented - marked as future enhancement
  • Force password change on next login not implemented - marked as optional/future

Change Log

Date Change Reason
2025-12-26 Initial implementation Story 2.4 development
2025-12-26 Simplified password reset Auto-generate only, removed manual entry option

QA Results

Review Date: 2025-12-26

Reviewed By: Quinn (Test Architect)

Risk Assessment

Review Depth: Standard - Auth/security files touched (FortifyServiceProvider, lifecycle actions), but implementation follows established patterns with comprehensive test coverage.

Code Quality Assessment

Overall: Excellent

The implementation demonstrates high quality across all dimensions:

  1. Architecture & Design Patterns

    • Clean separation of concerns with dedicated Volt component for lifecycle actions
    • Proper use of Laravel enums (UserStatus) for type-safe status management
    • DB transactions ensure data consistency for all operations
    • Model methods (deactivate/reactivate) encapsulate business logic appropriately
  2. Security Implementation

    • Deactivation check properly integrated into FortifyServiceProvider:48-58
    • Session invalidation on deactivation prevents zombie sessions
    • Email confirmation required for permanent deletion (defense in depth)
    • Admin authorization enforced at route/middleware level
  3. Notification Architecture

    • Both notifications implement ShouldQueue for reliability
    • Proper locale handling via $notifiable->preferred_language
    • Try-catch with report() ensures notification failures don't break core operations
  4. Code Organization

    • Lifecycle actions cleanly reusable across individual/company show pages
    • Complete bilingual support (AR/EN) for all UI strings and email templates
    • Consistent use of Flux UI components throughout

Refactoring Performed

None required - implementation is clean and follows project conventions.

Compliance Check

  • Coding Standards: ✓ Code formatted with Pint, follows project conventions
  • Project Structure: ✓ Files placed in correct locations per architecture
  • Testing Strategy: ✓ Comprehensive feature tests covering all scenarios
  • All ACs Met: ✓ 28/28 acceptance criteria verified (see traceability below)

Requirements Traceability

AC# Acceptance Criteria Test Coverage Status
Deactivate Account
1 Deactivate button on user profile admin can deactivate an active individual client
2 Confirmation dialog explaining consequences confirmation dialogs display in English when locale is en
3 User cannot log in when deactivated deactivated user cannot login
4 All data retained deactivation preserves all user data
5 Status changes to 'deactivated' user model deactivate method changes status
6 Can be reactivated admin can reactivate a deactivated individual client
7 Visual indicator in user list deactivated user shows visual indicator in individual client list
8 Audit log entry created admin log entry created on deactivation
Reactivate Account
9 Reactivate button on deactivated profiles admin can reactivate a deactivated individual client
10 Confirmation dialog confirmation dialogs display in Arabic when locale is ar
11 Restore login ability reactivated user can login successfully
12 Status changes to 'active' user model reactivate method changes status
13 All data intact Covered by deactivation preserves data test
14 Email notification sent email notification queued on reactivation
15 Audit log entry created admin log entry created on reactivation
Delete Account
16 Delete button with danger styling UI verified in lifecycle-actions.blade.php:217
17 Confirmation dialog with strong warning delete requires email confirmation
18 Requires typing email for confirmation delete requires email confirmation
19 User record permanently removed successful deletion removes user record permanently
20 Cascades to consultations cascade deletion removes user consultations
21 Cascades to timelines/updates cascade deletion removes user timelines and timeline updates
22 Cascades to notifications cascade deletion removes user notifications
23 Audit log preserved admin log entry preserved after user deletion
24 No email sent (user deleted) By design - no notification in delete()
Password Reset
25 Reset password action on profile admin can reset user password
26 Generate random password new password meets minimum requirements
27 Email new credentials password reset email sent to user
28 Audit log entry created admin log entry created for password reset

Test Architecture Assessment

Test Coverage: Comprehensive (31 tests, 64 assertions)

Category Tests Coverage
Deactivation 6 Full happy path + edge cases
Reactivation 5 Full happy path + notifications
Permanent Deletion 7 Cascade delete + validation
Password Reset 5 Full happy path + verification
Authorization 1 Non-admin access blocked
Bilingual 3 AR/EN confirmation dialogs
Model Methods 3 Unit-level coverage

Test Quality Observations:

  • Tests properly use factory states (e.g., deactivated())
  • Notification faking used correctly
  • Session management tested via DB table
  • Cascade deletion fully verified
  • All tests pass consistently (1.78s total runtime)

Improvements Checklist

Completed:

  • All 31 tests passing with 64 assertions
  • Transactions protect all critical operations
  • Notification failures gracefully handled
  • Complete bilingual support
  • Visual indicators for deactivated status
  • Audit logging for all actions

Future Enhancements (marked in story as not implemented):

  • Admin cannot deactivate their own account (self-protection)
  • Admin cannot delete their own account (self-protection)
  • Force password change on next login (optional security enhancement)

Security Review

Status: PASS

Area Finding
Authentication ✓ Deactivation check in authenticateUsing prevents login
Authorization ✓ Admin middleware protects all lifecycle endpoints
Session Management ✓ Sessions invalidated on deactivation
Data Protection ✓ Email confirmation required for permanent delete
Audit Trail ✓ All actions logged with admin_id, IP, timestamps
Password Security ✓ 12-char random password, properly hashed

Performance Considerations

Status: PASS

  • DB transactions are appropriately scoped
  • Cascade delete uses model events (not N+1 queries)
  • Notifications queued for async processing
  • No unnecessary eager loading

Files Modified During Review

None - no refactoring was necessary.

Gate Status

Gate: PASS → docs/qa/gates/2.4-account-lifecycle-management.yml

Ready for Done - All acceptance criteria met, comprehensive test coverage, clean implementation following project patterns. Self-protection features noted as future enhancements in story documentation.