14 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 Userscheduled_date- Date of consultationscheduled_time- Time of consultationduration- 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 templateresources/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_languagedefaults 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 configdocs/stories/story-3.6-calendar-file-generation.md- CalendarService for .ics generationdocs/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
Dev Agent Record
Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
Completion Notes
- Created
BookingApprovedMailMailable class with .ics attachment support - Created bilingual email templates (Arabic/English) with RTL support
- Created
ConsultationObserverto 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
ShouldQueuefor 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
BookingApprovedMailcorrectly 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
- All acceptance criteria implemented and tested
- Bilingual templates with RTL support
- Queue-based email delivery
- Observer-based trigger mechanism
- ICS attachment generation
- Payment instructions for paid consultations
- 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