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_atcolumn to consultations table - Consultation Model: Must have
status,scheduled_date,scheduled_time,consultation_type,payment_statusfields - User Model: Must have
preferred_languagefield
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'ANDpayment_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-2hcreated 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_atupdated) - Payment reminder shown only when
paidANDpayment_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