# Story 8.5: Booking Rejected Email

## Epic Reference
**Epic 8:** Email Notification System

## User Story
As a **client**,
I want **to be notified when my booking is rejected**,
So that **I can understand why and request a new consultation if needed**.

## Dependencies
- **Story 8.1:** Email Infrastructure Setup (provides base template, branding, queue configuration)
- **Epic 3:** Consultation/booking system with status management

## Assumptions
- `Consultation` model exists with `user` relationship (belongsTo User)
- `User` model has `preferred_language` field (defaults to `'ar'` if null)
- Admin rejection action captures optional `reason` field
- Consultation status changes to `'rejected'` when admin rejects
- Base email template and branding from Story 8.1 are available

## Acceptance Criteria

### Trigger
- [x] Sent when consultation status changes to `'rejected'`

### Content
- [x] "Your consultation request could not be approved"
- [x] Original requested date and time
- [x] Rejection reason (conditionally shown if provided by admin)
- [x] Invitation to request new consultation
- [x] Contact info for questions

### Tone
- [x] Empathetic, professional

### Language
- [x] Email in client's preferred language (Arabic or English)
- [x] Default to Arabic if no preference set

## Technical Notes

### Files to Create/Modify

| File | Action | Description |
|------|--------|-------------|
| `app/Mail/BookingRejectedEmail.php` | Create | Mailable class |
| `resources/views/emails/booking/rejected/ar.blade.php` | Create | Arabic template (RTL) |
| `resources/views/emails/booking/rejected/en.blade.php` | Create | English template (LTR) |
| `app/Listeners/SendBookingRejectedEmail.php` | Create | Event listener |
| `app/Events/ConsultationRejected.php` | Create | Event class (if not exists) |

### Mailable Implementation

```php
namespace App\Mail;

use App\Models\Consultation;
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 BookingRejectedEmail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

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

    public function envelope(): Envelope
    {
        $locale = $this->consultation->user->preferred_language ?? 'ar';
        $subject = $locale === 'ar'
            ? 'تعذر الموافقة على طلب الاستشارة'
            : 'Your Consultation Request Could Not Be Approved';

        return new Envelope(subject: $subject);
    }

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

        return new Content(
            markdown: "emails.booking.rejected.{$locale}",
            with: [
                'consultation' => $this->consultation,
                'reason' => $this->reason,
                'hasReason' => !empty($this->reason),
            ],
        );
    }
}
```

### Event/Listener Trigger

```php
// In admin controller or service when rejecting consultation:
use App\Events\ConsultationRejected;

$consultation->update(['status' => 'rejected']);
event(new ConsultationRejected($consultation, $reason));

// app/Events/ConsultationRejected.php
class ConsultationRejected
{
    public function __construct(
        public Consultation $consultation,
        public ?string $reason = null
    ) {}
}

// app/Listeners/SendBookingRejectedEmail.php
class SendBookingRejectedEmail
{
    public function handle(ConsultationRejected $event): void
    {
        Mail::to($event->consultation->user->email)
            ->send(new BookingRejectedEmail(
                $event->consultation,
                $event->reason
            ));
    }
}

// Register in EventServiceProvider boot() or use event discovery
```

### Template Structure (Arabic Example)

```blade
{{-- resources/views/emails/booking/rejected/ar.blade.php --}}
<x-mail::message>
# تعذر الموافقة على طلب الاستشارة

عزيزي/عزيزتي {{ $consultation->user->name }},

نأسف لإبلاغك بأنه تعذر علينا الموافقة على طلب الاستشارة الخاص بك.

**التاريخ المطلوب:** {{ $consultation->scheduled_at->format('Y-m-d') }}
**الوقت المطلوب:** {{ $consultation->scheduled_at->format('H:i') }}

@if($hasReason)
**السبب:** {{ $reason }}
@endif

نرحب بك لتقديم طلب استشارة جديد في وقت آخر يناسبك.

<x-mail::button :url="route('client.consultations.create')">
طلب استشارة جديدة
</x-mail::button>

للاستفسارات، تواصل معنا على: info@libra.ps

مع أطيب التحيات,
مكتب ليبرا للمحاماة
</x-mail::message>
```

## Edge Cases

| Scenario | Handling |
|----------|----------|
| Reason is null/empty | Hide reason section in template using `@if($hasReason)` |
| User has no preferred_language | Default to Arabic (`'ar'`) |
| Queue failure | Standard Laravel queue retry (3 attempts) |
| User email invalid | Queue will fail, logged for admin review |

## Testing Requirements

### Unit Tests
```php
// tests/Unit/Mail/BookingRejectedEmailTest.php

test('booking rejected email renders with reason', function () {
    $consultation = Consultation::factory()->create();
    $reason = 'Schedule conflict';

    $mailable = new BookingRejectedEmail($consultation, $reason);

    $mailable->assertSeeInHtml($reason);
    $mailable->assertSeeInHtml($consultation->scheduled_at->format('Y-m-d'));
});

test('booking rejected email renders without reason', function () {
    $consultation = Consultation::factory()->create();

    $mailable = new BookingRejectedEmail($consultation, null);

    $mailable->assertDontSeeInHtml('السبب:');
    $mailable->assertDontSeeInHtml('Reason:');
});

test('booking rejected email uses arabic template for arabic preference', function () {
    $user = User::factory()->create(['preferred_language' => 'ar']);
    $consultation = Consultation::factory()->for($user)->create();

    $mailable = new BookingRejectedEmail($consultation);

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

test('booking rejected email uses english template for english preference', function () {
    $user = User::factory()->create(['preferred_language' => 'en']);
    $consultation = Consultation::factory()->for($user)->create();

    $mailable = new BookingRejectedEmail($consultation);

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

test('booking rejected email defaults to arabic when no language preference', function () {
    $user = User::factory()->create(['preferred_language' => null]);
    $consultation = Consultation::factory()->for($user)->create();

    $mailable = new BookingRejectedEmail($consultation);

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

### Feature Tests
```php
// tests/Feature/Mail/BookingRejectedEmailTest.php

test('email is queued when consultation is rejected', function () {
    Mail::fake();

    $consultation = Consultation::factory()->create(['status' => 'pending']);
    $reason = 'Not available';

    event(new ConsultationRejected($consultation, $reason));

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

test('email is sent to correct recipient', function () {
    Mail::fake();

    $user = User::factory()->create(['email' => 'client@example.com']);
    $consultation = Consultation::factory()->for($user)->create();

    event(new ConsultationRejected($consultation));

    Mail::assertQueued(BookingRejectedEmail::class, function ($mail) {
        return $mail->hasTo('client@example.com');
    });
});
```

## Definition of Done
- [x] `BookingRejectedEmail` mailable class created
- [x] Arabic template created with RTL layout and empathetic tone
- [x] English template created with LTR layout and empathetic tone
- [x] Event and listener wired for consultation rejection (via existing Notification pattern)
- [x] Reason conditionally displayed when provided
- [x] Defaults to Arabic when no language preference
- [x] Email queued (not sent synchronously)
- [x] All unit tests pass
- [x] All feature tests pass
- [x] Code formatted with Pint

## Estimation
**Complexity:** Low | **Effort:** 2-3 hours

---

## Dev Agent Record

### Status
**Ready for Review**

### Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)

### File List
| File | Action | Description |
|------|--------|-------------|
| `app/Mail/BookingRejectedEmail.php` | Created | Mailable class with bilingual support, ShouldQueue |
| `resources/views/emails/booking/rejected/ar.blade.php` | Created | Arabic email template (RTL) |
| `resources/views/emails/booking/rejected/en.blade.php` | Created | English email template (LTR) |
| `app/Notifications/BookingRejected.php` | Modified | Updated to use new BookingRejectedEmail mailable |
| `tests/Feature/Mail/BookingRejectedEmailTest.php` | Created | 22 test cases for mailable and notification |

### Change Log
- Created `BookingRejectedEmail` mailable with bilingual template support (ar/en)
- Created Arabic email template with RTL layout, empathetic tone, conditional reason display
- Created English email template with LTR layout, empathetic tone, conditional reason display
- Updated existing `BookingRejected` notification to use the new mailable instead of inline view
- Created comprehensive test suite (22 tests) covering:
  - Template selection based on language preference
  - Subject lines in both languages
  - Reason display/hide logic
  - Date/time formatting
  - Notification integration
  - Email rendering

### Completion Notes
- **Implementation Note**: The story suggested Event/Listener pattern, but the codebase already had a `BookingRejected` Notification being dispatched from `admin/bookings/review.blade.php`. For consistency and to avoid duplication, I updated the existing notification to use the new Mailable instead of creating a separate Event/Listener system.
- **Database Schema**: The `preferred_language` column has a default of `'ar'` and is NOT NULL, so the "null handling" in the code is defensive but the database enforces Arabic as default.
- **All 22 tests pass**, all booking-related tests (141 total) pass, Pint formatting complete.

---

## QA Results

### Review Date: 2026-01-02

### Reviewed By: Quinn (Test Architect)

### Code Quality Assessment

**Overall Grade: Excellent**

The implementation is clean, well-structured, and follows established codebase patterns. The mailable class properly implements `ShouldQueue` for asynchronous processing, uses the correct bilingual template strategy, and maintains consistency with the existing `BookingApprovedMail` implementation.

**Strengths:**
- Consistent pattern with existing `BookingApprovedMail` class (same constructor style, locale handling, date/time formatting)
- Proper use of `ShouldQueue` and `SerializesModels` traits
- Clean separation of concerns with dedicated formatting methods
- Defensive null handling for `preferred_language` despite database default
- Empathetic tone in templates appropriate for rejection scenarios
- Proper RTL/LTR handling via `<div dir="rtl">` wrapper in Arabic template

**Minor Observations (No Action Required):**
- The class does not extend `BaseMailable` (like `WelcomeEmail` does), but this is consistent with `BookingApprovedMail` - both implement the pattern directly. No change needed for consistency.
- Button URL uses `config('app.url')` rather than `route('booking')` - this matches the pattern in `BookingApprovedMail` and is acceptable.

### Refactoring Performed

None required. The code is well-structured and follows established patterns.

### Compliance Check

- Coding Standards: ✓ Pint passes, follows PSR-12 conventions
- Project Structure: ✓ Files in correct locations (`app/Mail/`, `resources/views/emails/booking/rejected/`)
- Testing Strategy: ✓ Comprehensive test coverage with 22 tests covering all acceptance criteria
- All ACs Met: ✓ See traceability matrix below

### Requirements Traceability Matrix

| AC | Requirement | Test Coverage |
|----|-------------|---------------|
| Trigger | Sent when consultation status changes to 'rejected' | `notification sends booking rejected email`, `notification toMail returns BookingRejectedEmail mailable` |
| Content | "Your consultation request could not be approved" | `email renders without errors in english`, `email renders without errors in arabic` |
| Content | Original requested 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 includes all required data` |
| Content | Rejection reason (conditionally shown) | `booking rejected email includes reason when provided`, `booking rejected email handles null reason`, `booking rejected email handles empty reason`, `email renders reason section when reason provided`, `email does not render reason section when reason not provided` |
| Content | Invitation to request new consultation | `email renders without errors in english` (verifies button presence) |
| Content | Contact info for questions | Template contains `info@libra.ps` contact |
| Tone | Empathetic, professional | Manual review: ✓ Templates use appropriate language ("نأسف", "We regret") |
| Language | Email in client's preferred language | `booking rejected email uses arabic template for arabic preference`, `booking rejected email uses english template for english preference` |
| Language | Default to Arabic if no preference | `booking rejected email defaults to arabic from database default` |

### Improvements Checklist

All items satisfied - no improvements required:

- [x] Mailable implements ShouldQueue for async processing
- [x] Bilingual templates with proper RTL/LTR support
- [x] Conditional reason display with `@if($hasReason)` directive
- [x] Consistent date formatting per locale
- [x] Subject line in user's preferred language
- [x] 22 comprehensive tests covering all scenarios
- [x] Integration with existing Notification pattern
- [x] Pint formatting compliance

### Security Review

No security concerns identified:
- No user input is rendered unescaped
- Email content uses Blade's default escaping
- No SQL queries or user-controlled data paths

### Performance Considerations

No performance concerns:
- Email is queued via `ShouldQueue` (not sent synchronously)
- `SerializesModels` properly serializes the Consultation model for queue processing
- No N+1 queries in template rendering

### Files Modified During Review

None - no modifications were required.

### Gate Status

Gate: **PASS** → docs/qa/gates/8.5-booking-rejected-email.yml

### Recommended Status

✓ **Ready for Done**

All acceptance criteria are met, all 22 tests pass, code follows established patterns, and no security or performance concerns exist.
