# 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 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': ```php // 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 ```php 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