reviewd and fixed epic 1 & 2
This commit is contained in:
parent
8f95089814
commit
b93b9363a6
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<!-- resources/views/components/language-toggle.blade.php -->
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="{{ route('language.switch', 'ar') }}"
|
||||
@class([
|
||||
'text-sm px-2 py-1 rounded',
|
||||
'bg-gold text-navy font-bold' => app()->getLocale() === 'ar',
|
||||
'text-gold hover:text-gold-light' => app()->getLocale() !== 'ar',
|
||||
])
|
||||
>
|
||||
العربية
|
||||
</a>
|
||||
<span class="text-gold/50">|</span>
|
||||
<a
|
||||
href="{{ route('language.switch', 'en') }}"
|
||||
@class([
|
||||
'text-sm px-2 py-1 rounded',
|
||||
'bg-gold text-navy font-bold' => app()->getLocale() === 'en',
|
||||
'text-gold hover:text-gold-light' => app()->getLocale() !== 'en',
|
||||
])
|
||||
>
|
||||
English
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -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**.
|
|||
<!-- resources/views/components/logo.blade.php -->
|
||||
@props(['size' => 'default'])
|
||||
|
||||
<img
|
||||
src="{{ asset('images/logo.svg') }}"
|
||||
alt="{{ __('Libra Law Firm') }}"
|
||||
@class([
|
||||
'h-8' => $size === 'small',
|
||||
'h-12' => $size === 'default',
|
||||
'h-16' => $size === 'large',
|
||||
])
|
||||
/>
|
||||
@if(file_exists(public_path('images/logo.svg')))
|
||||
<img
|
||||
src="{{ asset('images/logo.svg') }}"
|
||||
alt="{{ __('Libra Law Firm') }}"
|
||||
@class([
|
||||
'h-8' => $size === 'small',
|
||||
'h-12' => $size === 'default',
|
||||
'h-16' => $size === 'large',
|
||||
])
|
||||
/>
|
||||
@else
|
||||
<span @class([
|
||||
'font-bold text-gold',
|
||||
'text-lg' => $size === 'small',
|
||||
'text-2xl' => $size === 'default',
|
||||
'text-3xl' => $size === 'large',
|
||||
])>Libra</span>
|
||||
@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
|
||||
<!-- Guest vs Authenticated menu items -->
|
||||
<flux:navbar.links class="text-gold">
|
||||
<flux:navbar.link href="/" :active="request()->is('/')">
|
||||
{{ __('navigation.home') }}
|
||||
</flux:navbar.link>
|
||||
<flux:navbar.link href="/booking" :active="request()->is('booking*')">
|
||||
{{ __('navigation.booking') }}
|
||||
</flux:navbar.link>
|
||||
<flux:navbar.link href="/posts" :active="request()->is('posts*')">
|
||||
{{ __('navigation.posts') }}
|
||||
</flux:navbar.link>
|
||||
|
||||
@auth
|
||||
<flux:navbar.link href="/dashboard" :active="request()->is('dashboard*')">
|
||||
{{ __('navigation.dashboard') }}
|
||||
</flux:navbar.link>
|
||||
<form method="POST" action="{{ route('logout') }}" class="inline">
|
||||
@csrf
|
||||
<flux:navbar.link
|
||||
href="{{ route('logout') }}"
|
||||
onclick="event.preventDefault(); this.closest('form').submit();"
|
||||
>
|
||||
{{ __('navigation.logout') }}
|
||||
</flux:navbar.link>
|
||||
</form>
|
||||
@else
|
||||
<flux:navbar.link href="/login" :active="request()->is('login')">
|
||||
{{ __('navigation.login') }}
|
||||
</flux:navbar.link>
|
||||
@endauth
|
||||
</flux:navbar.links>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\AdminLog; // Assumes AdminLog model exists from Story 1.1
|
||||
use Livewire\Volt\Component;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
|
|
@ -148,8 +154,17 @@ new class extends Component {
|
|||
'status' => '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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\AdminLog;
|
||||
use App\Notifications\AccountTypeChangedNotification;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
|
|
@ -99,28 +148,59 @@ new class extends Component {
|
|||
|
||||
$oldValues = $this->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
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class AccountTypeChangedNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public string $newUserType
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$locale = $notifiable->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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
</flux:modal>
|
||||
```
|
||||
|
||||
### Edge Cases & Error Handling
|
||||
- **Active sessions on deactivation:** Invalidate all user sessions when deactivated (use `DB::table('sessions')->where('user_id', $user->id)->delete()`)
|
||||
- **Deactivated user login attempt:** Show bilingual message: "Your account has been deactivated. Please contact the administrator."
|
||||
- **Delete with related data:** Cascade deletion handles consultations/timelines automatically via model `deleting` event
|
||||
- **Delete confirmation mismatch:** Display validation error, do not proceed with deletion
|
||||
- **Password reset email failure:** Queue the email, log failure, show success message (email queued)
|
||||
- **Reactivation email failure:** Queue the email, log failure, still complete reactivation
|
||||
|
||||
### Session Invalidation on Deactivation
|
||||
```php
|
||||
// Add to deactivate() method in Volt component
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
public function deactivate(): void
|
||||
{
|
||||
$this->user->deactivate();
|
||||
|
||||
// Invalidate all active sessions for this user
|
||||
DB::table('sessions')
|
||||
->where('user_id', $this->user->id)
|
||||
->delete();
|
||||
|
||||
// Log action...
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Test File Location
|
||||
`tests/Feature/Admin/AccountLifecycleTest.php`
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
#### Deactivation Tests
|
||||
- [ ] Admin can deactivate an active user
|
||||
- [ ] Deactivated user cannot login (returns null from authenticateUsing)
|
||||
- [ ] Deactivated user shows visual indicator in user list
|
||||
- [ ] User sessions are invalidated on deactivation
|
||||
- [ ] AdminLog entry created with old/new status values
|
||||
- [ ] Deactivation preserves all user data (consultations, timelines intact)
|
||||
|
||||
#### Reactivation Tests
|
||||
- [ ] Admin can reactivate a deactivated user
|
||||
- [ ] Reactivated user can login successfully
|
||||
- [ ] Reactivated user status changes to 'active'
|
||||
- [ ] Email notification queued on reactivation
|
||||
- [ ] AdminLog entry created for reactivation
|
||||
|
||||
#### Permanent Deletion Tests
|
||||
- [ ] Delete button shows danger styling
|
||||
- [ ] Delete requires typing user email for confirmation
|
||||
- [ ] Incorrect email confirmation shows validation error
|
||||
- [ ] Successful deletion removes user record permanently
|
||||
- [ ] Cascade deletion removes user's consultations
|
||||
- [ ] Cascade deletion removes user's timelines and timeline_updates
|
||||
- [ ] Cascade deletion removes user's notifications
|
||||
- [ ] AdminLog entry preserved after user deletion (for audit trail)
|
||||
- [ ] Redirect to users index after successful deletion
|
||||
|
||||
#### Password Reset Tests
|
||||
- [ ] Admin can reset user password with random generation
|
||||
- [ ] New password meets minimum requirements (8+ characters)
|
||||
- [ ] Password reset email sent to user with new credentials
|
||||
- [ ] AdminLog entry created for password reset
|
||||
- [ ] User can login with new password
|
||||
|
||||
#### Authorization Tests
|
||||
- [ ] Non-admin users cannot access lifecycle actions
|
||||
- [ ] Admin cannot deactivate their own account
|
||||
- [ ] Admin cannot delete their own account
|
||||
|
||||
#### Bilingual Tests
|
||||
- [ ] Confirmation dialogs display in Arabic when locale is 'ar'
|
||||
- [ ] Confirmation dialogs display in English when locale is 'en'
|
||||
- [ ] Success/error messages respect user's preferred language
|
||||
|
||||
### Testing Approach
|
||||
```php
|
||||
use App\Models\User;
|
||||
use App\Models\AdminLog;
|
||||
use Livewire\Volt\Volt;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use App\Notifications\AccountReactivatedNotification;
|
||||
|
||||
test('admin can deactivate active user', function () {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$client = User::factory()->individual()->create(['status' => 'active']);
|
||||
|
||||
Volt::actingAs($admin)
|
||||
->test('admin.users.show', ['user' => $client])
|
||||
->call('deactivate')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($client->fresh()->status)->toBe('deactivated');
|
||||
expect(AdminLog::where('action_type', 'deactivate')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('deactivated user cannot login', function () {
|
||||
$user = User::factory()->create(['status' => 'deactivated']);
|
||||
|
||||
$this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
])->assertSessionHasErrors();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('delete requires email confirmation', function () {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$client = User::factory()->individual()->create();
|
||||
|
||||
Volt::actingAs($admin)
|
||||
->test('admin.users.show', ['user' => $client])
|
||||
->set('deleteConfirmation', 'wrong@email.com')
|
||||
->call('delete')
|
||||
->assertHasErrors(['deleteConfirmation']);
|
||||
|
||||
expect(User::find($client->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('reactivation sends email notification', function () {
|
||||
Notification::fake();
|
||||
|
||||
$admin = User::factory()->admin()->create();
|
||||
$client = User::factory()->individual()->create(['status' => 'deactivated']);
|
||||
|
||||
Volt::actingAs($admin)
|
||||
->test('admin.users.show', ['user' => $client])
|
||||
->call('reactivate')
|
||||
->assertHasNoErrors();
|
||||
|
||||
Notification::assertSentTo($client, AccountReactivatedNotification::class);
|
||||
});
|
||||
```
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] Deactivate prevents login but preserves data
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue