390 lines
14 KiB
Markdown
390 lines
14 KiB
Markdown
# 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_status` fields
|
|
- **User Model:** Must have `preferred_language` field
|
|
|
|
## 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'` AND `payment_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:
|
|
|
|
```php
|
|
// 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
|
|
```php
|
|
// 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
|
|
```php
|
|
// 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
|
|
```php
|
|
// 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`)
|
|
```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`)
|
|
```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_at` column to consultations table
|
|
- [ ] Artisan command `reminders:send-24h` created 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 `paid` AND `payment_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_location` translation 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
|
|
|
|
- [x] All acceptance criteria implemented
|
|
- [x] All tests passing (7 for 24h reminders)
|
|
- [x] Migration adds required columns
|
|
- [x] Schedule registered in console routes
|
|
- [x] Notification implements ShouldQueue
|
|
- [x] Error handling with logging in command
|
|
- [x] 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
|