libra/docs/stories/story-8.6-consultation-remi...

14 KiB

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:

// 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

// 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

// 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

// 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)

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)

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

  • All acceptance criteria implemented
  • All tests passing (7 for 24h reminders)
  • Migration adds required columns
  • Schedule registered in console routes
  • Notification implements ShouldQueue
  • Error handling with logging in command
  • 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

Ready for Done - All acceptance criteria met, all tests passing, code quality excellent