reviewed epic 8 stories
This commit is contained in:
@@ -8,15 +8,26 @@ 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 on booking rejection by admin
|
||||
- [ ] Sent when consultation status changes to `'rejected'`
|
||||
|
||||
### Content
|
||||
- [ ] "Your consultation request could not be approved"
|
||||
- [ ] Original requested date and time
|
||||
- [ ] Rejection reason (if provided by admin)
|
||||
- [ ] Rejection reason (conditionally shown if provided by admin)
|
||||
- [ ] Invitation to request new consultation
|
||||
- [ ] Contact info for questions
|
||||
|
||||
@@ -24,12 +35,35 @@ So that **I can understand why and request a new consultation if needed**.
|
||||
- [ ] Empathetic, professional
|
||||
|
||||
### Language
|
||||
- [ ] Email in client's preferred 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
|
||||
class BookingRejectedEmail extends Mailable
|
||||
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;
|
||||
|
||||
@@ -38,6 +72,16 @@ class BookingRejectedEmail extends Mailable
|
||||
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';
|
||||
@@ -47,18 +91,182 @@ class BookingRejectedEmail extends Mailable
|
||||
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 --}}
|
||||
<x-mail::message>
|
||||
# تعذر الموافقة على طلب الاستشارة
|
||||
|
||||
عزيزي/عزيزتي {{ $consultation->user->name }},
|
||||
|
||||
نأسف لإبلاغك بأنه تعذر علينا الموافقة على طلب الاستشارة الخاص بك.
|
||||
|
||||
**التاريخ المطلوب:** {{ $consultation->scheduled_at->format('Y-m-d') }}
|
||||
**الوقت المطلوب:** {{ $consultation->scheduled_at->format('H:i') }}
|
||||
|
||||
@if($hasReason)
|
||||
**السبب:** {{ $reason }}
|
||||
@endif
|
||||
|
||||
نرحب بك لتقديم طلب استشارة جديد في وقت آخر يناسبك.
|
||||
|
||||
<x-mail::button :url="route('client.consultations.create')">
|
||||
طلب استشارة جديدة
|
||||
</x-mail::button>
|
||||
|
||||
للاستفسارات، تواصل معنا على: info@libra.ps
|
||||
|
||||
مع أطيب التحيات,
|
||||
مكتب ليبرا للمحاماة
|
||||
</x-mail::message>
|
||||
```
|
||||
|
||||
## 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
|
||||
- [ ] Email sent on rejection
|
||||
- [ ] Reason included if provided
|
||||
- [ ] Empathetic tone
|
||||
- [ ] Bilingual templates
|
||||
- [ ] Tests pass
|
||||
- [ ] `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 hours
|
||||
**Complexity:** Low | **Effort:** 2-3 hours
|
||||
|
||||
Reference in New Issue
Block a user