# Story 8.9: Admin Notification - New Booking ## Epic Reference **Epic 8:** Email Notification System ## Dependencies - **Story 8.1:** Email Infrastructure Setup (base templates, SMTP config, queue setup) ## Story Context This notification is triggered during **Step 2 of the Booking Flow** (PRD Section 5.4). When a client submits a consultation request, it enters the pending queue and the admin must be notified immediately so they can review and respond promptly. This email works alongside Story 8.3 (client confirmation) - both are sent on the same trigger but to different recipients. ## User Story As an **admin**, I want **to be notified when a client submits a booking request**, So that **I can review and respond promptly**. ## Acceptance Criteria ### Trigger - [ ] Sent immediately after successful consultation creation (same trigger as Story 8.3) - [ ] Consultation status: pending - [ ] Email queued for async delivery ### Recipient - [ ] First user with `user_type = 'admin'` in the database - [ ] If no admin exists, log error but don't fail the booking process ### Content - [ ] Subject line: "[Action Required] New Consultation Request" / "[إجراء مطلوب] طلب استشارة جديد" - [ ] "New Consultation Request" heading - [ ] Client name: - Individual: `full_name` - Company: `company_name` (with contact person: `contact_person_name`) - [ ] Requested date and time (formatted per admin's language preference) - [ ] Problem summary (full text, no truncation) - [ ] Client contact information: - Email address - Phone number - Client type indicator (Individual/Company) - [ ] "Review Request" button linking to consultation detail in admin dashboard ### Priority - [ ] "[Action Required]" prefix in subject line (English) - [ ] "[إجراء مطلوب]" prefix in subject line (Arabic) ### Language - [ ] Email sent in admin's `preferred_language` - [ ] Default to 'en' (English) if admin has no preference set (admin-facing communications default to English) ## Technical Notes ### Files to Create ``` app/Mail/NewBookingAdminEmail.php resources/views/emails/admin/new-booking/ar.blade.php resources/views/emails/admin/new-booking/en.blade.php ``` ### Mailable Implementation Using `Mailable` pattern to align with sibling stories (8.2-8.8): ```php getAdminUser(); $locale = $admin?->preferred_language ?? 'en'; return new Envelope( subject: $locale === 'ar' ? '[إجراء مطلوب] طلب استشارة جديد' : '[Action Required] New Consultation Request', ); } public function content(): Content { $admin = $this->getAdminUser(); $locale = $admin?->preferred_language ?? 'en'; return new Content( markdown: "emails.admin.new-booking.{$locale}", with: [ 'consultation' => $this->consultation, 'client' => $this->consultation->user, 'formattedDate' => $this->getFormattedDate($locale), 'formattedTime' => $this->getFormattedTime(), 'reviewUrl' => $this->getReviewUrl(), ], ); } private function getAdminUser(): ?User { return User::where('user_type', 'admin')->first(); } private function getFormattedDate(string $locale): string { $date = $this->consultation->booking_date; return $locale === 'ar' ? $date->format('d/m/Y') : $date->format('m/d/Y'); } private function getFormattedTime(): string { return $this->consultation->booking_time->format('h:i A'); } private function getReviewUrl(): string { return route('admin.consultations.show', $this->consultation); } } ``` ### Dispatch Point Same location as Story 8.3 - after consultation creation: ```php // In the controller or action handling booking submission // Dispatch AFTER the client confirmation email (Story 8.3) use App\Mail\NewBookingAdminEmail; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; // Send admin notification $admin = User::where('user_type', 'admin')->first(); if ($admin) { Mail::to($admin->email)->send(new NewBookingAdminEmail($consultation)); } else { Log::warning('No admin user found to notify about new booking', [ 'consultation_id' => $consultation->id, ]); } ``` ### Edge Cases - **No admin user exists:** Log warning, continue without sending (booking should not fail) - **Admin has no email:** Skip sending, log error - **Admin `preferred_language` is null:** Default to 'en' (English) - **Client is company type:** Display company name prominently, include contact person name - **Client is individual type:** Display full name - **Consultation missing `booking_date` or `booking_time`:** Should not happen (validation), but handle gracefully ### Client Information Display Logic ```php // In the email template @if($client->user_type === 'company') {{ $client->company_name }}
Contact: {{ $client->contact_person_name }} @else {{ $client->full_name }} @endif Email: {{ $client->email }} Phone: {{ $client->phone }} ``` ## Testing Requirements ### Unit Tests ```php create([ 'user_type' => 'admin', 'preferred_language' => 'en', ]); $client = User::factory()->create(['user_type' => 'individual']); $consultation = Consultation::factory()->create(['user_id' => $client->id]); $mailable = new NewBookingAdminEmail($consultation); expect($mailable->envelope()->subject) ->toBe('[Action Required] New Consultation Request'); }); test('admin email has action required prefix in Arabic subject', function () { $admin = User::factory()->create([ 'user_type' => 'admin', 'preferred_language' => 'ar', ]); $client = User::factory()->create(['user_type' => 'individual']); $consultation = Consultation::factory()->create(['user_id' => $client->id]); $mailable = new NewBookingAdminEmail($consultation); expect($mailable->envelope()->subject) ->toBe('[إجراء مطلوب] طلب استشارة جديد'); }); test('admin email defaults to English when admin has no language preference', function () { $admin = User::factory()->create([ 'user_type' => 'admin', 'preferred_language' => null, ]); $client = User::factory()->create(['user_type' => 'individual']); $consultation = Consultation::factory()->create(['user_id' => $client->id]); $mailable = new NewBookingAdminEmail($consultation); expect($mailable->envelope()->subject) ->toContain('[Action Required]'); }); test('admin email includes full problem summary', function () { $admin = User::factory()->create(['user_type' => 'admin']); $client = User::factory()->create(['user_type' => 'individual']); $longSummary = str_repeat('Legal issue description. ', 50); $consultation = Consultation::factory()->create([ 'user_id' => $client->id, 'problem_summary' => $longSummary, ]); $mailable = new NewBookingAdminEmail($consultation); $content = $mailable->content(); // Full summary passed, not truncated expect($content->with['consultation']->problem_summary) ->toBe($longSummary); }); test('admin email includes review URL', function () { $admin = User::factory()->create(['user_type' => 'admin']); $client = User::factory()->create(['user_type' => 'individual']); $consultation = Consultation::factory()->create(['user_id' => $client->id]); $mailable = new NewBookingAdminEmail($consultation); $content = $mailable->content(); expect($content->with['reviewUrl']) ->toContain('consultations') ->toContain((string) $consultation->id); }); ``` ### Feature Tests ```php create(['user_type' => 'admin']); $client = User::factory()->create(['user_type' => 'individual']); $consultation = Consultation::factory()->create(['user_id' => $client->id]); Mail::to($admin->email)->send(new NewBookingAdminEmail($consultation)); Mail::assertSent(NewBookingAdminEmail::class, function ($mail) use ($admin) { return $mail->hasTo($admin->email); }); }); test('admin email is queued for async delivery', function () { Mail::fake(); $admin = User::factory()->create(['user_type' => 'admin']); $client = User::factory()->create(['user_type' => 'individual']); $consultation = Consultation::factory()->create(['user_id' => $client->id]); Mail::to($admin->email)->send(new NewBookingAdminEmail($consultation)); Mail::assertQueued(NewBookingAdminEmail::class); }); test('warning is logged when no admin exists', function () { Log::shouldReceive('warning') ->once() ->with('No admin user found to notify about new booking', \Mockery::any()); $client = User::factory()->create(['user_type' => 'individual']); $consultation = Consultation::factory()->create(['user_id' => $client->id]); $admin = User::where('user_type', 'admin')->first(); if (!$admin) { Log::warning('No admin user found to notify about new booking', [ 'consultation_id' => $consultation->id, ]); } }); test('admin email displays company client information correctly', function () { Mail::fake(); $admin = User::factory()->create(['user_type' => 'admin']); $companyClient = User::factory()->create([ 'user_type' => 'company', 'company_name' => 'Acme Corp', 'contact_person_name' => 'John Doe', ]); $consultation = Consultation::factory()->create(['user_id' => $companyClient->id]); $mailable = new NewBookingAdminEmail($consultation); expect($mailable->content()->with['client']->company_name)->toBe('Acme Corp'); expect($mailable->content()->with['client']->contact_person_name)->toBe('John Doe'); }); ``` ## References - **PRD Section 5.4:** Booking Flow - Step 2 "Admin receives email notification at no-reply@libra.ps" - **PRD Section 8.2:** Admin Emails - "New Booking Request - With client details and problem summary" - **Story 8.1:** Base email template structure, SMTP config, queue setup - **Story 8.3:** Similar trigger pattern (booking submission) - client-facing counterpart ## Definition of Done - [x] `NewBookingAdminEmail` Mailable class created - [x] Arabic template created and renders correctly - [x] English template created and renders correctly - [x] Email dispatched on consultation creation (after Story 8.3 client email) - [x] Email queued (implements ShouldQueue) - [x] Subject contains "[Action Required]" / "[إجراء مطلوب]" prefix - [x] All client information included (name, email, phone, type) - [x] Company clients show company name and contact person - [x] Full problem summary displayed (no truncation) - [x] Review link navigates to admin consultation detail page - [x] Date/time formatted per admin language preference - [x] Graceful handling when no admin exists (log warning, don't fail) - [x] Unit tests pass - [x] Feature tests pass - [x] Code formatted with Pint ## Estimation **Complexity:** Low | **Effort:** 2-3 hours --- ## Dev Agent Record ### Agent Model Used Claude Opus 4.5 (claude-opus-4-5-20251101) ### Completion Notes - Created `NewBookingAdminEmail` Mailable class with bilingual support (EN/AR) - Created English and Arabic email templates in `emails/admin/new-booking/` - Updated `book.blade.php` to replace `NewBookingRequestMail` with `NewBookingAdminEmail` - Added Log warning when no admin exists - Updated `BookingSubmissionTest.php` to use the new Mailable class - Created comprehensive test suite in `NewBookingAdminEmailTest.php` with 20 tests - All tests pass (57 tests, 106 assertions for related files) ### File List | File | Action | |------|--------| | `app/Mail/NewBookingAdminEmail.php` | Created | | `resources/views/emails/admin/new-booking/en.blade.php` | Created | | `resources/views/emails/admin/new-booking/ar.blade.php` | Created | | `resources/views/livewire/client/consultations/book.blade.php` | Modified | | `tests/Feature/Mail/NewBookingAdminEmailTest.php` | Created | | `tests/Feature/Client/BookingSubmissionTest.php` | Modified | ### Change Log | Change | Reason | |--------|--------| | Replaced `NewBookingRequestMail` with `NewBookingAdminEmail` | New Mailable follows proper bilingual pattern with locale-based templates | | Added `Log` facade import to booking component | Required for warning when no admin exists | | Updated test imports and assertions | Tests now reference correct Mailable class | ### Status Ready for Review --- ## QA Results ### Review Date: 2026-01-02 ### Reviewed By: Quinn (Test Architect) ### Code Quality Assessment Excellent implementation following established patterns from sibling stories (8.2-8.8). The `NewBookingAdminEmail` Mailable class is well-structured with: - Proper ShouldQueue implementation for async delivery - Bilingual template support (EN/AR) using locale-based view selection - Clean separation of formatting logic (date/time methods) - Graceful degradation when no admin exists (logs warning, doesn't fail booking) The dispatch point in `book.blade.php` is correctly placed within the DB transaction, ensuring the email is only queued after successful booking creation. ### Refactoring Performed None required. Implementation is clean and follows project standards. ### Compliance Check - Coding Standards: [x] Code follows Laravel conventions, proper PHPDoc comments, clean formatting - Project Structure: [x] Files placed in correct locations per story specification - Testing Strategy: [x] Comprehensive test coverage with 20 unit/feature tests in dedicated file plus 21 integration tests in BookingSubmissionTest - All ACs Met: [x] All 16 acceptance criteria verified with test coverage ### Improvements Checklist All items are compliant - no changes required: - [x] Mailable implements ShouldQueue for async delivery - [x] Subject line contains [Action Required] / [x] prefix - [x] Email sent in admin's preferred_language with EN default - [x] Individual and company client information displayed correctly - [x] Problem summary passed without truncation - [x] Review URL points to admin consultation show page - [x] Date formatted per locale (d/m/Y for AR, m/d/Y for EN) - [x] Time formatted as 12-hour with AM/PM - [x] Warning logged when no admin exists - [x] Booking flow continues even if no admin found ### Security Review No security concerns: - Email recipient is admin only (internal system notification) - Review URL uses proper route helper with model binding - No sensitive data exposure beyond what admin should see - Client contact info appropriate for admin notification ### Performance Considerations No performance concerns: - Email queued for async delivery (ShouldQueue) - Admin lookup uses `first()` with minimal query - No N+1 queries - single consultation and user relationship loaded Future consideration: If email volume increases significantly, consider caching the admin user lookup within the mailable lifecycle to avoid repeated queries in `envelope()` and `content()` methods. ### Files Modified During Review None - implementation meets all requirements without modification. ### Gate Status Gate: PASS -> docs/qa/gates/8.9-admin-notification-new-booking.yml ### Recommended Status [x] Ready for Done Story owner may merge to main branch. All acceptance criteria verified, tests passing (41 tests across both test files), and implementation follows established patterns.