16 KiB
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)
- Individual:
- 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
namespace App\Mail;
use App\Models\Consultation;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class NewBookingAdminEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(
public Consultation $consultation
) {}
public function envelope(): Envelope
{
$admin = $this->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:
// 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_languageis 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_dateorbooking_time: Should not happen (validation), but handle gracefully
Client Information Display Logic
// In the email template
@if($client->user_type === 'company')
<strong>{{ $client->company_name }}</strong>
<br>Contact: {{ $client->contact_person_name }}
@else
<strong>{{ $client->full_name }}</strong>
@endif
Email: {{ $client->email }}
Phone: {{ $client->phone }}
Testing Requirements
Unit Tests
<?php
use App\Mail\NewBookingAdminEmail;
use App\Models\Consultation;
use App\Models\User;
test('admin email has action required prefix in English subject', function () {
$admin = User::factory()->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
use App\Mail\NewBookingAdminEmail;
use App\Models\Consultation;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
test('admin email is sent when consultation is created', 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::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
NewBookingAdminEmailMailable class created- Arabic template created and renders correctly
- English template created and renders correctly
- Email dispatched on consultation creation (after Story 8.3 client email)
- Email queued (implements ShouldQueue)
- Subject contains "[Action Required]" / "[إجراء مطلوب]" prefix
- All client information included (name, email, phone, type)
- Company clients show company name and contact person
- Full problem summary displayed (no truncation)
- Review link navigates to admin consultation detail page
- Date/time formatted per admin language preference
- Graceful handling when no admin exists (log warning, don't fail)
- Unit tests pass
- Feature tests pass
- 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
NewBookingAdminEmailMailable class with bilingual support (EN/AR) - Created English and Arabic email templates in
emails/admin/new-booking/ - Updated
book.blade.phpto replaceNewBookingRequestMailwithNewBookingAdminEmail - Added Log warning when no admin exists
- Updated
BookingSubmissionTest.phpto use the new Mailable class - Created comprehensive test suite in
NewBookingAdminEmailTest.phpwith 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: Code follows Laravel conventions, proper PHPDoc comments, clean formatting
- Project Structure: Files placed in correct locations per story specification
- Testing Strategy: Comprehensive test coverage with 20 unit/feature tests in dedicated file plus 21 integration tests in BookingSubmissionTest
- All ACs Met: All 16 acceptance criteria verified with test coverage
Improvements Checklist
All items are compliant - no changes required:
- Mailable implements ShouldQueue for async delivery
- Subject line contains [Action Required] / prefix
- Email sent in admin's preferred_language with EN default
- Individual and company client information displayed correctly
- Problem summary passed without truncation
- Review URL points to admin consultation show page
- Date formatted per locale (d/m/Y for AR, m/d/Y for EN)
- Time formatted as 12-hour with AM/PM
- Warning logged when no admin exists
- 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.