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

457 lines
15 KiB
Markdown

# 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
- [ ] Email new credentials to client
- [ ] Force password change on next login (optional)
- [ ] 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
```php
// 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
```php
// 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
```php
// 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
<?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
```blade
<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
```php
// 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
- [ ] Admin cannot delete their own account
#### 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
```php
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