libra/docs/stories/story-6.8-system-settings.md

15 KiB

Story 6.8: System Settings

Epic Reference

Epic 6: Admin Dashboard

User Story

As an admin, I want to configure system-wide settings, So that I can customize the platform to my needs.

Dependencies

  • Story 1.2: Authentication & Role System (admin auth, User model)
  • Story 8.1: Email Infrastructure Setup (mail configuration for test email)

Navigation Context

  • Accessible from admin dashboard sidebar/navigation
  • Route: /admin/settings
  • Named route: admin.settings

Acceptance Criteria

Profile Settings

  • Admin name
  • Email
  • Password change
  • Preferred language

Email Settings

  • Display current sender email from config('mail.from.address')
  • Display current sender name from config('mail.from.name')
  • "Send Test Email" button that sends a test email to admin's email address
  • Success/error feedback after test email attempt

Notification Preferences (Future Enhancement - Not in Scope)

Note: The following are documented for future consideration but are NOT required for this story's completion. All admin notifications are currently mandatory per PRD.

  • Toggle admin notifications (future)
  • Summary email frequency (future)

Behavior

  • Settings saved and applied immediately
  • Validation for all inputs
  • Flash messages for success/error states
  • Password fields cleared after successful update

Technical Notes

Database Migration Required

Add preferred_language column to users table:

// database/migrations/xxxx_add_preferred_language_to_users_table.php
Schema::table('users', function (Blueprint $table) {
    $table->string('preferred_language', 2)->default('ar')->after('remember_token');
});

Files to Create/Modify

File Action Purpose
resources/views/livewire/admin/settings.blade.php Create Main settings Volt component
app/Mail/TestEmail.php Create Test email mailable
resources/views/emails/test.blade.php Create Test email template
database/migrations/xxxx_add_preferred_language_to_users_table.php Create Migration
routes/web.php Modify Add admin settings route
app/Models/User.php Modify Add preferred_language to fillable

TestEmail Mailable

// app/Mail/TestEmail.php
namespace App\Mail;

use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;

class TestEmail extends Mailable
{
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: __('messages.test_email_subject'),
        );
    }

    public function content(): Content
    {
        return new Content(
            view: 'emails.test',
        );
    }
}

Volt Component Structure

<?php
// resources/views/livewire/admin/settings.blade.php

use App\Mail\TestEmail;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Rule;
use Livewire\Volt\Component;

new class extends Component {
    public string $name = '';
    public string $email = '';
    public string $current_password = '';
    public string $password = '';
    public string $password_confirmation = '';
    public string $preferred_language = 'ar';

    public function mount(): void
    {
        $user = auth()->user();
        $this->name = $user->name;
        $this->email = $user->email;
        $this->preferred_language = $user->preferred_language ?? 'ar';
    }

    public function updateProfile(): void
    {
        $this->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', Rule::unique('users')->ignore(auth()->id())],
            'preferred_language' => ['required', 'in:ar,en'],
        ]);

        auth()->user()->update([
            'name' => $this->name,
            'email' => $this->email,
            'preferred_language' => $this->preferred_language,
        ]);

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

    public function updatePassword(): void
    {
        $this->validate([
            'current_password' => ['required', 'current_password'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);

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

        $this->reset(['current_password', 'password', 'password_confirmation']);
        session()->flash('success', __('messages.password_updated'));
    }

    public function sendTestEmail(): void
    {
        try {
            Mail::to(auth()->user())->send(new TestEmail());
            session()->flash('success', __('messages.test_email_sent'));
        } catch (\Exception $e) {
            session()->flash('error', __('messages.test_email_failed'));
        }
    }
}; ?>

<div>
    {{-- UI Template Here --}}
</div>

Edge Cases & Error Handling

  • Wrong current password: Validation rule current_password handles this automatically
  • Duplicate email: Rule::unique with ignore(auth()->id()) prevents self-collision
  • Email send failure: Wrap in try/catch, show user-friendly error message
  • Empty preferred_language: Default to 'ar' in mount() if null

Testing Requirements

Test File

tests/Feature/Admin/SettingsTest.php

Test Scenarios

Profile Update Tests:

test('admin can view settings page', function () {
    $admin = User::factory()->create();

    $this->actingAs($admin)
        ->get(route('admin.settings'))
        ->assertOk()
        ->assertSeeLivewire('admin.settings');
});

test('admin can update profile information', function () {
    $admin = User::factory()->create();

    Volt::test('admin.settings')
        ->actingAs($admin)
        ->set('name', 'Updated Name')
        ->set('email', 'updated@example.com')
        ->set('preferred_language', 'en')
        ->call('updateProfile')
        ->assertHasNoErrors();

    expect($admin->fresh())
        ->name->toBe('Updated Name')
        ->email->toBe('updated@example.com')
        ->preferred_language->toBe('en');
});

test('profile update validates required fields', function () {
    $admin = User::factory()->create();

    Volt::test('admin.settings')
        ->actingAs($admin)
        ->set('name', '')
        ->set('email', '')
        ->call('updateProfile')
        ->assertHasErrors(['name', 'email']);
});

test('profile update prevents duplicate email', function () {
    $existingUser = User::factory()->create(['email' => 'taken@example.com']);
    $admin = User::factory()->create();

    Volt::test('admin.settings')
        ->actingAs($admin)
        ->set('email', 'taken@example.com')
        ->call('updateProfile')
        ->assertHasErrors(['email']);
});

Password Update Tests:

test('admin can update password with correct current password', function () {
    $admin = User::factory()->create([
        'password' => Hash::make('old-password'),
    ]);

    Volt::test('admin.settings')
        ->actingAs($admin)
        ->set('current_password', 'old-password')
        ->set('password', 'new-password')
        ->set('password_confirmation', 'new-password')
        ->call('updatePassword')
        ->assertHasNoErrors();

    expect(Hash::check('new-password', $admin->fresh()->password))->toBeTrue();
});

test('password update fails with wrong current password', function () {
    $admin = User::factory()->create([
        'password' => Hash::make('correct-password'),
    ]);

    Volt::test('admin.settings')
        ->actingAs($admin)
        ->set('current_password', 'wrong-password')
        ->set('password', 'new-password')
        ->set('password_confirmation', 'new-password')
        ->call('updatePassword')
        ->assertHasErrors(['current_password']);
});

test('password update requires confirmation match', function () {
    $admin = User::factory()->create([
        'password' => Hash::make('old-password'),
    ]);

    Volt::test('admin.settings')
        ->actingAs($admin)
        ->set('current_password', 'old-password')
        ->set('password', 'new-password')
        ->set('password_confirmation', 'different-password')
        ->call('updatePassword')
        ->assertHasErrors(['password']);
});

Test Email Tests:

test('admin can send test email', function () {
    Mail::fake();
    $admin = User::factory()->create();

    Volt::test('admin.settings')
        ->actingAs($admin)
        ->call('sendTestEmail')
        ->assertHasNoErrors();

    Mail::assertSent(TestEmail::class, fn ($mail) =>
        $mail->hasTo($admin->email)
    );
});

test('test email failure shows error message', function () {
    Mail::fake();
    Mail::shouldReceive('to->send')->andThrow(new \Exception('SMTP error'));

    $admin = User::factory()->create();

    Volt::test('admin.settings')
        ->actingAs($admin)
        ->call('sendTestEmail')
        ->assertSessionHas('error');
});

Definition of Done

  • Migration created and run for preferred_language column
  • User model updated with preferred_language in fillable
  • Settings Volt component created at resources/views/livewire/admin/settings.blade.php
  • TestEmail mailable created at app/Mail/TestEmail.php
  • Test email template created at resources/views/emails/test.blade.php
  • Route added: Route::get('/admin/settings', ...)->name('admin.settings')
  • Profile update works with validation
  • Password change works with current password verification
  • Language preference persists across sessions
  • Test email sends successfully (or shows error on failure)
  • Email settings display current sender info from config
  • All flash messages display correctly (success/error)
  • UI follows Flux UI component patterns
  • All tests pass (php artisan test --filter=SettingsTest)
  • Code formatted with Pint

Estimation

Complexity: Medium | Effort: 3-4 hours


Dev Agent Record

Status

Ready for Review

Agent Model Used

Claude Opus 4.5

File List

File Action Purpose
app/Mail/TestEmail.php Created Test email mailable class
resources/views/emails/test.blade.php Created Test email template
resources/views/livewire/settings/profile.blade.php Modified Added preferred_language and test email (admin-only)
lang/en/admin.php Modified Added system settings translations
lang/ar/admin.php Modified Added system settings translations (Arabic)
lang/en/emails.php Modified Added test email translations
lang/ar/emails.php Modified Added test email translations (Arabic)
tests/Feature/Settings/ProfileUpdateTest.php Modified Added preferred_language and test email tests

Completion Notes

  • Migration for preferred_language already existed in base users table migration
  • User model already had preferred_language in fillable array
  • All tests pass covering profile update, preferred language, and test email functionality
  • Used Flux UI components for consistent design
  • Bilingual support (AR/EN) implemented for all user-facing strings
  • Refactored: Consolidated into existing /settings/profile instead of creating duplicate /admin/settings

Change Log

Date Change Reason
2025-12-28 Initial implementation Story 6.8 development
2025-12-28 Refactored to use /settings/profile QA review identified duplication with existing settings pages

QA Results

Review Date: 2025-12-28

Reviewed By: Quinn (Test Architect)

Risk Assessment

  • Risk Level: Low
  • Escalation Triggers: None
  • Review Depth: Standard

Code Quality Assessment

Overall Assessment: EXCELLENT

After refactoring, the implementation extends the existing /settings/profile component cleanly:

  1. Volt Component (resources/views/livewire/settings/profile.blade.php):

    • Extended existing component with preferred_language field
    • Test email section added with @if(auth()->user()->isAdmin()) guard
    • Exception handling for email sending with user-friendly error messages
    • No code duplication
  2. TestEmail Mailable (app/Mail/TestEmail.php):

    • Follows Laravel mailable conventions
    • Properly accepts locale parameter for bilingual support
    • Clean envelope/content separation
  3. Email Template (resources/views/emails/test.blade.php):

    • Proper RTL support for Arabic locale
    • Uses markdown mail component
  4. Translations:

    • Complete bilingual support (AR/EN) in both lang/*/admin.php and lang/*/emails.php

Requirements Traceability (Given-When-Then)

AC Requirement Test Coverage Status
Profile Settings - Name User can update name test profile information can be updated
Profile Settings - Email User can update email test profile information can be updated
Profile Settings - Language User can set preferred language test user can update preferred language, test preferred language validates allowed values
Email Settings - Sender Display Admin sees current sender from config test admin sees email configuration section
Email Settings - Test Email Admin can send test email test admin can send test email, test test email uses admin preferred language
Access Control Test email is admin-only test non-admin cannot send test email

Refactoring Performed

ARCH-001 Resolution: Consolidated settings into existing /settings/profile

Action File Change
Modified resources/views/livewire/settings/profile.blade.php Added preferred_language and test email (admin-only)
Deleted resources/views/livewire/admin/settings.blade.php Removed duplicate component
Modified routes/web.php Removed admin.settings route
Deleted tests/Feature/Admin/SettingsTest.php Removed, tests consolidated
Modified tests/Feature/Settings/ProfileUpdateTest.php Added new feature tests

Compliance Check

  • Coding Standards: ✓ Pint passes with no issues
  • Project Structure: ✓ Extends existing component, no duplication
  • Testing Strategy: ✓ 12 tests in ProfileUpdateTest covering all acceptance criteria
  • All ACs Met: ✓ All acceptance criteria are implemented and tested

Improvements Checklist

  • Preferred language field added to profile
  • Test email section (admin-only) added
  • No code duplication
  • All tests pass
  • Bilingual support complete (AR/EN)
  • Flux UI components used consistently

Security Review

  • Authentication: ✓ Profile page protected by auth and active middleware
  • Authorization: ✓ Test email method checks isAdmin() before sending
  • Input Validation: ✓ All inputs properly validated
  • No Security Concerns Found

Performance Considerations

  • No performance issues identified
  • Component is lightweight with minimal database queries

Gate Status

Gate: PASS → docs/qa/gates/6.8-system-settings.yml

Ready for Done - Architectural concern resolved through refactoring. All acceptance criteria met with clean, non-duplicated implementation.