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

8.1 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