# Story 8.7: Consultation Reminder (2 Hours) ## 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 for my appointment with final details and contact information**. ## Acceptance Criteria ### Trigger - [ ] 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 - [ ] 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 rendered in client's `preferred_language` (ar/en) - [ ] Date/time formatted according to locale ## Technical Notes ### Artisan Command ```php // 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); $consultations = Consultation::query() ->where('status', 'approved') ->whereNull('reminder_2h_sent_at') ->whereDate('scheduled_date', today()) ->get() ->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 - [ ] 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