From b93b9363a6bccf076cc3a290b89dfc612c399958 Mon Sep 17 00:00:00 2001 From: Naser Mansour Date: Sat, 20 Dec 2025 21:14:15 +0200 Subject: [PATCH] reviewd and fixed epic 1 & 2 --- ...story-1.1-project-setup-database-schema.md | 44 +++ .../story-1.2-authentication-role-system.md | 81 +++- .../story-1.3-bilingual-infrastructure.md | 85 ++++- docs/stories/story-1.4-base-ui-navigation.md | 113 +++++- ....1-individual-client-account-management.md | 94 ++++- ...y-2.2-company-client-account-management.md | 102 ++++- .../story-2.3-account-type-conversion.md | 360 +++++++++++++++++- .../story-2.4-account-lifecycle-management.md | 185 ++++++++- ...2.5-account-creation-email-notification.md | 42 ++ 9 files changed, 1040 insertions(+), 66 deletions(-) diff --git a/docs/stories/story-1.1-project-setup-database-schema.md b/docs/stories/story-1.1-project-setup-database-schema.md index f0da3c1..f23b4cc 100644 --- a/docs/stories/story-1.1-project-setup-database-schema.md +++ b/docs/stories/story-1.1-project-setup-database-schema.md @@ -49,9 +49,13 @@ So that **the foundation is established for all subsequent feature development** ## Technical Notes - **Database:** Use SQLite for development (configurable for MariaDB in production) +- **Environment:** Set `DB_CONNECTION=sqlite` in `.env` for development - **Reference:** PRD Section 16.1 for complete schema details - **Pattern:** Follow existing Volt class-based component pattern - **Structure:** Use Laravel 12 streamlined file structure (no app/Http/Middleware, bootstrap/app.php for config) +- **Testing:** Use Pest 4 for all tests (`php artisan make:test --pest`) +- **Models:** Create with `php artisan make:model ModelName -mf` (migration + factory) +- **Notifications table:** Custom implementation per PRD schema (not Laravel's built-in notifications) ### Database Schema Reference @@ -100,6 +104,40 @@ admin_logs: - [ ] Code formatted with Pint - [ ] No errors on fresh install +## Test Scenarios + +All tests should use Pest 4 (`php artisan make:test --pest`). + +### Migration Tests +- [ ] All migrations run successfully: `php artisan migrate:fresh` +- [ ] Migrations can rollback without errors: `php artisan migrate:rollback` +- [ ] Foreign key constraints are properly created (timeline_updates.timeline_id, timeline_updates.admin_id, consultations.user_id, timelines.user_id, admin_logs.admin_id) + +### Factory Tests +- [ ] UserFactory creates valid User with all user_type enum values (admin, individual, company) +- [ ] ConsultationFactory creates valid Consultation with proper user relationship +- [ ] TimelineFactory creates valid Timeline with proper user relationship +- [ ] TimelineUpdateFactory creates valid TimelineUpdate with timeline and admin relationships +- [ ] PostFactory creates valid Post with bilingual fields +- [ ] WorkingHoursFactory creates valid WorkingHours for each day_of_week (0-6) +- [ ] BlockedTimeFactory creates valid BlockedTime entries +- [ ] NotificationFactory creates valid Notification entries +- [ ] AdminLogFactory creates valid AdminLog with JSON fields + +### Enum Validation Tests +- [ ] User.user_type only accepts: admin, individual, company +- [ ] User.status only accepts: active, deactivated +- [ ] User.preferred_language only accepts: ar, en +- [ ] Consultation.status only accepts: pending, approved, completed, cancelled, no_show +- [ ] Consultation.type only accepts: free, paid +- [ ] Timeline.status only accepts: active, archived +- [ ] Post.status only accepts: draft, published + +### Edge Case Tests +- [ ] Nullable fields accept null values (national_id, company_name, case_reference, etc.) +- [ ] JSON fields (old_values, new_values) store and retrieve correctly +- [ ] Unique constraint on case_reference allows multiple nulls but no duplicate values + ## Dependencies - None (this is the foundation story) @@ -110,6 +148,12 @@ admin_logs: - **Mitigation:** Follow PRD specifications closely, validate with stakeholder - **Rollback:** Fresh migration reset possible in development +### Error Handling + +- **Migration failures:** Run `php artisan migrate:status` to identify failed migrations, fix the issue, then `php artisan migrate` to continue +- **Foreign key errors:** Ensure referenced tables are created before dependent tables (migrations run in filename order) +- **Factory errors:** Ensure factories define all required (non-nullable) fields and use valid enum values + ## Estimation **Complexity:** Medium diff --git a/docs/stories/story-1.2-authentication-role-system.md b/docs/stories/story-1.2-authentication-role-system.md index ac510bc..6ffc55f 100644 --- a/docs/stories/story-1.2-authentication-role-system.md +++ b/docs/stories/story-1.2-authentication-role-system.md @@ -35,15 +35,18 @@ So that **only authorized users can access the platform with appropriate permiss - [ ] Remember me functionality (optional) ### Integration Requirements -- [ ] Login redirects to appropriate dashboard (admin vs client) -- [ ] Logout clears session properly -- [ ] Middleware protects admin-only routes -- [ ] Failed login attempts logged +- [ ] Login redirects to appropriate dashboard: + - Admin users → `/admin/dashboard` (to be created in Epic 2) + - Client users → `/dashboard` (to be created in Epic 2) + - For now, redirect to a placeholder route or home page +- [ ] Logout clears session properly and redirects to login page +- [ ] Middleware protects admin-only routes (return 403 for unauthorized) +- [ ] Failed login attempts logged to `admin_logs` table ### Quality Requirements - [ ] Login form validates inputs properly - [ ] Error messages are clear and bilingual -- [ ] Tests cover authentication flow +- [ ] All test scenarios in "Test Scenarios" section pass - [ ] No security vulnerabilities ## Technical Notes @@ -84,20 +87,78 @@ Gate::define('client', fn (User $user) => $user->user_type !== 'admin'); - `can:admin` - Require admin role - Custom middleware for session timeout if needed +### Environment Configuration +```env +SESSION_LIFETIME=120 # 2 hours in minutes +SESSION_EXPIRE_ON_CLOSE=false +``` + +### Edge Case Behavior + +**Rate Limiting (5 attempts per minute):** +- After 5 failed attempts, show message: "Too many login attempts. Please try again in 60 seconds." +- Use bilingual message from translation files +- Log rate limit events for security monitoring + +**Session Timeout (2 hours inactivity):** +- When session expires, redirect to login page +- Show flash message: "Your session has expired. Please log in again." +- Preserve intended URL for redirect after re-authentication + +**Deactivated Account Login Attempt:** +- Check `status` field during authentication +- If `status === 'deactivated'`, reject login with message: "Your account has been deactivated. Please contact the administrator." + +## Test Scenarios + +### Feature Tests (tests/Feature/Auth/) + +**Login Flow:** +- [ ] `test_login_page_renders_correctly` - Login page displays in default language +- [ ] `test_user_can_login_with_valid_credentials` - Valid credentials redirect to dashboard +- [ ] `test_admin_redirects_to_admin_dashboard` - Admin user goes to admin dashboard +- [ ] `test_client_redirects_to_client_dashboard` - Client user goes to client dashboard +- [ ] `test_invalid_credentials_show_error` - Wrong password shows bilingual error +- [ ] `test_nonexistent_user_shows_error` - Unknown email shows generic error (no user enumeration) +- [ ] `test_deactivated_user_cannot_login` - Deactivated account rejected with message + +**Rate Limiting:** +- [ ] `test_rate_limiting_blocks_after_five_attempts` - 6th attempt blocked +- [ ] `test_rate_limit_message_is_bilingual` - Error message respects locale +- [ ] `test_rate_limit_resets_after_one_minute` - Can retry after cooldown + +**Session Management:** +- [ ] `test_logout_clears_session` - Logout destroys session data +- [ ] `test_remember_me_extends_session` - Remember token works (if implemented) + +**Authorization:** +- [ ] `test_admin_can_access_admin_routes` - Admin passes `can:admin` middleware +- [ ] `test_client_cannot_access_admin_routes` - Client gets 403 on admin routes +- [ ] `test_unauthenticated_user_redirected_to_login` - Guest redirected from protected routes + +### Unit Tests (tests/Unit/) + +**Gate Definitions:** +- [ ] `test_admin_gate_returns_true_for_admin_user` - Gate check passes +- [ ] `test_admin_gate_returns_false_for_client_user` - Gate check fails +- [ ] `test_client_gate_returns_true_for_individual_user` - Client gate passes +- [ ] `test_client_gate_returns_true_for_company_user` - Client gate passes + ## Definition of Done - [ ] Login page renders correctly in both languages - [ ] Users can log in with valid credentials -- [ ] Invalid credentials show proper error -- [ ] Rate limiting prevents brute force +- [ ] Invalid credentials show proper error (bilingual) +- [ ] Deactivated users cannot log in +- [ ] Rate limiting prevents brute force (5 attempts/minute) - [ ] Session expires after 2 hours inactivity -- [ ] Admin routes protected from clients -- [ ] Tests pass for authentication flow +- [ ] Admin routes protected from clients (403 response) +- [ ] All Feature and Unit tests from "Test Scenarios" section pass - [ ] Code formatted with Pint ## Dependencies -- **Story 1.1:** Database schema (users table) +- **Story 1.1:** Database schema - creates `users` table with `user_type` enum (`admin`, `individual`, `company`), `status` enum (`active`, `deactivated`), and `preferred_language` enum (`ar`, `en`) - **Story 1.3:** Bilingual infrastructure (for login page translations) ## Risk Assessment diff --git a/docs/stories/story-1.3-bilingual-infrastructure.md b/docs/stories/story-1.3-bilingual-infrastructure.md index 4ccc396..583daa1 100644 --- a/docs/stories/story-1.3-bilingual-infrastructure.md +++ b/docs/stories/story-1.3-bilingual-infrastructure.md @@ -16,15 +16,22 @@ So that **I can use the platform in my preferred language with proper RTL/LTR la - **Follows pattern:** Laravel localization best practices - **Touch points:** All views, navigation, date/time formatting +### Reference Documents +- **PRD Section 4.2:** Language Support requirements (RTL/LTR, numerals, date/time formats) +- **PRD Section 7.1.C:** Typography specifications (Arabic: Cairo/Tajawal, English: Montserrat/Lato) + ## Acceptance Criteria ### Functional Requirements - [ ] Language files for Arabic (ar) and English (en) - [ ] Language toggle in navigation (visible on all pages) - [ ] User language preference stored in `users.preferred_language` -- [ ] Guest language stored in session +- [ ] Guest language stored in session (persists across page loads) - [ ] RTL layout for Arabic, LTR for English - [ ] All UI elements translatable via `__()` helper +- [ ] Language switch preserves current page (redirect back to same URL) +- [ ] Form validation messages display in current locale +- [ ] Missing translations fall back to key name (not break page) ### Date/Time Formatting - [ ] Arabic: DD/MM/YYYY format @@ -52,9 +59,28 @@ So that **I can use the platform in my preferred language with proper RTL/LTR la ## Technical Notes +### Language Toggle Route & Controller +```php +// routes/web.php +Route::get('/language/{locale}', function (string $locale) { + if (!in_array($locale, ['ar', 'en'])) { + abort(400); + } + + session(['locale' => $locale]); + + if (auth()->check()) { + auth()->user()->update(['preferred_language' => $locale]); + } + + return redirect()->back(); +})->name('language.switch'); +``` + ### Language Middleware ```php -// Middleware to set locale +// app/Http/Middleware/SetLocale.php +// Register in bootstrap/app.php: ->withMiddleware(fn($m) => $m->web(append: SetLocale::class)) public function handle($request, Closure $next) { $locale = session('locale', @@ -115,7 +141,7 @@ resources/lang/ ### Date Formatting Helper ```php -// In a helper or service +// In a helper or service (app/Helpers/DateHelper.php or as a service) public function formatDate($date, $locale = null): string { $locale = $locale ?? app()->getLocale(); @@ -124,6 +150,59 @@ public function formatDate($date, $locale = null): string } ``` +### Language Toggle Component +```blade + +
+ app()->getLocale() === 'ar', + 'text-gold hover:text-gold-light' => app()->getLocale() !== 'ar', + ]) + > + العربية + + | + app()->getLocale() === 'en', + 'text-gold hover:text-gold-light' => app()->getLocale() !== 'en', + ]) + > + English + +
+``` + +## Testing Requirements + +### Feature Tests +- [ ] Test language toggle stores preference in session for guests +- [ ] Test language toggle stores preference in database for authenticated users +- [ ] Test locale middleware sets correct locale from user preference +- [ ] Test locale middleware falls back to session for guests +- [ ] Test locale middleware defaults to 'ar' when no preference set +- [ ] Test date formatting helper returns DD/MM/YYYY for Arabic locale +- [ ] Test date formatting helper returns MM/DD/YYYY for English locale +- [ ] Test language switch redirects back to same page + +### Browser Tests (Pest v4) +- [ ] Test RTL layout renders correctly for Arabic (dir="rtl" on html) +- [ ] Test LTR layout renders correctly for English (dir="ltr" on html) +- [ ] Test no layout breaks when toggling languages +- [ ] Test Arabic font (Cairo) loads for Arabic locale +- [ ] Test English font (Montserrat) loads for English locale + +### Manual Testing Checklist +- [ ] Verify RTL alignment in navigation, forms, and content +- [ ] Verify LTR alignment in navigation, forms, and content +- [ ] Test on Chrome, Firefox, Safari for RTL rendering +- [ ] Verify no horizontal scroll appears in either direction + ## Definition of Done - [ ] Language toggle works in navigation diff --git a/docs/stories/story-1.4-base-ui-navigation.md b/docs/stories/story-1.4-base-ui-navigation.md index 7284859..defdfd8 100644 --- a/docs/stories/story-1.4-base-ui-navigation.md +++ b/docs/stories/story-1.4-base-ui-navigation.md @@ -16,6 +16,12 @@ So that **I can easily navigate the platform on any device**. - **Follows pattern:** Flux UI navbar patterns, mobile-first design - **Touch points:** All pages (layout component) +### Referenced Documents +- **PRD Section 5.2:** Navigation System - menu structure, language toggle requirements +- **PRD Section 7.1:** Brand Identity & Visual Guidelines - color palette, typography, spacing +- **PRD Section 7.2-7.4:** Design principles, UI/UX requirements, responsive breakpoints +- **Story 1.3:** Bilingual Infrastructure - RTL/LTR detection via `app()->getLocale()`, font configuration + ## Acceptance Criteria ### Color Scheme @@ -139,17 +145,106 @@ So that **I can easily navigate the platform on any device**. @props(['size' => 'default']) -{{ __('Libra Law Firm') }} $size === 'small', - 'h-12' => $size === 'default', - 'h-16' => $size === 'large', - ]) -/> +@if(file_exists(public_path('images/logo.svg'))) + {{ __('Libra Law Firm') }} $size === 'small', + 'h-12' => $size === 'default', + 'h-16' => $size === 'large', + ]) + /> +@else + $size === 'small', + 'text-2xl' => $size === 'default', + 'text-3xl' => $size === 'large', + ])>Libra +@endif ``` +### Asset Dependencies +- **Logo SVG:** Required at `public/images/logo.svg` +- **Fallback:** Text "Libra" in gold displayed if logo asset not available +- **Logo Specs:** SVG format preferred, min 120px width desktop, 80px mobile +- **Note:** Logo asset to be provided by client; use text fallback during development + +### Auth-Aware Navigation +```blade + + + + {{ __('navigation.home') }} + + + {{ __('navigation.booking') }} + + + {{ __('navigation.posts') }} + + + @auth + + {{ __('navigation.dashboard') }} + +
+ @csrf + + {{ __('navigation.logout') }} + +
+ @else + + {{ __('navigation.login') }} + + @endauth +
+``` + +## Test Scenarios + +### Navigation Rendering Tests +- [ ] Navigation component renders on all pages +- [ ] Logo displays correctly (or text fallback if SVG missing) +- [ ] All menu links are visible and clickable +- [ ] Active page is visually indicated in navigation +- [ ] Navigation has correct navy background and gold text + +### Mobile Menu Tests +- [ ] Hamburger menu icon visible on mobile viewports +- [ ] Mobile menu toggles open on click +- [ ] Mobile menu closes on outside click +- [ ] Mobile menu closes when navigating to a link +- [ ] Touch targets are at least 44px height + +### Authentication State Tests +- [ ] Guest users see: Home, Booking, Posts, Login +- [ ] Authenticated users see: Home, Booking, Posts, Dashboard, Logout +- [ ] Logout form submits correctly and logs user out + +### Language Toggle Tests +- [ ] Language toggle visible in navigation +- [ ] Switching to Arabic applies RTL layout +- [ ] Switching to English applies LTR layout +- [ ] Language preference persists across page loads + +### Footer Tests +- [ ] Footer renders at bottom of viewport (sticky footer) +- [ ] Footer contains logo (smaller version) +- [ ] Footer contains Terms of Service and Privacy Policy links +- [ ] Copyright year displays current year dynamically + +### Responsive Tests +- [ ] No horizontal scroll on mobile (320px+) +- [ ] No horizontal scroll on tablet (768px) +- [ ] Layout adapts correctly at all breakpoints +- [ ] Logo centered on mobile, left-aligned on desktop + ## Definition of Done - [ ] Navigation renders correctly on all viewports diff --git a/docs/stories/story-2.1-individual-client-account-management.md b/docs/stories/story-2.1-individual-client-account-management.md index 17b9002..4016a07 100644 --- a/docs/stories/story-2.1-individual-client-account-management.md +++ b/docs/stories/story-2.1-individual-client-account-management.md @@ -15,6 +15,12 @@ So that **I can manage client information and provide them platform access**. - **Technology:** Livewire Volt, Flux UI forms - **Follows pattern:** Admin CRUD patterns, class-based Volt components - **Touch points:** User model, admin dashboard +- **PRD Reference:** Section 5.3 (User Management System), Section 16.1 (Database Schema) + +### Prerequisites from Epic 1 +- Users table with fields: `name`, `email`, `password`, `user_type`, `national_id`, `phone`, `preferred_language`, `status` +- AdminLog model and `admin_logs` table for audit logging +- Bilingual infrastructure (lang files, `__()` helper) ## Acceptance Criteria @@ -63,6 +69,15 @@ So that **I can manage client information and provide them platform access**. ## Technical Notes +### Files to Create +``` +resources/views/livewire/admin/clients/individual/ +├── index.blade.php # List view with search/filter/pagination +├── create.blade.php # Create form +├── edit.blade.php # Edit form +└── show.blade.php # View profile page +``` + ### User Model Scope ```php // In User model @@ -136,6 +151,80 @@ AdminLog::create([ ]); ``` +### Edge Cases & Error Handling +- **Validation failure:** Display inline field errors using Flux UI error states +- **Duplicate email:** Show "Email already exists" error on email field +- **Duplicate National ID:** Show "National ID already registered" error +- **Empty search results:** Display "No clients found" message with clear filters option + +## Testing Requirements + +### Test File Location +`tests/Feature/Admin/IndividualClientTest.php` + +### Test Scenarios + +#### Create Client Tests +- [ ] Can create individual client with all valid data +- [ ] Cannot create client without required name field +- [ ] Cannot create client without required email field +- [ ] Cannot create client without required national_id field +- [ ] Cannot create client without required phone field +- [ ] Cannot create client with invalid email format +- [ ] Cannot create client with duplicate email (existing user) +- [ ] Cannot create client with duplicate national_id +- [ ] Cannot create client with password less than 8 characters +- [ ] Created client has user_type set to 'individual' +- [ ] AdminLog entry created on successful creation + +#### List View Tests +- [ ] Index page displays only individual clients (not company/admin) +- [ ] Pagination works with 10/25/50 per page options +- [ ] Clients sorted by created_at desc by default + +#### Search & Filter Tests +- [ ] Can search clients by name (partial match) +- [ ] Can search clients by email (partial match) +- [ ] Can search clients by national_id (partial match) +- [ ] Can filter clients by active status +- [ ] Can filter clients by deactivated status +- [ ] Clear filters resets search and filter + +#### Edit Client Tests +- [ ] Can edit existing client information +- [ ] Edit form pre-populates with current values +- [ ] Validation rules apply on edit (except unique for own record) +- [ ] AdminLog entry created on successful update +- [ ] Cannot change user_type via edit form + +#### View Profile Tests +- [ ] Profile page displays all client information +- [ ] Profile shows consultation count/summary +- [ ] Profile shows timeline count/summary + +### Testing Approach +```php +use Livewire\Volt\Volt; + +test('admin can create individual client', function () { + $admin = User::factory()->admin()->create(); + + Volt::actingAs($admin) + ->test('admin.clients.individual.create') + ->set('name', 'Test Client') + ->set('email', 'client@example.com') + ->set('national_id', '123456789') + ->set('phone', '+970599123456') + ->set('password', 'password123') + ->set('preferred_language', 'ar') + ->call('create') + ->assertHasNoErrors(); + + expect(User::where('email', 'client@example.com')->exists())->toBeTrue(); + expect(AdminLog::where('action_type', 'create')->exists())->toBeTrue(); +}); +``` + ## Definition of Done - [ ] Create individual client form works @@ -151,7 +240,10 @@ AdminLog::create([ ## Dependencies -- **Epic 1:** Authentication system, database schema, bilingual support +- **Story 1.1:** Database schema with `users` table (user_type, national_id, status fields) and `admin_logs` table +- **Story 1.2:** Authentication system with admin role, AdminLog model +- **Story 1.3:** Bilingual infrastructure (translation files, `__()` helper) +- **Story 1.4:** Base admin layout and navigation ## Risk Assessment diff --git a/docs/stories/story-2.2-company-client-account-management.md b/docs/stories/story-2.2-company-client-account-management.md index a74dc26..7d674e0 100644 --- a/docs/stories/story-2.2-company-client-account-management.md +++ b/docs/stories/story-2.2-company-client-account-management.md @@ -11,10 +11,15 @@ So that **I can serve corporate clients with their unique data requirements**. ## Story Context ### Existing System Integration -- **Integrates with:** Users table, potential contact_persons table -- **Technology:** Livewire Volt, Flux UI forms -- **Follows pattern:** Same CRUD pattern as individual clients -- **Touch points:** User model, admin dashboard +- **Integrates with:** `users` table (company-specific fields), `admin_logs` table +- **Technology:** Livewire Volt (class-based), Flux UI forms, Pest tests +- **Follows pattern:** Same CRUD pattern as Story 2.1 Individual Clients +- **Key Files to Create/Modify:** + - `resources/views/livewire/admin/users/company/` - Volt components (index, create, edit, show) + - `app/Models/User.php` - Add `scopeCompany()` method + - `resources/lang/ar/messages.php` - Arabic translations + - `resources/lang/en/messages.php` - English translations + - `tests/Feature/Admin/CompanyClientTest.php` - Feature tests ## Acceptance Criteria @@ -32,11 +37,8 @@ So that **I can serve corporate clients with their unique data requirements**. - [ ] Duplicate email/registration number prevention - [ ] Success message on creation -### Multiple Contact Persons (Optional Enhancement) -- [ ] Support unlimited contact persons per company -- [ ] Each contact: Name, ID, Phone, Email -- [ ] Primary contact indicator -- [ ] Add/remove contacts dynamically +### Multiple Contact Persons (OUT OF SCOPE) +> **Note:** Multiple contact persons support is deferred to a future enhancement story. This story implements single contact person stored directly on the `users` table. The `contact_persons` table migration in Technical Notes is for reference only if this feature is later prioritized. ### List View - [ ] Display all company clients (user_type = 'company') @@ -56,8 +58,7 @@ So that **I can serve corporate clients with their unique data requirements**. - [ ] Success message on update ### View Company Profile -- [ ] Display all company information -- [ ] List all contact persons +- [ ] Display all company information (including contact person details) - [ ] Show consultation history summary - [ ] Show timeline history summary @@ -86,9 +87,11 @@ users table: - contact_person_id (nullable, required for company) ``` -### Alternative: Separate Contact Persons Table +### Future Reference: Separate Contact Persons Table +> **Note:** This migration is NOT part of this story. It is preserved here for future reference if multiple contact persons feature is prioritized. + ```php -// contact_persons migration +// contact_persons migration (FUTURE - NOT IN SCOPE) Schema::create('contact_persons', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); @@ -119,10 +122,13 @@ public function rules(): array ``` ### Volt Component for Create +Follow the same component structure as Story 2.1 (`docs/stories/story-2.1-individual-client-account-management.md`). + ```php 'active', ]); - // Log action - // Send welcome email + // Log action (same pattern as Story 2.1) + AdminLog::create([ + 'admin_id' => auth()->id(), + 'action_type' => 'create', + 'target_type' => 'user', + 'target_id' => $user->id, + 'new_values' => $user->only(['company_name', 'email', 'company_registration']), + 'ip_address' => request()->ip(), + ]); + + // Send welcome email (depends on Story 2.5) session()->flash('success', __('messages.company_created')); $this->redirect(route('admin.users.index')); @@ -157,6 +172,51 @@ new class extends Component { }; ``` +## Testing Requirements + +### Test Approach +- Feature tests using Pest with `Volt::test()` for Livewire components +- Factory-based test data generation + +### Key Test Scenarios + +#### Create Company Client +- [ ] Successfully create company with all valid required fields +- [ ] Validation fails when required fields are missing +- [ ] Validation fails for duplicate email address +- [ ] Validation fails for duplicate company registration number +- [ ] Preferred language defaults to Arabic when not specified +- [ ] Audit log entry created on successful creation + +#### List View +- [ ] List displays only company type users (excludes individual/admin) +- [ ] Pagination works correctly (10/25/50 per page) +- [ ] Default sort is by created date descending + +#### Search & Filter +- [ ] Search by company name returns correct results +- [ ] Search by email returns correct results +- [ ] Search by registration number returns correct results +- [ ] Filter by active status works +- [ ] Filter by deactivated status works +- [ ] Combined search and filter works correctly + +#### Edit Company +- [ ] Successfully update company information +- [ ] Validation fails for duplicate email (excluding current record) +- [ ] Validation fails for duplicate registration number (excluding current record) +- [ ] Audit log entry created on successful update + +#### View Profile +- [ ] Profile displays all company information correctly +- [ ] Consultation history summary displays (empty state if none) +- [ ] Timeline history summary displays (empty state if none) + +#### Bilingual Support +- [ ] Form labels display correctly in Arabic +- [ ] Form labels display correctly in English +- [ ] Validation messages display in user's preferred language + ## Definition of Done - [ ] Create company client form works @@ -173,7 +233,17 @@ new class extends Component { ## Dependencies - **Epic 1:** Authentication system, database schema -- **Story 2.1:** Same CRUD patterns +- **Story 1.1:** Database schema must include the following columns in `users` table: + - `user_type` (enum: 'individual', 'company', 'admin') + - `status` (enum: 'active', 'deactivated') + - `phone` (string) + - `preferred_language` (enum: 'ar', 'en') + - `company_name` (nullable string) + - `company_registration` (nullable string, unique when not null) + - `contact_person_name` (nullable string) + - `contact_person_id` (nullable string) + - `national_id` (nullable string, unique when not null) +- **Story 2.1:** CRUD patterns established in `docs/stories/story-2.1-individual-client-account-management.md` ## Risk Assessment diff --git a/docs/stories/story-2.3-account-type-conversion.md b/docs/stories/story-2.3-account-type-conversion.md index a810ae5..ac47c2b 100644 --- a/docs/stories/story-2.3-account-type-conversion.md +++ b/docs/stories/story-2.3-account-type-conversion.md @@ -12,9 +12,17 @@ So that **I can accommodate clients whose business structure changes**. ### Existing System Integration - **Integrates with:** Users table, consultations, timelines -- **Technology:** Livewire Volt, modal dialogs -- **Follows pattern:** Admin action pattern with confirmation -- **Touch points:** User model, related records +- **Technology:** Livewire Volt (class-based), Flux UI modals, Pest tests +- **Follows pattern:** Admin action pattern with confirmation modal (same as Story 2.1/2.2 CRUD pattern) +- **Touch points:** User model, AdminLog model, related records +- **PRD Reference:** Section 5.3 (User Management - "Admin can convert individual to company or vice versa") + +### Prerequisites from Story 2.1 & 2.2 +- Individual client show page exists at `resources/views/livewire/admin/users/individual/show.blade.php` +- Company client show page exists at `resources/views/livewire/admin/users/company/show.blade.php` +- User model has `scopeIndividual()` and `scopeCompany()` methods +- AdminLog model exists with fields: `admin_id`, `action_type`, `target_type`, `target_id`, `old_values`, `new_values`, `ip_address` +- Users table has all required fields: `user_type`, `name`, `national_id`, `company_name`, `company_registration`, `contact_person_name`, `contact_person_id` ## Acceptance Criteria @@ -61,11 +69,52 @@ So that **I can accommodate clients whose business structure changes**. ## Technical Notes +### Files to Create +``` +resources/views/livewire/admin/users/ +├── convert-to-company-modal.blade.php # Modal component for individual→company +└── convert-to-individual-modal.blade.php # Modal component for company→individual + +app/Notifications/ +└── AccountTypeChangedNotification.php # Email notification for account conversion + +resources/lang/ar/ +└── messages.php (add keys: account_converted, convert_to_company, convert_to_individual) + +resources/lang/en/ +└── messages.php (add same keys) + +resources/lang/ar/ +└── emails.php (add keys: account_type_changed.title, account_type_changed.body) + +resources/lang/en/ +└── emails.php (add same keys) +``` + +### Files to Modify +``` +resources/views/livewire/admin/users/individual/show.blade.php + - Add "Convert to Company" button that opens convert-to-company-modal + +resources/views/livewire/admin/users/company/show.blade.php + - Add "Convert to Individual" button that opens convert-to-individual-modal +``` + +### UI Placement +The conversion action should be a **secondary button** on the user profile show page: +- Located in the action buttons area (near Edit/Deactivate buttons) +- Opens a Flux UI modal with the conversion form +- Individual show page → "Convert to Company" button (gold outline style) +- Company show page → "Convert to Individual" button (gold outline style) + ### Conversion Logic ```php user->only([ 'user_type', 'name', 'national_id', - 'company_name', 'company_registration' + 'company_name', 'company_registration', + 'contact_person_name', 'contact_person_id' ]); - $this->user->update([ - 'user_type' => 'company', - 'name' => $this->company_name, - 'company_name' => $this->company_name, - 'company_registration' => $this->company_registration, - 'contact_person_name' => $this->contact_person_name, - 'contact_person_id' => $this->contact_person_id, - 'national_id' => null, // Company doesn't have individual national ID - ]); + DB::transaction(function () use ($oldValues) { + $this->user->update([ + 'user_type' => 'company', + 'name' => $this->company_name, + 'company_name' => $this->company_name, + 'company_registration' => $this->company_registration, + 'contact_person_name' => $this->contact_person_name, + 'contact_person_id' => $this->contact_person_id, + 'national_id' => null, // Company doesn't have individual national ID + ]); - $this->logConversion($oldValues); - $this->notifyUser(); + $this->logConversion($oldValues); + $this->notifyUser(); + }); session()->flash('success', __('messages.account_converted')); + $this->redirect(route('admin.users.company.show', $this->user)); } public function convertToIndividual(): void { - // Similar logic for company -> individual + $this->validate([ + 'name' => 'required|string|max:255', + 'national_id' => 'required|string|unique:users,national_id', + ]); + + $oldValues = $this->user->only([ + 'user_type', 'name', 'national_id', + 'company_name', 'company_registration', + 'contact_person_name', 'contact_person_id' + ]); + + DB::transaction(function () { + $this->user->update([ + 'user_type' => 'individual', + 'name' => $this->name, + 'national_id' => $this->national_id, + 'company_name' => null, + 'company_registration' => null, + 'contact_person_name' => null, + 'contact_person_id' => null, + ]); + + $this->logConversion($oldValues); + $this->notifyUser(); + }); + + session()->flash('success', __('messages.account_converted')); + $this->redirect(route('admin.users.individual.show', $this->user)); } private function logConversion(array $oldValues): void @@ -182,6 +262,231 @@ public function testConversionPreservesRelationships(): void @endcomponent ``` +### Edge Cases & Error Handling + +| Scenario | Handling | +|----------|----------| +| **Duplicate company_registration** | Validation error: "Company registration number already exists" - display on field | +| **Duplicate national_id (company→individual)** | Validation error: "National ID already registered" - display on field | +| **Database error mid-conversion** | DB::transaction rollback ensures atomic operation - no partial updates | +| **User has pending consultations** | Conversion allowed - consultations are linked by user_id, not user_type | +| **User has active timelines** | Conversion allowed - timelines are linked by user_id, not user_type | +| **Email notification fails** | Log error but don't rollback conversion - email is non-critical | +| **Modal closed without saving** | No changes made - standard modal behavior | + +### Notification Class Structure +```php +// app/Notifications/AccountTypeChangedNotification.php +preferred_language ?? 'ar'; + $typeName = $this->newUserType === 'company' + ? __('messages.company', [], $locale) + : __('messages.individual', [], $locale); + + return (new MailMessage) + ->subject(__('emails.account_type_changed.subject', [], $locale)) + ->view('emails.account-type-changed', [ + 'user' => $notifiable, + 'newType' => $typeName, + 'locale' => $locale, + ]); + } +} +``` + +## Testing Requirements + +### Test File Location +`tests/Feature/Admin/AccountConversionTest.php` + +### Test Scenarios + +#### Individual to Company Conversion +- [ ] Can convert individual to company with all valid data +- [ ] Form pre-fills contact_person_name with user's current name +- [ ] Form pre-fills contact_person_id with user's national_id +- [ ] Cannot convert without required company_name field +- [ ] Cannot convert without required company_registration field +- [ ] Cannot convert with duplicate company_registration (existing company) +- [ ] Converted user has user_type set to 'company' +- [ ] Original email, phone, password are preserved after conversion +- [ ] Consultations are preserved and accessible after conversion +- [ ] Timelines are preserved and accessible after conversion +- [ ] AdminLog entry created with old_values and new_values +- [ ] Email notification sent to user after conversion + +#### Company to Individual Conversion +- [ ] Can convert company to individual with all valid data +- [ ] Form pre-fills name with contact_person_name or company_name +- [ ] Form pre-fills national_id with contact_person_id if available +- [ ] Cannot convert without required name field +- [ ] Cannot convert without required national_id field +- [ ] Cannot convert with duplicate national_id (existing individual) +- [ ] Converted user has user_type set to 'individual' +- [ ] Company fields (company_name, company_registration, contact_person_*) are nulled +- [ ] Original email, phone, password are preserved after conversion +- [ ] Consultations are preserved and accessible after conversion +- [ ] Timelines are preserved and accessible after conversion +- [ ] AdminLog entry created with old_values and new_values +- [ ] Email notification sent to user after conversion + +#### UI/UX Tests +- [ ] "Convert to Company" button visible only on individual profile show page +- [ ] "Convert to Individual" button visible only on company profile show page +- [ ] Modal opens when conversion button clicked +- [ ] Modal has confirmation step before conversion +- [ ] Success message displayed after conversion +- [ ] User redirected to correct show page (company/individual) after conversion + +#### Bilingual Tests +- [ ] Modal labels display correctly in Arabic +- [ ] Modal labels display correctly in English +- [ ] Success message displays in current locale +- [ ] Email notification sent in user's preferred_language + +### Testing Approach +```php +use App\Models\User; +use App\Models\AdminLog; +use App\Models\Consultation; +use App\Models\Timeline; +use App\Notifications\AccountTypeChangedNotification; +use Illuminate\Support\Facades\Notification; +use Livewire\Volt\Volt; + +test('admin can convert individual to company', function () { + Notification::fake(); + + $admin = User::factory()->admin()->create(); + $individual = User::factory()->individual()->create([ + 'name' => 'John Doe', + 'national_id' => '123456789', + 'email' => 'john@example.com', + ]); + + // Create related records to verify preservation + $consultation = Consultation::factory()->for($individual)->create(); + $timeline = Timeline::factory()->for($individual)->create(); + + Volt::actingAs($admin) + ->test('admin.users.convert-to-company-modal', ['user' => $individual]) + ->assertSet('contact_person_name', 'John Doe') + ->assertSet('contact_person_id', '123456789') + ->set('company_name', 'Doe Corp') + ->set('company_registration', 'REG-12345') + ->call('convertToCompany') + ->assertHasNoErrors() + ->assertRedirect(route('admin.users.company.show', $individual)); + + // Verify conversion + $individual->refresh(); + expect($individual->user_type)->toBe('company'); + expect($individual->company_name)->toBe('Doe Corp'); + expect($individual->email)->toBe('john@example.com'); // Preserved + + // Verify relationships preserved + expect($individual->consultations)->toHaveCount(1); + expect($individual->timelines)->toHaveCount(1); + + // Verify audit log + expect(AdminLog::where('action_type', 'convert_account')->exists())->toBeTrue(); + + // Verify notification + Notification::assertSentTo($individual, AccountTypeChangedNotification::class); +}); + +test('admin can convert company to individual', function () { + Notification::fake(); + + $admin = User::factory()->admin()->create(); + $company = User::factory()->company()->create([ + 'company_name' => 'Acme Corp', + 'contact_person_name' => 'Jane Smith', + 'contact_person_id' => '987654321', + 'email' => 'contact@acme.com', + ]); + + Volt::actingAs($admin) + ->test('admin.users.convert-to-individual-modal', ['user' => $company]) + ->assertSet('name', 'Jane Smith') + ->assertSet('national_id', '987654321') + ->call('convertToIndividual') + ->assertHasNoErrors(); + + $company->refresh(); + expect($company->user_type)->toBe('individual'); + expect($company->company_name)->toBeNull(); + expect($company->email)->toBe('contact@acme.com'); // Preserved +}); + +test('conversion fails with duplicate company registration', function () { + $admin = User::factory()->admin()->create(); + $existingCompany = User::factory()->company()->create([ + 'company_registration' => 'REG-EXISTING', + ]); + $individual = User::factory()->individual()->create(); + + Volt::actingAs($admin) + ->test('admin.users.convert-to-company-modal', ['user' => $individual]) + ->set('company_name', 'New Corp') + ->set('company_registration', 'REG-EXISTING') // Duplicate! + ->set('contact_person_name', 'Test Person') + ->set('contact_person_id', '111222333') + ->call('convertToCompany') + ->assertHasErrors(['company_registration']); +}); + +test('conversion preserves consultations and timelines', function () { + $admin = User::factory()->admin()->create(); + $individual = User::factory()->individual()->create(); + + // Create multiple related records + $consultations = Consultation::factory(3)->for($individual)->create(); + $timelines = Timeline::factory(2)->for($individual)->create(); + + Volt::actingAs($admin) + ->test('admin.users.convert-to-company-modal', ['user' => $individual]) + ->set('company_name', 'Test Corp') + ->set('company_registration', 'REG-TEST') + ->set('contact_person_name', 'Test Person') + ->set('contact_person_id', '123123123') + ->call('convertToCompany') + ->assertHasNoErrors(); + + $individual->refresh(); + expect($individual->consultations)->toHaveCount(3); + expect($individual->timelines)->toHaveCount(2); + + // Verify foreign keys still work + foreach ($consultations as $consultation) { + expect($consultation->fresh()->user_id)->toBe($individual->id); + } +}); +``` + ## Definition of Done - [ ] Convert individual to company works @@ -198,10 +503,25 @@ public function testConversionPreservesRelationships(): void ## Dependencies -- **Story 2.1:** Individual client management -- **Story 2.2:** Company client management -- **Epic 3:** Consultations (for preservation testing) -- **Epic 4:** Timelines (for preservation testing) +### Required from Story 2.1 (Individual Client Management) +- Individual client show page: `resources/views/livewire/admin/users/individual/show.blade.php` +- User model scope: `scopeIndividual()` method +- User factory state: `User::factory()->individual()` + +### Required from Story 2.2 (Company Client Management) +- Company client show page: `resources/views/livewire/admin/users/company/show.blade.php` +- User model scope: `scopeCompany()` method +- User factory state: `User::factory()->company()` + +### Required from Story 1.1/1.2 (Database & Auth) +- AdminLog model with `create()` method accepting: `admin_id`, `action_type`, `target_type`, `target_id`, `old_values`, `new_values`, `ip_address` +- Users table columns: `user_type`, `name`, `national_id`, `company_name`, `company_registration`, `contact_person_name`, `contact_person_id` +- User factory state: `User::factory()->admin()` + +### For Preservation Testing (if models exist) +- **Epic 3 (Consultations):** `Consultation` model with `user_id` foreign key, `Consultation::factory()` +- **Epic 4 (Timelines):** `Timeline` model with `user_id` foreign key, `Timeline::factory()` +- Note: If Epic 3/4 not implemented yet, skip those specific test scenarios and mark as N/A ## Risk Assessment diff --git a/docs/stories/story-2.4-account-lifecycle-management.md b/docs/stories/story-2.4-account-lifecycle-management.md index 7cab7f9..f281603 100644 --- a/docs/stories/story-2.4-account-lifecycle-management.md +++ b/docs/stories/story-2.4-account-lifecycle-management.md @@ -11,10 +11,17 @@ So that **I can manage the full lifecycle of client relationships**. ## Story Context ### Existing System Integration -- **Integrates with:** Users table, consultations, timelines, notifications -- **Technology:** Livewire Volt, confirmation modals -- **Follows pattern:** Soft deactivation, hard deletion with cascade -- **Touch points:** User model, all related models +- **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 @@ -69,6 +76,19 @@ So that **I can manage the full lifecycle of client relationships**. ## 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 @@ -252,6 +272,142 @@ new class extends Component { ``` +### 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 @@ -268,10 +424,25 @@ new class extends Component { ## Dependencies -- **Story 2.1:** Individual client management +### 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 -- **Epic 3:** Consultations (cascade delete) -- **Epic 4:** Timelines (cascade delete) + +### 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 diff --git a/docs/stories/story-2.5-account-creation-email-notification.md b/docs/stories/story-2.5-account-creation-email-notification.md index ac85c3e..3f890e2 100644 --- a/docs/stories/story-2.5-account-creation-email-notification.md +++ b/docs/stories/story-2.5-account-creation-email-notification.md @@ -48,6 +48,13 @@ So that **clients receive their login credentials and know how to access the pla - [ ] From Name: Libra Law Firm / مكتب ليبرا للمحاماة - [ ] Reply-To: (firm contact email) +### Environment Variables +Ensure the following are configured in `.env`: +``` +MAIL_FROM_ADDRESS=no-reply@libra.ps +MAIL_FROM_NAME="Libra Law Firm" +``` + ### Language Support - [ ] Email in user's preferred_language - [ ] Arabic email for Arabic preference @@ -65,6 +72,11 @@ So that **clients receive their login credentials and know how to access the pla - [ ] Password visible but not overly prominent - [ ] Tests verify email sending +### Error Handling +- [ ] Email queued with retry on failure (Laravel default: 3 attempts) +- [ ] Failed emails logged to `failed_jobs` table +- [ ] User creation succeeds even if email fails (non-blocking) + ## Technical Notes ### Mailable Class @@ -245,6 +257,34 @@ it('sends email in user preferred language', function () { return str_contains($mail->envelope()->subject, 'مرحباً'); }); }); + +it('queues welcome email instead of sending synchronously', function () { + Mail::fake(); + + $user = User::factory()->create(); + + Mail::to($user)->queue(new WelcomeEmail($user, 'password123')); + + Mail::assertQueued(WelcomeEmail::class); + Mail::assertNothingSent(); // Confirms it was queued, not sent sync +}); + +it('does not send email for admin accounts', function () { + Mail::fake(); + + // Admin creation flow should NOT trigger welcome email + $admin = User::factory()->admin()->create(); + + Mail::assertNothingQueued(); +}); + +it('generates plain text version of email', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $mailable = new WelcomeEmail($user, 'password123'); + + // Verify mailable can render (catches template errors) + expect($mailable->render())->toBeString(); +}); ``` ## Definition of Done @@ -257,6 +297,8 @@ it('sends email in user preferred language', function () { - [ ] English email for English preference - [ ] Plain text fallback works - [ ] Email queued (not blocking) +- [ ] No email sent for admin accounts +- [ ] Failed emails logged (non-blocking to user creation) - [ ] Tests verify email sending - [ ] Code formatted with Pint