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

20 KiB

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

Prerequisites & Assumptions

Consultation Model (from Story 3.4/3.5)

The Consultation model must exist with the following structure:

  • Fields:
    • status - enum: pending, approved, completed, cancelled, no_show
    • scheduled_date - date (cast to Carbon)
    • scheduled_time - time string (H:i:s)
    • type - enum: free, paid
    • payment_status - enum: pending, received, not_applicable
    • payment_amount - decimal, nullable
    • user_id - foreign key to users table
    • reminder_24h_sent_at - timestamp, nullable (added by this story)
    • reminder_2h_sent_at - timestamp, nullable (added by this story)
  • Relationships:
    • user() - BelongsTo User
  • Factory States:
    • approved() - sets status to 'approved'

User Model Requirements

  • preferred_language field must exist (values: ar, en)
  • This field should be added in Epic 1 or early user stories

Route Requirements

  • client.consultations.calendar route from Story 3.6 must exist for calendar file download link

Technical Notes

Reminder Commands

24-Hour Reminder Command

<?php

namespace App\Console\Commands;

use App\Models\Consultation;
use App\Notifications\ConsultationReminder24h;
use Illuminate\Console\Command;
use Carbon\Carbon;

class Send24HourReminders extends Command
{
    protected $signature = 'reminders:send-24h';
    protected $description = 'Send consultation reminders 24 hours before appointment';

    public function handle(): int
    {
        $targetTime = now()->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

namespace App\Console\Commands;

use App\Models\Consultation;
use App\Notifications\ConsultationReminder2h;
use Illuminate\Console\Command;
use Carbon\Carbon;

class Send2HourReminders extends Command
{
    protected $signature = 'reminders:send-2h';
    protected $description = 'Send consultation reminders 2 hours before appointment';

    public function handle(): int
    {
        $targetTime = now()->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

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

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

namespace App\Notifications;

use App\Models\Consultation;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class ConsultationReminder24h extends Notification
{
    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';

        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)

<!-- resources/views/emails/reminders/24h/ar.blade.php -->
<x-mail::message>
# تذكير بموعدك

عزيزي {{ $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)
<x-mail::panel>
**تذكير بالدفع:** يرجى إتمام عملية الدفع قبل موعد الاستشارة.
المبلغ المطلوب: {{ number_format($consultation->payment_amount, 2) }} شيكل
</x-mail::panel>
@endif

<x-mail::button :url="route('client.consultations.calendar', $consultation)">
تحميل ملف التقويم
</x-mail::button>

إذا كنت بحاجة لإعادة جدولة الموعد، يرجى التواصل معنا في أقرب وقت.

مع أطيب التحيات،
مكتب ليبرا للمحاماة
</x-mail::message>

Email Template (2h Arabic)

<!-- resources/views/emails/reminders/2h/ar.blade.php -->
<x-mail::message>
# موعدك بعد ساعتين

عزيزي {{ $user->name }}،

تذكير أخير: موعد استشارتك خلال ساعتين.

**الوقت:** {{ \Carbon\Carbon::parse($consultation->scheduled_time)->format('g:i A') }}

@if($showPaymentReminder)
<x-mail::panel>
**هام:** لم نستلم الدفعة بعد. يرجى إتمام الدفع قبل بدء الاستشارة.
</x-mail::panel>
@endif

إذا كان لديك أي استفسار طارئ، يرجى التواصل معنا على:
[رقم الهاتف]

نتطلع للقائك،
مكتب ليبرا للمحاماة
</x-mail::message>

Testing

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);
});

it('does not send reminder for no-show consultation', function () {
    Notification::fake();

    $consultation = Consultation::factory()->create([
        'status' => 'no_show',
        '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);
});

it('does not send duplicate 24h reminders', 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' => now()->subHour(), // Already sent
    ]);

    $this->artisan('reminders:send-24h')
        ->assertSuccessful();

    Notification::assertNotSentTo($consultation->user, ConsultationReminder24h::class);
});

it('sends 2h reminder for upcoming consultation', function () {
    Notification::fake();

    $consultation = Consultation::factory()->approved()->create([
        'scheduled_date' => now()->addHours(2)->toDateString(),
        'scheduled_time' => now()->addHours(2)->format('H:i:s'),
        'reminder_2h_sent_at' => null,
    ]);

    $this->artisan('reminders:send-2h')
        ->assertSuccessful();

    Notification::assertSentTo(
        $consultation->user,
        ConsultationReminder2h::class
    );

    expect($consultation->fresh()->reminder_2h_sent_at)->not->toBeNull();
});

it('includes payment reminder for unpaid consultations', function () {
    Notification::fake();

    $consultation = Consultation::factory()->approved()->create([
        'scheduled_date' => now()->addHours(24)->toDateString(),
        'scheduled_time' => now()->addHours(24)->format('H:i:s'),
        'type' => 'paid',
        'payment_status' => 'pending',
        'payment_amount' => 200.00,
    ]);

    $this->artisan('reminders:send-24h')
        ->assertSuccessful();

    Notification::assertSentTo(
        $consultation->user,
        ConsultationReminder24h::class,
        function ($notification) {
            return $notification->consultation->type === 'paid'
                && $notification->consultation->payment_status === 'pending';
        }
    );
});

it('respects user language preference for reminders', function () {
    Notification::fake();

    $user = User::factory()->create(['preferred_language' => 'en']);
    $consultation = Consultation::factory()->approved()->create([
        'user_id' => $user->id,
        'scheduled_date' => now()->addHours(24)->toDateString(),
        'scheduled_time' => now()->addHours(24)->format('H:i:s'),
    ]);

    $this->artisan('reminders:send-24h')
        ->assertSuccessful();

    Notification::assertSentTo($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.4: Consultation model and booking submission (app/Models/Consultation.php, database/factories/ConsultationFactory.php)
  • Story 3.5: Booking approval workflow (creates approved consultations)
  • Story 3.6: Calendar file generation (provides client.consultations.calendar route)
  • Epic 1: User model with preferred_language field (app/Models/User.php)
  • Epic 8: Email infrastructure (mail configuration, queue setup)

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


Dev Agent Record

Status

Ready for Review

Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

File List

File Action
database/migrations/2025_12_26_180923_add_reminder_columns_to_consultations_table.php Created
app/Models/Consultation.php Modified (added reminder columns to fillable/casts)
app/Notifications/ConsultationReminder24h.php Created
app/Notifications/ConsultationReminder2h.php Created
app/Console/Commands/Send24HourReminders.php Created
app/Console/Commands/Send2HourReminders.php Created
resources/views/emails/reminder-24h.blade.php Created
resources/views/emails/reminder-2h.blade.php Created
lang/ar/emails.php Modified (added reminder translations)
lang/en/emails.php Modified (added reminder translations)
routes/console.php Modified (added scheduler commands)
tests/Feature/ConsultationReminderTest.php Created

Change Log

  • Added reminder_24h_sent_at and reminder_2h_sent_at columns to consultations table
  • Created ConsultationReminder24h and ConsultationReminder2h notification classes with queue support
  • Created Send24HourReminders and Send2HourReminders artisan commands
  • Created bilingual email templates (Arabic/English) for both reminder types
  • Configured Laravel scheduler to run 24h reminders hourly and 2h reminders every 15 minutes
  • Added 11 comprehensive tests covering all acceptance criteria

Completion Notes

  • The story specified scheduled_date/scheduled_time fields but the existing model uses booking_date/booking_time - implementation adapted accordingly
  • The story specified type field but the existing model uses consultation_type - implementation adapted accordingly
  • Email templates follow the existing project pattern using @component('mail::message') with locale-based RTL/LTR support
  • Notifications implement ShouldQueue for async processing via Laravel's queue system
  • Window-based reminder logic prevents edge cases (24h: ±30min window, 2h: ±15min window)

QA Results

Review Date: 2025-12-26

Reviewed By: Quinn (Test Architect)

Code Quality Assessment

Implementation is solid and well-structured. The developer correctly adapted the story specifications to match the existing codebase conventions (using booking_date/booking_time instead of scheduled_date/scheduled_time, and consultation_type instead of type). The window-based reminder logic is a smart approach that handles edge cases around scheduler timing.

Key highlights:

  • Clean separation between Commands, Notifications, and Views
  • Proper use of PHP 8 enums throughout
  • Bilingual email templates with proper RTL/LTR support
  • Comprehensive test coverage with 11 tests all passing
  • Proper queue integration with ShouldQueue

Refactoring Performed

None required - implementation meets all quality standards.

Compliance Check

  • Coding Standards: ✓ Pint formatting verified
  • Project Structure: ✓ Files in correct locations per Laravel conventions
  • Testing Strategy: ✓ Feature tests cover all acceptance criteria
  • All ACs Met: ✓ All 15+ acceptance criteria validated

Improvements Checklist

  • Migration properly adds nullable timestamp columns
  • Model fillable and casts updated correctly
  • Commands use enum constants for status comparison
  • Notifications implement ShouldQueue for async processing
  • Email templates support both Arabic and English
  • Translations added to lang/ar and lang/en
  • Scheduler configured in routes/console.php
  • Error handling with try/catch and logging
  • All 11 tests passing
  • Consider adding a test for 2h reminder with payment scenario (minor - 24h version covers this logic)

Security Review

No security concerns. The implementation:

  • Only sends emails to the consultation owner
  • No sensitive data exposed in email content beyond appointment details
  • Uses Laravel's notification system with proper escaping

Performance Considerations

  • Notifications are queued (ShouldQueue) for async processing
  • DB queries filter by date first, then in-memory filter by time window
  • For typical consultation volumes (tens to hundreds per day), this is efficient
  • Scheduler runs hourly (24h) and every 15 minutes (2h) which is appropriate

Files Modified During Review

None - no modifications were necessary.

Gate Status

Gate: PASS → docs/qa/gates/3.8-consultation-reminders.yml

✓ Ready for Done

All acceptance criteria are met, tests pass, code quality is high, and no blocking issues identified.