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

10 KiB

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

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

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

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

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)

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