generated sotries
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
# 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
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Reminder Commands
|
||||
|
||||
#### 24-Hour Reminder Command
|
||||
```php
|
||||
<?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
|
||||
<?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
|
||||
```php
|
||||
// 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
|
||||
```php
|
||||
// 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
|
||||
<?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)
|
||||
```blade
|
||||
<!-- 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)
|
||||
```blade
|
||||
<!-- 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
|
||||
```php
|
||||
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);
|
||||
});
|
||||
```
|
||||
|
||||
## 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.5:** Booking approval (creates approved consultations)
|
||||
- **Epic 8:** Email infrastructure
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user