reviewed epic 8 stories
This commit is contained in:
@@ -3,66 +3,276 @@
|
||||
## Epic Reference
|
||||
**Epic 8:** Email Notification System
|
||||
|
||||
## Dependencies
|
||||
- **Story 8.1:** Email infrastructure setup (base template, queue config, SMTP)
|
||||
- **Story 8.4:** BookingApprovedEmail pattern and CalendarService integration
|
||||
- **Story 8.6:** Migration that adds `reminder_2h_sent_at` column to consultations table
|
||||
- **Consultation Model:** Must have `status`, `scheduled_date`, `scheduled_time`, `consultation_type`, `payment_status` fields
|
||||
- **User Model:** Must have `preferred_language` field
|
||||
|
||||
## User Story
|
||||
As a **client**,
|
||||
I want **to receive a reminder 2 hours before my consultation**,
|
||||
So that **I'm prepared and ready**.
|
||||
So that **I'm prepared and ready for my appointment with final details and contact information**.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Trigger
|
||||
- [ ] Scheduled job runs every 15 minutes
|
||||
- [ ] Find consultations 2 hours away
|
||||
- [ ] Only for approved consultations
|
||||
- [ ] Skip cancelled/no-show
|
||||
- [ ] Scheduled artisan command runs every 15 minutes
|
||||
- [ ] Find consultations approximately 2 hours away (within 7-minute window)
|
||||
- [ ] Only for approved consultations (`status = 'approved'`)
|
||||
- [ ] Skip cancelled/no-show/completed consultations
|
||||
- [ ] Track sent reminders to prevent duplicates via `reminder_2h_sent_at`
|
||||
|
||||
### Content
|
||||
- [ ] "Your consultation is in 2 hours"
|
||||
- [ ] Date and time
|
||||
- [ ] Final payment reminder (if applicable)
|
||||
- [ ] Contact info for last-minute issues
|
||||
- [ ] Subject: "Your consultation is in 2 hours" / "استشارتك بعد ساعتين"
|
||||
- [ ] Consultation date and time (formatted per locale)
|
||||
- [ ] Final payment reminder: Show if `consultation_type = 'paid'` AND `payment_status != 'received'`
|
||||
- [ ] Office contact information for last-minute issues/questions
|
||||
|
||||
### Language
|
||||
- [ ] Email in client's preferred language
|
||||
- [ ] Email rendered in client's `preferred_language` (ar/en)
|
||||
- [ ] Date/time formatted according to locale
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Artisan Command
|
||||
```php
|
||||
// Command: php artisan reminders:send-2h
|
||||
// Schedule: everyFifteenMinutes
|
||||
// app/Console/Commands/Send2HourReminders.php
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Consultation;
|
||||
use App\Notifications\ConsultationReminder2h;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class Send2HourReminders extends Command
|
||||
{
|
||||
protected $signature = 'reminders:send-2h';
|
||||
protected $description = 'Send 2-hour consultation reminders';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$targetTime = now()->addHours(2);
|
||||
$windowStart = $targetTime->copy()->subMinutes(7);
|
||||
$windowEnd = $targetTime->copy()->addMinutes(7);
|
||||
|
||||
Consultation::where('status', 'approved')
|
||||
$consultations = Consultation::query()
|
||||
->where('status', 'approved')
|
||||
->whereNull('reminder_2h_sent_at')
|
||||
->whereDate('scheduled_date', today())
|
||||
->get()
|
||||
->filter(function ($c) use ($windowStart, $windowEnd) {
|
||||
$time = Carbon::parse($c->scheduled_date->format('Y-m-d') . ' ' . $c->scheduled_time);
|
||||
return $time->between($windowStart, $windowEnd);
|
||||
})
|
||||
->each(function ($consultation) {
|
||||
$consultation->user->notify(new ConsultationReminder2h($consultation));
|
||||
$consultation->update(['reminder_2h_sent_at' => now()]);
|
||||
->filter(function ($consultation) use ($windowStart, $windowEnd) {
|
||||
$consultationDateTime = Carbon::parse(
|
||||
$consultation->scheduled_date->format('Y-m-d') . ' ' . $consultation->scheduled_time
|
||||
);
|
||||
return $consultationDateTime->between($windowStart, $windowEnd);
|
||||
});
|
||||
|
||||
$count = 0;
|
||||
foreach ($consultations as $consultation) {
|
||||
$consultation->user->notify(new ConsultationReminder2h($consultation));
|
||||
$consultation->update(['reminder_2h_sent_at' => now()]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->info("Sent {$count} 2-hour reminder(s).");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notification Class
|
||||
```php
|
||||
// app/Notifications/ConsultationReminder2h.php
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Consultation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ConsultationReminder2h extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public Consultation $consultation
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$locale = $notifiable->preferred_language ?? 'ar';
|
||||
$consultation = $this->consultation;
|
||||
|
||||
$subject = $locale === 'ar'
|
||||
? 'استشارتك بعد ساعتين'
|
||||
: 'Your consultation is in 2 hours';
|
||||
|
||||
return (new MailMessage)
|
||||
->subject($subject)
|
||||
->markdown("emails.reminders.consultation-2h.{$locale}", [
|
||||
'consultation' => $consultation,
|
||||
'user' => $notifiable,
|
||||
'showPaymentReminder' => $this->shouldShowPaymentReminder(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function shouldShowPaymentReminder(): bool
|
||||
{
|
||||
return $this->consultation->consultation_type === 'paid'
|
||||
&& $this->consultation->payment_status !== 'received';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Email Templates
|
||||
|
||||
**Arabic Template:** `resources/views/emails/reminders/consultation-2h/ar.blade.php`
|
||||
**English Template:** `resources/views/emails/reminders/consultation-2h/en.blade.php`
|
||||
|
||||
Template content should include:
|
||||
- Greeting with client name
|
||||
- Urgent reminder message ("Your consultation is in 2 hours")
|
||||
- Date/time (formatted: `scheduled_date->translatedFormat()`)
|
||||
- **Final payment reminder section** (conditional) - more urgent tone than 24h reminder
|
||||
- Office contact information for last-minute issues (phone, email)
|
||||
- Branded footer (from Story 8.1 base template)
|
||||
|
||||
**Note:** Unlike the 24-hour reminder, this template does NOT include a calendar download link (client should already have it from approval email and 24h reminder).
|
||||
|
||||
### Schedule Registration
|
||||
```php
|
||||
// routes/console.php or bootstrap/app.php
|
||||
Schedule::command('reminders:send-2h')->everyFifteenMinutes();
|
||||
```
|
||||
|
||||
**Why 15 minutes?** The 2-hour reminder uses a 7-minute window (tighter than 24h's 30-minute window) because timing is more critical close to the appointment. Running every 15 minutes ensures consultations are caught within the window while balancing server load.
|
||||
|
||||
## Edge Cases & Error Handling
|
||||
|
||||
| Scenario | Handling |
|
||||
|----------|----------|
|
||||
| Notification fails to send | Queue will retry; failed jobs logged to `failed_jobs` table |
|
||||
| Consultation rescheduled after 24h reminder but before 2h | New datetime will trigger 2h reminder (tracking column is separate) |
|
||||
| Consultation cancelled after reminder sent | No action needed - reminder already sent |
|
||||
| User has no email | Notification skipped (Laravel handles gracefully) |
|
||||
| Timezone considerations | All times stored/compared in app timezone (configured in `config/app.php`) |
|
||||
| 24h reminder not sent (e.g., booking made same day) | 2h reminder still sends independently |
|
||||
| Consultation scheduled less than 2 hours away | Won't receive 2h reminder (outside window) |
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Unit Tests (`tests/Unit/Commands/Send2HourRemindersTest.php`)
|
||||
```php
|
||||
test('command finds consultations approximately 2 hours away', function () {
|
||||
// Create consultation 2 hours from now
|
||||
// Run command
|
||||
// Assert notification sent
|
||||
// Assert reminder_2h_sent_at is set
|
||||
});
|
||||
|
||||
test('command skips consultations with reminder already sent', function () {
|
||||
// Create consultation with reminder_2h_sent_at already set
|
||||
// Run command
|
||||
// Assert no notification sent
|
||||
});
|
||||
|
||||
test('command skips non-approved consultations', function () {
|
||||
// Create cancelled, no-show, pending, completed consultations
|
||||
// Run command
|
||||
// Assert no notifications sent
|
||||
});
|
||||
|
||||
test('command uses 7-minute window for matching', function () {
|
||||
// Create consultation at exactly 2h + 8 minutes (outside window)
|
||||
// Run command
|
||||
// Assert no notification sent
|
||||
|
||||
// Create consultation at exactly 2h + 6 minutes (inside window)
|
||||
// Run command
|
||||
// Assert notification sent
|
||||
});
|
||||
|
||||
test('command only checks consultations scheduled for today', function () {
|
||||
// Create consultation 2 hours from now but tomorrow's date
|
||||
// Run command
|
||||
// Assert no notification sent
|
||||
});
|
||||
```
|
||||
|
||||
### Feature Tests (`tests/Feature/Notifications/ConsultationReminder2hTest.php`)
|
||||
```php
|
||||
test('reminder email contains correct consultation details', function () {
|
||||
// Create consultation
|
||||
// Send notification
|
||||
// Assert email contains date, time
|
||||
});
|
||||
|
||||
test('final payment reminder shown for unpaid paid consultations', function () {
|
||||
// Create paid consultation with payment_status = 'pending'
|
||||
// Assert email contains payment reminder section
|
||||
});
|
||||
|
||||
test('payment reminder hidden when payment received', function () {
|
||||
// Create paid consultation with payment_status = 'received'
|
||||
// Assert email does NOT contain payment reminder
|
||||
});
|
||||
|
||||
test('email uses client preferred language', function () {
|
||||
// Create user with preferred_language = 'en'
|
||||
// Assert email template is English version
|
||||
});
|
||||
|
||||
test('email includes office contact information', function () {
|
||||
// Send notification
|
||||
// Assert email contains contact phone/email
|
||||
});
|
||||
|
||||
test('email does not include calendar download link', function () {
|
||||
// Send notification
|
||||
// Assert email does NOT contain calendar route
|
||||
});
|
||||
```
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `app/Console/Commands/Send2HourReminders.php` | CREATE |
|
||||
| `app/Notifications/ConsultationReminder2h.php` | CREATE |
|
||||
| `resources/views/emails/reminders/consultation-2h/ar.blade.php` | CREATE |
|
||||
| `resources/views/emails/reminders/consultation-2h/en.blade.php` | CREATE |
|
||||
| `routes/console.php` | MODIFY (add schedule) |
|
||||
| `tests/Unit/Commands/Send2HourRemindersTest.php` | CREATE |
|
||||
| `tests/Feature/Notifications/ConsultationReminder2hTest.php` | CREATE |
|
||||
|
||||
**Note:** Migration for `reminder_2h_sent_at` column is handled in Story 8.6.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Command runs successfully
|
||||
- [ ] Correct timing (2 hours before)
|
||||
- [ ] Payment reminder if unpaid
|
||||
- [ ] No duplicate reminders
|
||||
- [ ] Bilingual templates
|
||||
- [ ] Tests pass
|
||||
- [ ] Artisan command `reminders:send-2h` created and works
|
||||
- [ ] Command scheduled to run every 15 minutes
|
||||
- [ ] Notification class implements `ShouldQueue`
|
||||
- [ ] Reminders only sent for approved consultations within 2h window (7-min tolerance)
|
||||
- [ ] No duplicate reminders (tracking column `reminder_2h_sent_at` updated)
|
||||
- [ ] Payment reminder shown only when `paid` AND `payment_status != 'received'`
|
||||
- [ ] Contact information for last-minute issues included
|
||||
- [ ] Bilingual email templates (Arabic/English)
|
||||
- [ ] All unit and feature tests pass
|
||||
- [ ] Code formatted with `vendor/bin/pint`
|
||||
|
||||
## References
|
||||
- **PRD Section 5.4:** Email Notifications - "Consultation reminder (2 hours before)"
|
||||
- **PRD Section 8.2:** Email Templates - Template requirements and branding
|
||||
- **Story 8.1:** Base email template and queue configuration
|
||||
- **Story 8.6:** Migration for reminder columns, similar command/notification pattern
|
||||
|
||||
## Estimation
|
||||
**Complexity:** Medium | **Effort:** 2-3 hours
|
||||
|
||||
Reference in New Issue
Block a user