# Story 8.6: Consultation Reminder (24 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 - **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 24 hours before my consultation**, So that **I don't forget my appointment**. ## Acceptance Criteria ### Trigger - [ ] Scheduled artisan command runs hourly - [ ] Find consultations approximately 24 hours away (within 30-minute window) - [ ] Only for approved consultations (`status = 'approved'`) - [ ] Skip cancelled/no-show/completed consultations - [ ] Track sent reminders to prevent duplicates ### Content - [ ] Subject: "Reminder: Your consultation is tomorrow" / "تذكير: استشارتك غدًا" - [ ] Consultation date and time (formatted per locale) - [ ] Consultation type (free/paid) - [ ] Payment reminder: Show if `consultation_type = 'paid'` AND `payment_status != 'received'` - [ ] Calendar file download link (using route to CalendarService) - [ ] Office contact information for questions ### Language - [ ] Email rendered in client's `preferred_language` (ar/en) - [ ] Date/time formatted according to locale ## Technical Notes ### Database Migration Add tracking column to prevent duplicate reminders: ```php // database/migrations/xxxx_add_reminder_sent_columns_to_consultations_table.php Schema::table('consultations', function (Blueprint $table) { $table->timestamp('reminder_24h_sent_at')->nullable()->after('status'); $table->timestamp('reminder_2h_sent_at')->nullable()->after('reminder_24h_sent_at'); }); ``` ### Artisan Command ```php // app/Console/Commands/Send24HourReminders.php namespace App\Console\Commands; use App\Models\Consultation; use App\Notifications\ConsultationReminder24h; use Carbon\Carbon; use Illuminate\Console\Command; class Send24HourReminders extends Command { protected $signature = 'reminders:send-24h'; protected $description = 'Send 24-hour consultation reminders'; public function handle(): int { $targetTime = now()->addHours(24); $windowStart = $targetTime->copy()->subMinutes(30); $windowEnd = $targetTime->copy()->addMinutes(30); $consultations = Consultation::query() ->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) { $consultation->user->notify(new ConsultationReminder24h($consultation)); $consultation->update(['reminder_24h_sent_at' => now()]); $count++; } $this->info("Sent {$count} reminder(s)."); return Command::SUCCESS; } } ``` ### Notification Class ```php // app/Notifications/ConsultationReminder24h.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 ConsultationReminder24h 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' ? 'تذكير: استشارتك غدًا' : 'Reminder: Your consultation is tomorrow'; $message = (new MailMessage) ->subject($subject) ->markdown("emails.reminders.consultation-24h.{$locale}", [ 'consultation' => $consultation, 'user' => $notifiable, 'showPaymentReminder' => $this->shouldShowPaymentReminder(), 'calendarUrl' => route('consultations.calendar', $consultation), ]); return $message; } private function shouldShowPaymentReminder(): bool { return $this->consultation->consultation_type === 'paid' && $this->consultation->payment_status !== 'received'; } } ``` ### Email Templates **Arabic Template:** `resources/views/emails/reminders/consultation-24h/ar.blade.php` **English Template:** `resources/views/emails/reminders/consultation-24h/en.blade.php` Template content should include: - Greeting with client name - Reminder message ("Your consultation is scheduled for tomorrow") - Date/time (formatted: `scheduled_date->translatedFormat()`) - Consultation type badge - Payment reminder section (conditional) - "Add to Calendar" button linking to `$calendarUrl` - Office contact information - Branded footer (from Story 8.1 base template) ### Schedule Registration ```php // routes/console.php or bootstrap/app.php Schedule::command('reminders:send-24h')->hourly(); ``` ## Edge Cases & Error Handling | Scenario | Handling | |----------|----------| | Notification fails to send | Queue will retry; failed jobs logged to `failed_jobs` table | | Consultation rescheduled after reminder | New datetime won't trigger duplicate (24h check resets) | | 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`) | ## Test Scenarios ### Unit Tests (`tests/Unit/Commands/Send24HourRemindersTest.php`) ```php test('command finds consultations approximately 24 hours away', function () { // Create consultation 24 hours from now // Run command // Assert notification sent }); test('command skips consultations with reminder already sent', function () { // Create consultation with reminder_24h_sent_at set // Run command // Assert no notification sent }); test('command skips non-approved consultations', function () { // Create cancelled, no-show, pending consultations // Run command // Assert no notifications sent }); ``` ### Feature Tests (`tests/Feature/Notifications/ConsultationReminder24hTest.php`) ```php test('reminder email contains correct consultation details', function () { // Create consultation // Send notification // Assert email contains date, time, type }); test('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('calendar download link is included', function () { // Assert email contains route('consultations.calendar', $consultation) }); ``` ## Files to Create/Modify | File | Action | |------|--------| | `database/migrations/xxxx_add_reminder_sent_columns_to_consultations_table.php` | CREATE | | `app/Console/Commands/Send24HourReminders.php` | CREATE | | `app/Notifications/ConsultationReminder24h.php` | CREATE | | `resources/views/emails/reminders/consultation-24h/ar.blade.php` | CREATE | | `resources/views/emails/reminders/consultation-24h/en.blade.php` | CREATE | | `routes/console.php` | MODIFY (add schedule) | | `tests/Unit/Commands/Send24HourRemindersTest.php` | CREATE | | `tests/Feature/Notifications/ConsultationReminder24hTest.php` | CREATE | ## Definition of Done - [ ] Migration adds `reminder_24h_sent_at` column to consultations table - [ ] Artisan command `reminders:send-24h` created and works - [ ] Command scheduled to run hourly - [ ] Notification class implements `ShouldQueue` - [ ] Reminders only sent for approved consultations within 24h window - [ ] No duplicate reminders (tracking column updated) - [ ] Payment reminder shown only when `paid` AND `payment_status != 'received'` - [ ] Calendar download link 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 (24 hours before)" - **PRD Section 8.2:** Email Templates - Template requirements and branding - **Story 8.1:** Base email template and queue configuration - **Story 8.4:** Pattern for calendar file attachment/linking ## Estimation **Complexity:** Medium | **Effort:** 3 hours --- ## Dev Agent Record ### Status Ready for Review ### Agent Model Used Claude Opus 4.5 (claude-opus-4-5-20251101) ### Completion Notes - All implementation was already completed from previous story work (8.5 included 2h reminders which shared infrastructure) - Added missing test for completed consultations (`it does not send 24h reminder for completed consultation`) - Added office contact information panel to email template - Added `office_location` translation key to both ar/en emails.php - All 12 reminder tests pass (7 for 24h, 5 for 2h) - Code formatted with Pint ### File List | File | Action | |------|--------| | `database/migrations/2025_12_26_180923_add_reminder_columns_to_consultations_table.php` | EXISTS | | `app/Console/Commands/Send24HourReminders.php` | EXISTS | | `app/Notifications/ConsultationReminder24h.php` | EXISTS | | `resources/views/emails/reminder-24h.blade.php` | MODIFIED (added office location panel) | | `routes/console.php` | EXISTS (schedule registered) | | `tests/Feature/ConsultationReminderTest.php` | MODIFIED (added completed consultation test) | | `lang/en/emails.php` | MODIFIED (added office_location key) | | `lang/ar/emails.php` | MODIFIED (added office_location key) | | `app/Models/Consultation.php` | EXISTS (has reminder columns in fillable/casts) | ### Change Log | Date | Change | |------|--------| | 2026-01-02 | Added test for completed consultations not receiving reminders | | 2026-01-02 | Added office location panel to reminder-24h email template | | 2026-01-02 | Added office_location translation keys to en/ar emails.php | ### Debug Log References N/A - No debug issues encountered --- ## QA Results ### Review Date: 2026-01-02 ### Reviewed By: Quinn (Test Architect) ### Code Quality Assessment **Overall: Excellent implementation.** The code follows Laravel best practices and implements all acceptance criteria effectively. The implementation demonstrates: - Clean separation of concerns (Command, Notification, Views) - Proper use of Laravel's queue system (`ShouldQueue`) - Type-safe enum usage for status checks - Comprehensive error handling with logging - Bilingual support using Laravel's localization system ### Refactoring Performed None required. The code is well-structured and follows project patterns established in previous stories. ### Compliance Check - Coding Standards: ✓ Follows PSR-12 and Laravel conventions - Project Structure: ✓ Files in correct locations per architecture docs - Testing Strategy: ✓ 7 dedicated tests for 24h reminder functionality - All ACs Met: ✓ See traceability below ### Requirements Traceability | AC # | Acceptance Criteria | Test Coverage | |------|---------------------|---------------| | 1 | Scheduled artisan command runs hourly | ✓ `routes/console.php:12` - `Schedule::command('reminders:send-24h')->hourly()` | | 2 | Find consultations ~24h away (30-min window) | ✓ `Send24HourReminders.php:33-49` - Tested in `it sends 24h reminder for upcoming consultation` | | 3 | Only for approved consultations | ✓ Tested in `it does not send 24h reminder for pending/cancelled/no-show/completed consultation` | | 4 | Track sent reminders to prevent duplicates | ✓ `reminder_24h_sent_at` column + Tested in `it does not send duplicate 24h reminders` | | 5 | Subject in Arabic/English | ✓ `ConsultationReminder24h.php:52-57` | | 6 | Consultation date/time formatted | ✓ `reminder-24h.blade.php:15-16, 51-52` with `translatedFormat()` | | 7 | Consultation type displayed | ✓ `reminder-24h.blade.php:18, 55` | | 8 | Payment reminder conditional | ✓ `shouldShowPaymentReminder()` method + Tested in `it includes payment reminder for unpaid consultations` | | 9 | Calendar download link | ✓ `reminder-24h.blade.php:34, 71` using `route('client.consultations.calendar')` | | 10 | Office contact information | ✓ `reminder-24h.blade.php:28-32, 65-69` | | 11 | Client preferred language | ✓ Tested in `it respects user language preference for 24h reminders` | ### Improvements Checklist - [x] All acceptance criteria implemented - [x] All tests passing (7 for 24h reminders) - [x] Migration adds required columns - [x] Schedule registered in console routes - [x] Notification implements ShouldQueue - [x] Error handling with logging in command - [x] Bilingual email templates (ar/en) - [ ] *Minor observation*: Payment reminder check uses `=== 'pending'` vs story's `!= 'received'`. Functionally equivalent for current enum values but could be made more defensive. ### Security Review No security concerns identified: - No user input directly rendered (XSS safe) - Route uses Laravel's model binding with authorization - Queue jobs execute in controlled environment ### Performance Considerations No performance issues identified: - Query filters by date first, then filters in-memory (appropriate for typical consultation volume) - Each consultation processes independently with try/catch - Failed sends don't block other reminders ### Files Modified During Review None - implementation is complete and correct. ### Gate Status Gate: **PASS** → docs/qa/gates/8.6-consultation-reminder-24h.yml ### Recommended Status ✓ **Ready for Done** - All acceptance criteria met, all tests passing, code quality excellent