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

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