15 KiB
15 KiB
Story 3.8: Consultation Reminders
Epic Reference
Epic 3: Booking & Consultation System
User Story
As a client, I want to receive reminder emails before my consultation, So that I don't forget my appointment and can prepare accordingly.
Story Context
Existing System Integration
- Integrates with: consultations table, Laravel scheduler, email notifications
- Technology: Laravel Queue, Scheduler, Mailable
- Follows pattern: Scheduled job pattern
- Touch points: Email system, consultation status
Acceptance Criteria
24-Hour Reminder
- Sent 24 hours before consultation
- Includes:
- Consultation date and time
- Type (free/paid)
- Payment reminder if paid and not received
- Calendar file link
- Any special instructions
2-Hour Reminder
- Sent 2 hours before consultation
- Includes:
- Consultation date and time
- Final payment reminder if applicable
- Contact information for last-minute issues
Reminder Logic
- Only for approved consultations
- Skip cancelled consultations
- Skip no-show consultations
- Don't send duplicate reminders
- Handle timezone correctly
Language Support
- Email in client's preferred language
- Arabic template for Arabic preference
- English template for English preference
Quality Requirements
- Scheduled jobs run reliably
- Retry on failure
- Logging for debugging
- Tests for reminder logic
Prerequisites & Assumptions
Consultation Model (from Story 3.4/3.5)
The Consultation model must exist with the following structure:
- Fields:
status- enum:pending,approved,completed,cancelled,no_showscheduled_date- date (cast to Carbon)scheduled_time- time string (H:i:s)type- enum:free,paidpayment_status- enum:pending,received,not_applicablepayment_amount- decimal, nullableuser_id- foreign key to users tablereminder_24h_sent_at- timestamp, nullable (added by this story)reminder_2h_sent_at- timestamp, nullable (added by this story)
- Relationships:
user()- BelongsTo User
- Factory States:
approved()- sets status to 'approved'
User Model Requirements
preferred_languagefield must exist (values:ar,en)- This field should be added in Epic 1 or early user stories
Route Requirements
client.consultations.calendarroute from Story 3.6 must exist for calendar file download link
Technical Notes
Reminder Commands
24-Hour Reminder Command
<?php
namespace App\Console\Commands;
use App\Models\Consultation;
use App\Notifications\ConsultationReminder24h;
use Illuminate\Console\Command;
use Carbon\Carbon;
class Send24HourReminders extends Command
{
protected $signature = 'reminders:send-24h';
protected $description = 'Send consultation reminders 24 hours before appointment';
public function handle(): int
{
$targetTime = now()->addHours(24);
$windowStart = $targetTime->copy()->subMinutes(30);
$windowEnd = $targetTime->copy()->addMinutes(30);
$consultations = Consultation::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) {
try {
$consultation->user->notify(new ConsultationReminder24h($consultation));
$consultation->update(['reminder_24h_sent_at' => now()]);
$count++;
$this->info("Sent 24h reminder for consultation #{$consultation->id}");
} catch (\Exception $e) {
$this->error("Failed to send reminder for consultation #{$consultation->id}: {$e->getMessage()}");
\Log::error('24h reminder failed', [
'consultation_id' => $consultation->id,
'error' => $e->getMessage(),
]);
}
}
$this->info("Sent {$count} 24-hour reminders");
return Command::SUCCESS;
}
}
2-Hour Reminder Command
<?php
namespace App\Console\Commands;
use App\Models\Consultation;
use App\Notifications\ConsultationReminder2h;
use Illuminate\Console\Command;
use Carbon\Carbon;
class Send2HourReminders extends Command
{
protected $signature = 'reminders:send-2h';
protected $description = 'Send consultation reminders 2 hours before appointment';
public function handle(): int
{
$targetTime = now()->addHours(2);
$windowStart = $targetTime->copy()->subMinutes(15);
$windowEnd = $targetTime->copy()->addMinutes(15);
$consultations = Consultation::where('status', 'approved')
->whereNull('reminder_2h_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) {
try {
$consultation->user->notify(new ConsultationReminder2h($consultation));
$consultation->update(['reminder_2h_sent_at' => now()]);
$count++;
$this->info("Sent 2h reminder for consultation #{$consultation->id}");
} catch (\Exception $e) {
$this->error("Failed to send reminder for consultation #{$consultation->id}: {$e->getMessage()}");
\Log::error('2h reminder failed', [
'consultation_id' => $consultation->id,
'error' => $e->getMessage(),
]);
}
}
$this->info("Sent {$count} 2-hour reminders");
return Command::SUCCESS;
}
}
Scheduler Configuration
// In routes/console.php or app/Console/Kernel.php (Laravel 12)
use Illuminate\Support\Facades\Schedule;
Schedule::command('reminders:send-24h')->hourly();
Schedule::command('reminders:send-2h')->everyFifteenMinutes();
Migration for Tracking
// Add reminder tracking columns to consultations
Schema::table('consultations', function (Blueprint $table) {
$table->timestamp('reminder_24h_sent_at')->nullable();
$table->timestamp('reminder_2h_sent_at')->nullable();
});
24-Hour Reminder Notification
<?php
namespace App\Notifications;
use App\Models\Consultation;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ConsultationReminder24h extends Notification
{
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';
return (new MailMessage)
->subject($this->getSubject($locale))
->markdown('emails.reminders.24h.' . $locale, [
'consultation' => $this->consultation,
'user' => $notifiable,
'showPaymentReminder' => $this->shouldShowPaymentReminder(),
]);
}
private function getSubject(string $locale): string
{
return $locale === 'ar'
? 'تذكير: موعدك غداً مع مكتب ليبرا للمحاماة'
: 'Reminder: Your consultation is tomorrow';
}
private function shouldShowPaymentReminder(): bool
{
return $this->consultation->type === 'paid' &&
$this->consultation->payment_status === 'pending';
}
}
Email Template (24h Arabic)
<!-- resources/views/emails/reminders/24h/ar.blade.php -->
<x-mail::message>
# تذكير بموعدك
عزيزي {{ $user->name }}،
نود تذكيرك بموعد استشارتك غداً:
**التاريخ:** {{ $consultation->scheduled_date->translatedFormat('l، d F Y') }}
**الوقت:** {{ \Carbon\Carbon::parse($consultation->scheduled_time)->format('g:i A') }}
**المدة:** 45 دقيقة
**النوع:** {{ $consultation->type === 'free' ? 'مجانية' : 'مدفوعة' }}
@if($showPaymentReminder)
<x-mail::panel>
**تذكير بالدفع:** يرجى إتمام عملية الدفع قبل موعد الاستشارة.
المبلغ المطلوب: {{ number_format($consultation->payment_amount, 2) }} شيكل
</x-mail::panel>
@endif
<x-mail::button :url="route('client.consultations.calendar', $consultation)">
تحميل ملف التقويم
</x-mail::button>
إذا كنت بحاجة لإعادة جدولة الموعد، يرجى التواصل معنا في أقرب وقت.
مع أطيب التحيات،
مكتب ليبرا للمحاماة
</x-mail::message>
Email Template (2h Arabic)
<!-- resources/views/emails/reminders/2h/ar.blade.php -->
<x-mail::message>
# موعدك بعد ساعتين
عزيزي {{ $user->name }}،
تذكير أخير: موعد استشارتك خلال ساعتين.
**الوقت:** {{ \Carbon\Carbon::parse($consultation->scheduled_time)->format('g:i A') }}
@if($showPaymentReminder)
<x-mail::panel>
**هام:** لم نستلم الدفعة بعد. يرجى إتمام الدفع قبل بدء الاستشارة.
</x-mail::panel>
@endif
إذا كان لديك أي استفسار طارئ، يرجى التواصل معنا على:
[رقم الهاتف]
نتطلع للقائك،
مكتب ليبرا للمحاماة
</x-mail::message>
Testing
use App\Console\Commands\Send24HourReminders;
use App\Models\Consultation;
use App\Notifications\ConsultationReminder24h;
use Illuminate\Support\Facades\Notification;
it('sends 24h reminder for upcoming consultation', function () {
Notification::fake();
$consultation = Consultation::factory()->approved()->create([
'scheduled_date' => now()->addHours(24)->toDateString(),
'scheduled_time' => now()->addHours(24)->format('H:i:s'),
'reminder_24h_sent_at' => null,
]);
$this->artisan('reminders:send-24h')
->assertSuccessful();
Notification::assertSentTo(
$consultation->user,
ConsultationReminder24h::class
);
expect($consultation->fresh()->reminder_24h_sent_at)->not->toBeNull();
});
it('does not send reminder for cancelled consultation', function () {
Notification::fake();
$consultation = Consultation::factory()->create([
'status' => 'cancelled',
'scheduled_date' => now()->addHours(24)->toDateString(),
'scheduled_time' => now()->addHours(24)->format('H:i:s'),
]);
$this->artisan('reminders:send-24h')
->assertSuccessful();
Notification::assertNotSentTo($consultation->user, ConsultationReminder24h::class);
});
it('does not send reminder for no-show consultation', function () {
Notification::fake();
$consultation = Consultation::factory()->create([
'status' => 'no_show',
'scheduled_date' => now()->addHours(24)->toDateString(),
'scheduled_time' => now()->addHours(24)->format('H:i:s'),
]);
$this->artisan('reminders:send-24h')
->assertSuccessful();
Notification::assertNotSentTo($consultation->user, ConsultationReminder24h::class);
});
it('does not send duplicate 24h reminders', function () {
Notification::fake();
$consultation = Consultation::factory()->approved()->create([
'scheduled_date' => now()->addHours(24)->toDateString(),
'scheduled_time' => now()->addHours(24)->format('H:i:s'),
'reminder_24h_sent_at' => now()->subHour(), // Already sent
]);
$this->artisan('reminders:send-24h')
->assertSuccessful();
Notification::assertNotSentTo($consultation->user, ConsultationReminder24h::class);
});
it('sends 2h reminder for upcoming consultation', function () {
Notification::fake();
$consultation = Consultation::factory()->approved()->create([
'scheduled_date' => now()->addHours(2)->toDateString(),
'scheduled_time' => now()->addHours(2)->format('H:i:s'),
'reminder_2h_sent_at' => null,
]);
$this->artisan('reminders:send-2h')
->assertSuccessful();
Notification::assertSentTo(
$consultation->user,
ConsultationReminder2h::class
);
expect($consultation->fresh()->reminder_2h_sent_at)->not->toBeNull();
});
it('includes payment reminder for unpaid consultations', function () {
Notification::fake();
$consultation = Consultation::factory()->approved()->create([
'scheduled_date' => now()->addHours(24)->toDateString(),
'scheduled_time' => now()->addHours(24)->format('H:i:s'),
'type' => 'paid',
'payment_status' => 'pending',
'payment_amount' => 200.00,
]);
$this->artisan('reminders:send-24h')
->assertSuccessful();
Notification::assertSentTo(
$consultation->user,
ConsultationReminder24h::class,
function ($notification) {
return $notification->consultation->type === 'paid'
&& $notification->consultation->payment_status === 'pending';
}
);
});
it('respects user language preference for reminders', function () {
Notification::fake();
$user = User::factory()->create(['preferred_language' => 'en']);
$consultation = Consultation::factory()->approved()->create([
'user_id' => $user->id,
'scheduled_date' => now()->addHours(24)->toDateString(),
'scheduled_time' => now()->addHours(24)->format('H:i:s'),
]);
$this->artisan('reminders:send-24h')
->assertSuccessful();
Notification::assertSentTo($user, ConsultationReminder24h::class);
});
Definition of Done
- 24h reminder command works
- 2h reminder command works
- Scheduler configured correctly
- Only approved consultations receive reminders
- Cancelled/no-show don't receive reminders
- No duplicate reminders sent
- Payment reminder included when applicable
- Arabic emails work correctly
- English emails work correctly
- Logging for debugging
- Tests pass for reminder logic
- Code formatted with Pint
Dependencies
- Story 3.4: Consultation model and booking submission (
app/Models/Consultation.php,database/factories/ConsultationFactory.php) - Story 3.5: Booking approval workflow (creates approved consultations)
- Story 3.6: Calendar file generation (provides
client.consultations.calendarroute) - Epic 1: User model with
preferred_languagefield (app/Models/User.php) - Epic 8: Email infrastructure (mail configuration, queue setup)
Risk Assessment
- Primary Risk: Scheduler not running
- Mitigation: Monitor scheduler, alert on failures
- Rollback: Manual reminder sending if needed
Estimation
Complexity: Medium Estimated Effort: 3-4 hours