libra/docs/stories/story-8.9-admin-notificatio...

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)
  • 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_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

// 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

  • NewBookingAdminEmail Mailable 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 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: 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

[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.