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_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
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:
- Window changed from 15 minutes to 7 minutes (per story spec)
- Subject lines added for Arabic/English
- Payment status check updated from
=== 'pending'to!== 'received' - Office contact information added to template
- 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.phpresources/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:
- Reduces file count and maintenance overhead
- Is consistent with other email templates in the project
- Achieves the same bilingual functionality
- 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_datefirst (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
Recommended Status
✓ 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.