# Story 3.8: Consultation Reminders ## Epic Reference **Epic 3:** Booking & Consultation System ## User Story As a **client**, I want **to receive reminder emails before my consultation**, So that **I don't forget my appointment and can prepare accordingly**. ## Story Context ### Existing System Integration - **Integrates with:** consultations table, Laravel scheduler, email notifications - **Technology:** Laravel Queue, Scheduler, Mailable - **Follows pattern:** Scheduled job pattern - **Touch points:** Email system, consultation status ## Acceptance Criteria ### 24-Hour Reminder - [ ] Sent 24 hours before consultation - [ ] Includes: - Consultation date and time - Type (free/paid) - Payment reminder if paid and not received - Calendar file link - Any special instructions ### 2-Hour Reminder - [ ] Sent 2 hours before consultation - [ ] Includes: - Consultation date and time - Final payment reminder if applicable - Contact information for last-minute issues ### Reminder Logic - [ ] Only for approved consultations - [ ] Skip cancelled consultations - [ ] Skip no-show consultations - [ ] Don't send duplicate reminders - [ ] Handle timezone correctly ### Language Support - [ ] Email in client's preferred language - [ ] Arabic template for Arabic preference - [ ] English template for English preference ### Quality Requirements - [ ] Scheduled jobs run reliably - [ ] Retry on failure - [ ] Logging for debugging - [ ] Tests for reminder logic ## Technical Notes ### Reminder Commands #### 24-Hour Reminder Command ```php addHours(24); $windowStart = $targetTime->copy()->subMinutes(30); $windowEnd = $targetTime->copy()->addMinutes(30); $consultations = Consultation::where('status', 'approved') ->whereNull('reminder_24h_sent_at') ->whereDate('scheduled_date', $targetTime->toDateString()) ->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) { try { $consultation->user->notify(new ConsultationReminder24h($consultation)); $consultation->update(['reminder_24h_sent_at' => now()]); $count++; $this->info("Sent 24h reminder for consultation #{$consultation->id}"); } catch (\Exception $e) { $this->error("Failed to send reminder for consultation #{$consultation->id}: {$e->getMessage()}"); \Log::error('24h reminder failed', [ 'consultation_id' => $consultation->id, 'error' => $e->getMessage(), ]); } } $this->info("Sent {$count} 24-hour reminders"); return Command::SUCCESS; } } ``` #### 2-Hour Reminder Command ```php addHours(2); $windowStart = $targetTime->copy()->subMinutes(15); $windowEnd = $targetTime->copy()->addMinutes(15); $consultations = Consultation::where('status', 'approved') ->whereNull('reminder_2h_sent_at') ->whereDate('scheduled_date', $targetTime->toDateString()) ->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) { try { $consultation->user->notify(new ConsultationReminder2h($consultation)); $consultation->update(['reminder_2h_sent_at' => now()]); $count++; $this->info("Sent 2h reminder for consultation #{$consultation->id}"); } catch (\Exception $e) { $this->error("Failed to send reminder for consultation #{$consultation->id}: {$e->getMessage()}"); \Log::error('2h reminder failed', [ 'consultation_id' => $consultation->id, 'error' => $e->getMessage(), ]); } } $this->info("Sent {$count} 2-hour reminders"); return Command::SUCCESS; } } ``` ### Scheduler Configuration ```php // In routes/console.php or app/Console/Kernel.php (Laravel 12) use Illuminate\Support\Facades\Schedule; Schedule::command('reminders:send-24h')->hourly(); Schedule::command('reminders:send-2h')->everyFifteenMinutes(); ``` ### Migration for Tracking ```php // Add reminder tracking columns to consultations Schema::table('consultations', function (Blueprint $table) { $table->timestamp('reminder_24h_sent_at')->nullable(); $table->timestamp('reminder_2h_sent_at')->nullable(); }); ``` ### 24-Hour Reminder Notification ```php preferred_language ?? 'ar'; return (new MailMessage) ->subject($this->getSubject($locale)) ->markdown('emails.reminders.24h.' . $locale, [ 'consultation' => $this->consultation, 'user' => $notifiable, 'showPaymentReminder' => $this->shouldShowPaymentReminder(), ]); } private function getSubject(string $locale): string { return $locale === 'ar' ? 'تذكير: موعدك غداً مع مكتب ليبرا للمحاماة' : 'Reminder: Your consultation is tomorrow'; } private function shouldShowPaymentReminder(): bool { return $this->consultation->type === 'paid' && $this->consultation->payment_status === 'pending'; } } ``` ### Email Template (24h Arabic) ```blade # تذكير بموعدك عزيزي {{ $user->name }}، نود تذكيرك بموعد استشارتك غداً: **التاريخ:** {{ $consultation->scheduled_date->translatedFormat('l، d F Y') }} **الوقت:** {{ \Carbon\Carbon::parse($consultation->scheduled_time)->format('g:i A') }} **المدة:** 45 دقيقة **النوع:** {{ $consultation->type === 'free' ? 'مجانية' : 'مدفوعة' }} @if($showPaymentReminder) **تذكير بالدفع:** يرجى إتمام عملية الدفع قبل موعد الاستشارة. المبلغ المطلوب: {{ number_format($consultation->payment_amount, 2) }} شيكل @endif تحميل ملف التقويم إذا كنت بحاجة لإعادة جدولة الموعد، يرجى التواصل معنا في أقرب وقت. مع أطيب التحيات، مكتب ليبرا للمحاماة ``` ### Email Template (2h Arabic) ```blade # موعدك بعد ساعتين عزيزي {{ $user->name }}، تذكير أخير: موعد استشارتك خلال ساعتين. **الوقت:** {{ \Carbon\Carbon::parse($consultation->scheduled_time)->format('g:i A') }} @if($showPaymentReminder) **هام:** لم نستلم الدفعة بعد. يرجى إتمام الدفع قبل بدء الاستشارة. @endif إذا كان لديك أي استفسار طارئ، يرجى التواصل معنا على: [رقم الهاتف] نتطلع للقائك، مكتب ليبرا للمحاماة ``` ### Testing ```php use App\Console\Commands\Send24HourReminders; use App\Models\Consultation; use App\Notifications\ConsultationReminder24h; use Illuminate\Support\Facades\Notification; it('sends 24h reminder for upcoming consultation', function () { Notification::fake(); $consultation = Consultation::factory()->approved()->create([ 'scheduled_date' => now()->addHours(24)->toDateString(), 'scheduled_time' => now()->addHours(24)->format('H:i:s'), 'reminder_24h_sent_at' => null, ]); $this->artisan('reminders:send-24h') ->assertSuccessful(); Notification::assertSentTo( $consultation->user, ConsultationReminder24h::class ); expect($consultation->fresh()->reminder_24h_sent_at)->not->toBeNull(); }); it('does not send reminder for cancelled consultation', function () { Notification::fake(); $consultation = Consultation::factory()->create([ 'status' => 'cancelled', 'scheduled_date' => now()->addHours(24)->toDateString(), 'scheduled_time' => now()->addHours(24)->format('H:i:s'), ]); $this->artisan('reminders:send-24h') ->assertSuccessful(); Notification::assertNotSentTo($consultation->user, ConsultationReminder24h::class); }); ``` ## Definition of Done - [ ] 24h reminder command works - [ ] 2h reminder command works - [ ] Scheduler configured correctly - [ ] Only approved consultations receive reminders - [ ] Cancelled/no-show don't receive reminders - [ ] No duplicate reminders sent - [ ] Payment reminder included when applicable - [ ] Arabic emails work correctly - [ ] English emails work correctly - [ ] Logging for debugging - [ ] Tests pass for reminder logic - [ ] Code formatted with Pint ## Dependencies - **Story 3.5:** Booking approval (creates approved consultations) - **Epic 8:** Email infrastructure ## Risk Assessment - **Primary Risk:** Scheduler not running - **Mitigation:** Monitor scheduler, alert on failures - **Rollback:** Manual reminder sending if needed ## Estimation **Complexity:** Medium **Estimated Effort:** 3-4 hours