libra/docs/stories/story-8.4-booking-approved-...

14 KiB

Story 8.4: Booking Approved Email

Epic Reference

Epic 8: Email Notification System

Dependencies

  • Story 8.1: Email infrastructure setup (base template, queue config, SMTP)
  • Story 3.6: CalendarService for .ics file generation

User Story

As a client, I want to receive notification when my booking is approved, So that I can confirm the appointment and add it to my calendar.

Acceptance Criteria

Trigger

  • Sent on booking approval by admin

Content

  • "Your consultation has been approved"
  • Confirmed date and time
  • Duration (45 minutes)
  • Consultation type (free/paid)
  • If paid: amount and payment instructions
  • .ics calendar file attached
  • "Add to Calendar" button
  • Location/contact information

Language

  • Email in client's preferred language

Attachment

  • Valid .ics calendar file

Technical Notes

Required Consultation Model Fields

This story assumes the following fields exist on the Consultation model (from Epic 3):

  • id - Unique identifier (booking reference)
  • user_id - Foreign key to User
  • scheduled_date - Date of consultation
  • scheduled_time - Time of consultation
  • duration - Duration in minutes (default: 45)
  • status - Consultation status ('pending', 'approved', 'rejected', etc.)
  • type - 'free' or 'paid'
  • payment_amount - Amount for paid consultations (nullable)

Views to Create

  • resources/views/emails/booking/approved/ar.blade.php - Arabic template
  • resources/views/emails/booking/approved/en.blade.php - English template

Mailable Class

Create app/Mail/BookingApprovedEmail.php:

<?php

namespace App\Mail;

use App\Models\Consultation;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class BookingApprovedEmail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public Consultation $consultation,
        public string $icsContent,
        public ?string $paymentInstructions = null
    ) {}

    public function envelope(): Envelope
    {
        $locale = $this->consultation->user->preferred_language ?? 'ar';
        $subject = $locale === 'ar'
            ? 'تمت الموافقة على استشارتك'
            : 'Your Consultation Has Been Approved';

        return new Envelope(subject: $subject);
    }

    public function content(): Content
    {
        $locale = $this->consultation->user->preferred_language ?? 'ar';

        return new Content(
            markdown: "emails.booking.approved.{$locale}",
            with: [
                'consultation' => $this->consultation,
                'user' => $this->consultation->user,
                'paymentInstructions' => $this->paymentInstructions,
            ],
        );
    }

    public function attachments(): array
    {
        return [
            Attachment::fromData(fn() => $this->icsContent, 'consultation.ics')
                ->withMime('text/calendar'),
        ];
    }
}

Trigger Mechanism

Add observer or listener to send email when consultation status changes to 'approved':

// Option 1: In Consultation model boot method or observer
use App\Mail\BookingApprovedEmail;
use App\Services\CalendarService;
use Illuminate\Support\Facades\Mail;

// In ConsultationObserver or model event
public function updated(Consultation $consultation): void
{
    if ($consultation->wasChanged('status') && $consultation->status === 'approved') {
        $icsContent = app(CalendarService::class)->generateIcs($consultation);

        $paymentInstructions = null;
        if ($consultation->type === 'paid') {
            $paymentInstructions = $this->getPaymentInstructions($consultation);
        }

        Mail::to($consultation->user)
            ->queue(new BookingApprovedEmail($consultation, $icsContent, $paymentInstructions));
    }
}

Payment Instructions

For paid consultations, include payment details:

  • Amount to pay
  • Payment methods accepted
  • Payment deadline (before consultation)
  • Bank transfer details or payment link

Testing Guidance

Test Approach

  • Unit tests for Mailable class
  • Feature tests for trigger mechanism (observer)
  • Integration tests for email queue

Key Test Scenarios

use App\Mail\BookingApprovedEmail;
use App\Models\Consultation;
use App\Models\User;
use App\Services\CalendarService;
use Illuminate\Support\Facades\Mail;

it('queues email when consultation is approved', function () {
    Mail::fake();

    $consultation = Consultation::factory()->create(['status' => 'pending']);
    $consultation->update(['status' => 'approved']);

    Mail::assertQueued(BookingApprovedEmail::class, function ($mail) use ($consultation) {
        return $mail->consultation->id === $consultation->id;
    });
});

it('does not send email when status changes to non-approved', function () {
    Mail::fake();

    $consultation = Consultation::factory()->create(['status' => 'pending']);
    $consultation->update(['status' => 'rejected']);

    Mail::assertNotQueued(BookingApprovedEmail::class);
});

it('includes ics attachment', function () {
    $consultation = Consultation::factory()->approved()->create();
    $icsContent = app(CalendarService::class)->generateIcs($consultation);

    $mailable = new BookingApprovedEmail($consultation, $icsContent);

    expect($mailable->attachments())->toHaveCount(1);
    expect($mailable->attachments()[0]->as)->toBe('consultation.ics');
});

it('uses Arabic template for Arabic-preferring users', function () {
    $user = User::factory()->create(['preferred_language' => 'ar']);
    $consultation = Consultation::factory()->approved()->for($user)->create();
    $icsContent = app(CalendarService::class)->generateIcs($consultation);

    $mailable = new BookingApprovedEmail($consultation, $icsContent);

    expect($mailable->content()->markdown)->toBe('emails.booking.approved.ar');
});

it('uses English template for English-preferring users', function () {
    $user = User::factory()->create(['preferred_language' => 'en']);
    $consultation = Consultation::factory()->approved()->for($user)->create();
    $icsContent = app(CalendarService::class)->generateIcs($consultation);

    $mailable = new BookingApprovedEmail($consultation, $icsContent);

    expect($mailable->content()->markdown)->toBe('emails.booking.approved.en');
});

it('includes payment instructions for paid consultations', function () {
    $consultation = Consultation::factory()->approved()->create([
        'type' => 'paid',
        'payment_amount' => 150.00,
    ]);
    $icsContent = app(CalendarService::class)->generateIcs($consultation);
    $paymentInstructions = 'Please pay 150 ILS before your consultation.';

    $mailable = new BookingApprovedEmail($consultation, $icsContent, $paymentInstructions);

    expect($mailable->paymentInstructions)->toBe($paymentInstructions);
});

it('excludes payment instructions for free consultations', function () {
    $consultation = Consultation::factory()->approved()->create(['type' => 'free']);
    $icsContent = app(CalendarService::class)->generateIcs($consultation);

    $mailable = new BookingApprovedEmail($consultation, $icsContent);

    expect($mailable->paymentInstructions)->toBeNull();
});

Edge Cases to Test

  • User with null preferred_language defaults to 'ar'
  • Consultation without payment_amount for paid type (handle gracefully)
  • Email render test with $mailable->render()

References

  • docs/stories/story-8.1-email-infrastructure-setup.md - Base email template and queue config
  • docs/stories/story-3.6-calendar-file-generation.md - CalendarService for .ics generation
  • docs/epics/epic-8-email-notifications.md#story-84-booking-approved-email - Epic acceptance criteria

Definition of Done

  • Email sent on approval
  • All details included (date, time, duration, type)
  • Payment info for paid consultations
  • .ics file attached
  • Bilingual templates (Arabic/English)
  • Observer/listener triggers on status change
  • Tests pass (all scenarios above)
  • Code formatted with Pint

Estimation

Complexity: Medium | Effort: 3 hours


Dev Agent Record

Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

Completion Notes

  • Created BookingApprovedMail Mailable class with .ics attachment support
  • Created bilingual email templates (Arabic/English) with RTL support
  • Created ConsultationObserver to trigger email on status change to approved
  • Registered observer in AppServiceProvider
  • Implemented 25 comprehensive tests covering all acceptance criteria
  • All tests pass, code formatted with Pint

File List

File Action
app/Mail/BookingApprovedMail.php Created
app/Observers/ConsultationObserver.php Created
app/Providers/AppServiceProvider.php Modified
resources/views/emails/booking/approved/ar.blade.php Created
resources/views/emails/booking/approved/en.blade.php Created
tests/Feature/Mail/BookingApprovedMailTest.php Created

Change Log

Date Change
2026-01-02 Initial implementation of Story 8.4

Status

Ready for Review


QA Results

Review Date: 2026-01-02

Reviewed By: Quinn (Test Architect)

Code Quality Assessment

Overall: Excellent - The implementation is well-structured, follows Laravel best practices, and demonstrates good architectural decisions.

Strengths:

  • Mailable Design: Clean separation of concerns with proper use of Laravel's Mailable components (Envelope, Content, attachments)
  • Queue Support: Correctly implements ShouldQueue for background processing
  • Observer Pattern: Appropriate use of Eloquent Observer for decoupled event handling
  • Bilingual Support: Proper RTL support in Arabic template with dir="rtl" attribute
  • Null Safety: Good use of null coalescing operators for optional fields
  • Helper Methods: Well-organized helper methods (getFormattedDate, getFormattedTime, getConsultationTypeLabel) improve testability

Architecture Notes:

  • The BookingApprovedMail correctly uses dependency injection for the consultation model
  • Payment instructions are properly isolated in the Observer, keeping the Mailable focused on presentation
  • The CalendarService integration is appropriately handled through dependency injection in the Observer

Refactoring Performed

None required. The code quality is high and follows established patterns.

Compliance Check

  • Coding Standards: ✓ Passes Pint formatting, follows naming conventions
  • Project Structure: ✓ Files placed in correct locations per architecture
  • Testing Strategy: ✓ Comprehensive Pest tests with proper factory usage
  • All ACs Met: ✓ All acceptance criteria covered (see traceability below)

Requirements Traceability

AC Requirement Test Coverage
Trigger Email sent on booking approval queues email when consultation is approved, does not send email when status changes to non-approved, does not send email when consultation is created as approved, does not send email when other fields change on approved consultation
Content: Title "Your consultation has been approved" email renders without errors in Arabic/English
Content: Date/Time Confirmed date and time date is formatted as d/m/Y for Arabic users, date is formatted as m/d/Y for English users, time is formatted as h:i A
Content: Duration 45 minutes duration defaults to 45 minutes
Content: Type Consultation type (free/paid) consultation type label is correct in Arabic/English
Content: Payment Payment info for paid includes payment instructions for paid consultations, paid consultation email includes payment amount, excludes payment instructions for free consultations, free consultation email does not include payment section
Attachment .ics calendar file includes ics attachment
Language Client's preferred language uses Arabic template for Arabic-preferring users, uses English template for English-preferring users, defaults to Arabic when preferred_language is ar
Subject Correct subject line has correct Arabic subject, has correct English subject

Test Architecture Assessment

  • Test Count: 25 tests with 37 assertions
  • Test Levels: Appropriate mix of unit tests (Mailable class) and integration tests (Observer behavior)
  • Coverage: All acceptance criteria have corresponding test coverage
  • Factory Usage: Proper use of factory states (approved(), pending(), free(), paid())
  • Edge Cases: All documented edge cases covered

Improvements Checklist

  • All acceptance criteria implemented and tested
  • Bilingual templates with RTL support
  • Queue-based email delivery
  • Observer-based trigger mechanism
  • ICS attachment generation
  • Payment instructions for paid consultations
  • Code formatted with Pint

Security Review

No security concerns identified

  • Email is only sent to the consultation owner (user relationship)
  • No sensitive data exposure in email templates
  • Payment amounts are properly formatted without exposing internal IDs
  • No user-controllable input that could lead to injection

Performance Considerations

No performance concerns

  • Email is queued via ShouldQueue, preventing blocking during approval
  • Observer uses loadMissing() to prevent N+1 queries
  • ICS generation is lightweight and inline

Files Modified During Review

None - code quality was satisfactory.

Gate Status

Gate: PASSdocs/qa/gates/8.4-booking-approved-email.yml

Ready for Done - All acceptance criteria met, comprehensive test coverage, code quality excellent