diff --git a/app/Mail/BookingApprovedMail.php b/app/Mail/BookingApprovedMail.php new file mode 100644 index 0000000..85154da --- /dev/null +++ b/app/Mail/BookingApprovedMail.php @@ -0,0 +1,92 @@ +locale = $consultation->user->preferred_language ?? 'ar'; + } + + public function envelope(): Envelope + { + $locale = $this->consultation->user->preferred_language ?? 'ar'; + + return new Envelope( + subject: $locale === 'ar' + ? 'تمت الموافقة على استشارتك' + : 'Your Consultation Has Been Approved', + ); + } + + 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, + 'formattedDate' => $this->getFormattedDate($locale), + 'formattedTime' => $this->getFormattedTime(), + 'duration' => $this->consultation->duration ?? 45, + 'consultationType' => $this->getConsultationTypeLabel($locale), + 'isPaid' => $this->consultation->consultation_type?->value === 'paid', + 'paymentAmount' => $this->consultation->payment_amount, + ], + ); + } + + public function attachments(): array + { + return [ + Attachment::fromData(fn () => $this->icsContent, 'consultation.ics') + ->withMime('text/calendar'), + ]; + } + + public function getFormattedDate(string $locale): string + { + $date = $this->consultation->booking_date; + + return $locale === 'ar' + ? $date->format('d/m/Y') + : $date->format('m/d/Y'); + } + + public function getFormattedTime(): string + { + $time = $this->consultation->booking_time; + + return Carbon::parse($time)->format('h:i A'); + } + + public function getConsultationTypeLabel(string $locale): string + { + $type = $this->consultation->consultation_type?->value ?? 'free'; + + if ($locale === 'ar') { + return $type === 'paid' ? 'مدفوعة' : 'مجانية'; + } + + return $type === 'paid' ? 'Paid' : 'Free'; + } +} diff --git a/app/Observers/ConsultationObserver.php b/app/Observers/ConsultationObserver.php new file mode 100644 index 0000000..590a000 --- /dev/null +++ b/app/Observers/ConsultationObserver.php @@ -0,0 +1,51 @@ +wasChanged('status') && $consultation->status === ConsultationStatus::Approved) { + $this->sendApprovalEmail($consultation); + } + } + + protected function sendApprovalEmail(Consultation $consultation): void + { + $consultation->loadMissing('user'); + + $icsContent = $this->calendarService->generateIcs($consultation); + + $paymentInstructions = null; + if ($consultation->consultation_type === ConsultationType::Paid) { + $paymentInstructions = $this->getPaymentInstructions($consultation); + } + + Mail::to($consultation->user) + ->queue(new BookingApprovedMail($consultation, $icsContent, $paymentInstructions)); + } + + protected function getPaymentInstructions(Consultation $consultation): string + { + $locale = $consultation->user->preferred_language ?? 'ar'; + $amount = number_format($consultation->payment_amount ?? 0, 2); + + if ($locale === 'ar') { + return "يرجى دفع مبلغ {$amount} شيكل قبل موعد الاستشارة."; + } + + return "Please pay {$amount} ILS before your consultation."; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 97d1c5a..923ea3f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,8 @@ namespace App\Providers; use App\Listeners\LogFailedLoginAttempt; +use App\Models\Consultation; +use App\Observers\ConsultationObserver; use Illuminate\Auth\Events\Failed; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; @@ -23,5 +25,7 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { Event::listen(Failed::class, LogFailedLoginAttempt::class); + + Consultation::observe(ConsultationObserver::class); } } diff --git a/docs/qa/gates/8.4-booking-approved-email.yml b/docs/qa/gates/8.4-booking-approved-email.yml new file mode 100644 index 0000000..606c549 --- /dev/null +++ b/docs/qa/gates/8.4-booking-approved-email.yml @@ -0,0 +1,45 @@ +schema: 1 +story: "8.4" +story_title: "Booking Approved Email" +gate: PASS +status_reason: "All acceptance criteria implemented with comprehensive test coverage (25 tests). Code follows Laravel best practices with proper queue handling, observer pattern, and bilingual support." +reviewer: "Quinn (Test Architect)" +updated: "2026-01-02T20:15:00Z" + +waiver: { active: false } + +top_issues: [] + +risk_summary: + totals: { critical: 0, high: 0, medium: 0, low: 0 } + recommendations: + must_fix: [] + monitor: [] + +quality_score: 100 +expires: "2026-01-16T00:00:00Z" + +evidence: + tests_reviewed: 25 + risks_identified: 0 + trace: + ac_covered: [1, 2, 3, 4, 5, 6, 7, 8, 9] + ac_gaps: [] + +nfr_validation: + security: + status: PASS + notes: "Email sent only to consultation owner. No sensitive data exposure." + performance: + status: PASS + notes: "Queue-based delivery prevents blocking. Observer uses loadMissing() to avoid N+1." + reliability: + status: PASS + notes: "Proper null handling. Observer only triggers on status change to approved." + maintainability: + status: PASS + notes: "Clean separation of concerns. Helper methods improve testability." + +recommendations: + immediate: [] + future: [] diff --git a/docs/stories/story-8.4-booking-approved-email.md b/docs/stories/story-8.4-booking-approved-email.md index 5d1cade..10e01a6 100644 --- a/docs/stories/story-8.4-booking-approved-email.md +++ b/docs/stories/story-8.4-booking-approved-email.md @@ -243,14 +243,142 @@ it('excludes payment instructions for free consultations', function () { - `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 +- [x] Email sent on approval +- [x] All details included (date, time, duration, type) +- [x] Payment info for paid consultations +- [x] .ics file attached +- [x] Bilingual templates (Arabic/English) +- [x] Observer/listener triggers on status change +- [x] Tests pass (all scenarios above) +- [x] 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 + +- [x] All acceptance criteria implemented and tested +- [x] Bilingual templates with RTL support +- [x] Queue-based email delivery +- [x] Observer-based trigger mechanism +- [x] ICS attachment generation +- [x] Payment instructions for paid consultations +- [x] 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: **PASS** → `docs/qa/gates/8.4-booking-approved-email.yml` + +### Recommended Status + +✓ **Ready for Done** - All acceptance criteria met, comprehensive test coverage, code quality excellent diff --git a/resources/views/emails/booking/approved/ar.blade.php b/resources/views/emails/booking/approved/ar.blade.php new file mode 100644 index 0000000..64292bc --- /dev/null +++ b/resources/views/emails/booking/approved/ar.blade.php @@ -0,0 +1,47 @@ + +
+# تمت الموافقة على استشارتك + +عزيزي {{ $user->company_name ?? $user->full_name }}، + +يسعدنا إبلاغك بأنه تمت الموافقة على طلب الاستشارة الخاص بك. + +**تفاصيل الموعد:** + +- **التاريخ:** {{ $formattedDate }} +- **الوقت:** {{ $formattedTime }} +- **المدة:** {{ $duration }} دقيقة +- **نوع الاستشارة:** {{ $consultationType }} + +@if($isPaid && $paymentAmount) + +**معلومات الدفع:** + +المبلغ المطلوب: **{{ number_format($paymentAmount, 2) }} شيكل** + +@if($paymentInstructions) +{{ $paymentInstructions }} +@else +يرجى إتمام الدفع قبل موعد الاستشارة. +@endif + +@endif + + +**موقع المكتب:** + +{{ config('libra.office_address.ar', 'مكتب ليبرا للمحاماة') }} + + + +إضافة إلى التقويم + + +تم إرفاق ملف التقويم (.ics) لإضافة الموعد إلى تقويمك. + +إذا كان لديك أي استفسار، لا تتردد في التواصل معنا. + +مع أطيب التحيات،
+{{ config('app.name') }} +
+
diff --git a/resources/views/emails/booking/approved/en.blade.php b/resources/views/emails/booking/approved/en.blade.php new file mode 100644 index 0000000..fb4126b --- /dev/null +++ b/resources/views/emails/booking/approved/en.blade.php @@ -0,0 +1,45 @@ + +# Your Consultation Has Been Approved + +Dear {{ $user->company_name ?? $user->full_name }}, + +We are pleased to inform you that your consultation request has been approved. + +**Appointment Details:** + +- **Date:** {{ $formattedDate }} +- **Time:** {{ $formattedTime }} +- **Duration:** {{ $duration }} minutes +- **Consultation Type:** {{ $consultationType }} + +@if($isPaid && $paymentAmount) + +**Payment Information:** + +Amount Due: **{{ number_format($paymentAmount, 2) }} ILS** + +@if($paymentInstructions) +{{ $paymentInstructions }} +@else +Please complete your payment before the consultation date. +@endif + +@endif + + +**Office Location:** + +{{ config('libra.office_address.en', 'Libra Law Firm') }} + + + +Add to Calendar + + +A calendar file (.ics) has been attached for you to add this appointment to your calendar. + +If you have any questions, please don't hesitate to contact us. + +Regards,
+{{ config('app.name') }} +
diff --git a/tests/Feature/Mail/BookingApprovedMailTest.php b/tests/Feature/Mail/BookingApprovedMailTest.php new file mode 100644 index 0000000..c19a880 --- /dev/null +++ b/tests/Feature/Mail/BookingApprovedMailTest.php @@ -0,0 +1,317 @@ +create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->pending()->create(['user_id' => $user->id]); + + $consultation->update(['status' => ConsultationStatus::Approved]); + + Mail::assertQueued(BookingApprovedMail::class, function ($mail) use ($consultation) { + return $mail->consultation->id === $consultation->id; + }); +}); + +test('does not send email when status changes to non-approved', function () { + Mail::fake(); + + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->pending()->create(['user_id' => $user->id]); + + $consultation->update(['status' => ConsultationStatus::Rejected]); + + Mail::assertNotQueued(BookingApprovedMail::class); +}); + +test('does not send email when consultation is created as approved', function () { + Mail::fake(); + + $user = User::factory()->create(['preferred_language' => 'ar']); + Consultation::factory()->approved()->create(['user_id' => $user->id]); + + Mail::assertNotQueued(BookingApprovedMail::class); +}); + +test('does not send email when other fields change on approved consultation', function () { + Mail::fake(); + + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + + $consultation->update(['problem_summary' => 'Updated summary']); + + Mail::assertNotQueued(BookingApprovedMail::class); +}); + +test('includes ics attachment', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + $attachments = $mailable->attachments(); + + expect($attachments)->toHaveCount(1); + expect($attachments[0]->as)->toBe('consultation.ics'); +}); + +test('uses Arabic template for Arabic-preferring users', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->content()->markdown)->toBe('emails.booking.approved.ar'); +}); + +test('uses English template for English-preferring users', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->content()->markdown)->toBe('emails.booking.approved.en'); +}); + +test('includes payment instructions for paid consultations', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->create([ + 'user_id' => $user->id, + 'consultation_type' => ConsultationType::Paid, + 'payment_amount' => 150.00, + 'payment_status' => PaymentStatus::Pending, + ]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + $paymentInstructions = 'Please pay 150 ILS before your consultation.'; + + $mailable = new BookingApprovedMail($consultation, $icsContent, $paymentInstructions); + + expect($mailable->paymentInstructions)->toBe($paymentInstructions); +}); + +test('excludes payment instructions for free consultations', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->free()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->paymentInstructions)->toBeNull(); +}); + +test('has correct Arabic subject', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->envelope()->subject)->toBe('تمت الموافقة على استشارتك'); +}); + +test('has correct English subject', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->envelope()->subject)->toBe('Your Consultation Has Been Approved'); +}); + +test('date is formatted as d/m/Y for Arabic users', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->create([ + 'user_id' => $user->id, + 'booking_date' => '2025-03-15', + ]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->getFormattedDate('ar'))->toBe('15/03/2025'); +}); + +test('date is formatted as m/d/Y for English users', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->create([ + 'user_id' => $user->id, + 'booking_date' => '2025-03-15', + ]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->getFormattedDate('en'))->toBe('03/15/2025'); +}); + +test('time is formatted as h:i A', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->create([ + 'user_id' => $user->id, + 'booking_time' => '14:30:00', + ]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->getFormattedTime())->toBe('02:30 PM'); +}); + +test('consultation type label is correct in Arabic', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + + $freeConsultation = Consultation::factory()->approved()->free()->create(['user_id' => $user->id]); + $paidConsultation = Consultation::factory()->approved()->paid()->create(['user_id' => $user->id]); + + $freeIcs = app(CalendarService::class)->generateIcs($freeConsultation); + $paidIcs = app(CalendarService::class)->generateIcs($paidConsultation); + + $freeMailable = new BookingApprovedMail($freeConsultation, $freeIcs); + $paidMailable = new BookingApprovedMail($paidConsultation, $paidIcs); + + expect($freeMailable->getConsultationTypeLabel('ar'))->toBe('مجانية'); + expect($paidMailable->getConsultationTypeLabel('ar'))->toBe('مدفوعة'); +}); + +test('consultation type label is correct in English', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + + $freeConsultation = Consultation::factory()->approved()->free()->create(['user_id' => $user->id]); + $paidConsultation = Consultation::factory()->approved()->paid()->create(['user_id' => $user->id]); + + $freeIcs = app(CalendarService::class)->generateIcs($freeConsultation); + $paidIcs = app(CalendarService::class)->generateIcs($paidConsultation); + + $freeMailable = new BookingApprovedMail($freeConsultation, $freeIcs); + $paidMailable = new BookingApprovedMail($paidConsultation, $paidIcs); + + expect($freeMailable->getConsultationTypeLabel('en'))->toBe('Free'); + expect($paidMailable->getConsultationTypeLabel('en'))->toBe('Paid'); +}); + +test('booking approved email implements ShouldQueue', function () { + expect(BookingApprovedMail::class)->toImplement(ShouldQueue::class); +}); + +test('content includes all required data', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->create([ + 'user_id' => $user->id, + 'booking_date' => '2025-03-15', + 'booking_time' => '10:00:00', + ]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + $content = $mailable->content(); + + expect($content->with) + ->toHaveKey('consultation') + ->toHaveKey('user') + ->toHaveKey('paymentInstructions') + ->toHaveKey('formattedDate') + ->toHaveKey('formattedTime') + ->toHaveKey('duration') + ->toHaveKey('consultationType') + ->toHaveKey('isPaid') + ->toHaveKey('paymentAmount'); +}); + +test('email has correct recipient when queued via observer', function () { + Mail::fake(); + + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->pending()->create(['user_id' => $user->id]); + + $consultation->update(['status' => ConsultationStatus::Approved]); + + Mail::assertQueued(BookingApprovedMail::class, function ($mail) use ($user) { + return $mail->hasTo($user->email); + }); +}); + +test('email renders without errors in Arabic', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + $rendered = $mailable->render(); + + expect($rendered)->toContain('تمت الموافقة على استشارتك'); +}); + +test('email renders without errors in English', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + $rendered = $mailable->render(); + + expect($rendered)->toContain('Your Consultation Has Been Approved'); +}); + +test('paid consultation email includes payment amount', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->create([ + 'user_id' => $user->id, + 'consultation_type' => ConsultationType::Paid, + 'payment_amount' => 250.00, + 'payment_status' => PaymentStatus::Pending, + ]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + $paymentInstructions = 'Please pay before your appointment.'; + + $mailable = new BookingApprovedMail($consultation, $icsContent, $paymentInstructions); + $rendered = $mailable->render(); + + expect($rendered)->toContain('250.00'); +}); + +test('free consultation email does not include payment section', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->free()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + $rendered = $mailable->render(); + + expect($rendered)->not->toContain('Payment Information'); +}); + +test('defaults to Arabic when preferred_language is ar', function () { + $user = User::factory()->create(['preferred_language' => 'ar']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + + expect($mailable->envelope()->subject)->toBe('تمت الموافقة على استشارتك'); + expect($mailable->content()->markdown)->toBe('emails.booking.approved.ar'); +}); + +test('duration defaults to 45 minutes', function () { + $user = User::factory()->create(['preferred_language' => 'en']); + $consultation = Consultation::factory()->approved()->create(['user_id' => $user->id]); + $icsContent = app(CalendarService::class)->generateIcs($consultation); + + $mailable = new BookingApprovedMail($consultation, $icsContent); + $content = $mailable->content(); + + expect($content->with['duration'])->toBe(45); +});