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_statusfields - User Model: Must have
preferred_languagefield
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'ANDpayment_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_atcolumn to consultations table - Artisan command
reminders:send-24hcreated 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
paidANDpayment_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_locationtranslation 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
Recommended Status
✓ Ready for Done - All acceptance criteria met, all tests passing, code quality excellent