12 KiB
12 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