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

17 KiB

Story 8.7: Consultation Reminder (2 Hours)

Status: Ready for Review

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

Dev Agent Record

Agent Model Used

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

Completion Notes

  • Story 8.7 implementation completed
  • Command already existed from Story 8.6, updated window from 15 minutes to 7 minutes per story spec
  • Notification already existed, updated subject lines and payment status check (changed from === 'pending' to !== 'received')
  • Email template updated to include office contact information (phone/email) and consultation date
  • Added office_phone and office_email to config/libra.php
  • Added translation keys for office contact labels
  • Email does NOT include calendar download link (per story requirement)
  • 25 tests pass for ConsultationReminderTest including 13 new tests for Story 8.7 requirements
  • Pre-existing test failures in Settings tests (not related to this story) - tests were failing before changes

File List

File Action
app/Console/Commands/Send2HourReminders.php MODIFIED (window 15→7 min)
app/Notifications/ConsultationReminder2h.php MODIFIED (subject, payment check)
config/libra.php MODIFIED (added office_phone, office_email)
lang/ar/emails.php MODIFIED (updated 2h reminder keys, added contact labels)
lang/en/emails.php MODIFIED (updated 2h reminder keys, added contact labels)
resources/views/emails/reminder-2h.blade.php MODIFIED (added date, office contact panel)
tests/Feature/ConsultationReminderTest.php MODIFIED (added 13 new tests for 2h reminder)

Change Log

  • 2026-01-02: Implemented Story 8.7 - 2-hour consultation reminder

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

QA Results

Review Date: 2026-01-02

Reviewed By: Quinn (Test Architect)

Code Quality Assessment

PASS - Implementation is complete and functional. The story was NOT "partly implemented" as the Dev Agent stated - it is fully complete. The Dev Agent's note about "partly implemented" appears to be a misstatement or referring to the fact that some files already existed from Story 8.6 (which is expected per the dependencies).

Analysis of Dev Agent's "Partly Implemented" Claim:

The Dev Agent Record notes that "Command already existed from Story 8.6" and "Notification already existed" - this is actually correct behavior, not partial implementation. Story 8.7 explicitly depends on Story 8.6 which created the migration, and the notification/command were already scaffolded. Story 8.7's task was to update and complete these files with the specific 2-hour reminder requirements, which was done:

  1. Window changed from 15 minutes to 7 minutes (per story spec)
  2. Subject lines added for Arabic/English
  3. Payment status check updated from === 'pending' to !== 'received'
  4. Office contact information added to template
  5. All required translation keys added

No AI Hallucination Detected. All claimed changes are verifiable in the actual code files.

Requirements Traceability

Acceptance Criteria Implementation Status Test Coverage
Scheduled every 15 mins ✓ routes/console.php:13 Implicit via command tests
7-minute window ✓ Send2HourReminders.php:34-35 ✓ Tests lines 204-234
Only approved consultations ✓ Send2HourReminders.php:38 ✓ Multiple tests
Skip cancelled/no-show/completed ✓ status = 'approved' filter ✓ Tests lines 92-104, 188-292
Track via reminder_2h_sent_at ✓ Send2HourReminders.php:56 ✓ Tests lines 106-119
Correct subjects (ar/en) ✓ ConsultationReminder2h.php:52-57 ✓ Tests lines 357-379
Date/time in email ✓ reminder-2h.blade.php:13-15 Implicit
Payment reminder conditional ✓ shouldShowPaymentReminder() ✓ Tests lines 310-353
Office contact info ✓ reminder-2h.blade.php:23-31,54-61 Implicit
Bilingual (ar/en) ✓ Single template with locale switch ✓ Tests lines 294-308
No calendar download link ✓ Verified not present ✓ Test line 381-392
ShouldQueue implemented ✓ ConsultationReminder2h.php:11 ✓ Test line 394-397

Refactoring Performed

None required - implementation is clean and follows Laravel conventions.

Compliance Check

  • Coding Standards: ✓ Code follows Laravel conventions, uses enums properly
  • Project Structure: ✓ Files in correct locations per architecture
  • Testing Strategy: ✓ 25 tests covering all acceptance criteria
  • All ACs Met: ✓ All acceptance criteria marked as complete in story are verified implemented

Improvements Checklist

All items handled - no outstanding issues:

  • Artisan command exists and works (reminders:send-2h)
  • Scheduled every 15 minutes in routes/console.php
  • Notification implements ShouldQueue
  • 7-minute window correctly implemented (not 15)
  • reminder_2h_sent_at tracking column exists and used
  • Payment reminder logic correct (!== 'received')
  • Office contact info added to config and template
  • Bilingual translations complete
  • No calendar link in 2h template (as per story requirement)
  • All 25 tests pass

Architecture Deviation Note

The story specified creating separate template files:

  • resources/views/emails/reminders/consultation-2h/ar.blade.php
  • resources/views/emails/reminders/consultation-2h/en.blade.php

Actual implementation: Single template at resources/views/emails/reminder-2h.blade.php with locale-based conditional rendering.

Assessment: This is an acceptable deviation. The single-file approach:

  1. Reduces file count and maintenance overhead
  2. Is consistent with other email templates in the project
  3. Achieves the same bilingual functionality
  4. Is actually cleaner for this use case

Security Review

  • No security concerns. Email rendering uses translation keys and config values.
  • No user input is rendered without proper escaping (Blade handles this).

Performance Considerations

  • Query filters by booking_date first (has index) before in-memory time filtering - efficient approach.
  • ShouldQueue ensures emails don't block the command execution.
  • 15-minute schedule interval is appropriate for 7-minute window.

Files Modified During Review

None - no modifications made.

Gate Status

Gate: PASS → docs/qa/gates/8.7-consultation-reminder-2h.yml

Ready for Done

The implementation is complete, all tests pass (25/25), all acceptance criteria are met, and there is no evidence of AI hallucination. The Dev Agent's statement about "partly implemented" was referring to the expected scenario where dependent files already existed from Story 8.6, not incomplete work.