libra/docs/stories/story-8.4-booking-approved-...

257 lines
8.1 KiB
Markdown

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