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
+
+
+```
+
+## 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'])
-
$size === 'small',
- 'h-12' => $size === 'default',
- 'h-16' => $size === 'large',
- ])
-/>
+@if(file_exists(public_path('images/logo.svg')))
+
$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') }}
+
+
+ @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