# 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 - [ ] Sent when consultation status changes to `'rejected'` ### Content - [ ] "Your consultation request could not be approved" - [ ] Original requested date and time - [ ] Rejection reason (conditionally shown if provided by admin) - [ ] Invitation to request new consultation - [ ] Contact info for questions ### Tone - [ ] Empathetic, professional ### Language - [ ] Email in client's preferred language (Arabic or English) - [ ] 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 --}} # تعذر الموافقة على طلب الاستشارة عزيزي/عزيزتي {{ $consultation->user->name }}, نأسف لإبلاغك بأنه تعذر علينا الموافقة على طلب الاستشارة الخاص بك. **التاريخ المطلوب:** {{ $consultation->scheduled_at->format('Y-m-d') }} **الوقت المطلوب:** {{ $consultation->scheduled_at->format('H:i') }} @if($hasReason) **السبب:** {{ $reason }} @endif نرحب بك لتقديم طلب استشارة جديد في وقت آخر يناسبك. طلب استشارة جديدة للاستفسارات، تواصل معنا على: info@libra.ps مع أطيب التحيات, مكتب ليبرا للمحاماة ``` ## 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 - [ ] `BookingRejectedEmail` mailable class created - [ ] Arabic template created with RTL layout and empathetic tone - [ ] English template created with LTR layout and empathetic tone - [ ] Event and listener wired for consultation rejection - [ ] Reason conditionally displayed when provided - [ ] Defaults to Arabic when no language preference - [ ] Email queued (not sent synchronously) - [ ] All unit tests pass - [ ] All feature tests pass - [ ] Code formatted with Pint ## Estimation **Complexity:** Low | **Effort:** 2-3 hours