439 lines
15 KiB
Markdown
439 lines
15 KiB
Markdown
# Story 4.6: Timeline Update Notifications
|
|
|
|
## Epic Reference
|
|
**Epic 4:** Case Timeline System
|
|
|
|
## User Story
|
|
As a **client**,
|
|
I want **to receive email notifications when my timeline is updated**,
|
|
So that **I stay informed about my case progress without checking the portal**.
|
|
|
|
## Story Context
|
|
|
|
### Existing System Integration
|
|
- **Integrates with:** timeline_updates creation, email system
|
|
- **Technology:** Laravel Notifications, queued emails
|
|
- **Follows pattern:** Event-driven notification pattern
|
|
- **Touch points:** Timeline update creation
|
|
|
|
### Previous Story Context (Story 4.2)
|
|
Story 4.2 established:
|
|
- `TimelineUpdate` model with fields: `timeline_id`, `admin_id`, `update_text`
|
|
- `TimelineUpdate` belongs to `Timeline` and `User` (admin)
|
|
- `Timeline` has many `TimelineUpdate` records
|
|
- Updates created via `addUpdate()` method in timeline management Volt component
|
|
- Notification trigger point: after `$this->timeline->updates()->create([...])`
|
|
|
|
### Prerequisites
|
|
The following must exist before implementing this story:
|
|
- **User model requirements:**
|
|
- `preferred_language` field (string, defaults to 'ar') - from Epic 2 user registration
|
|
- `isActive()` method returning boolean (checks `status !== 'deactivated'`) - from Epic 2
|
|
- **Route requirement:**
|
|
- `client.timelines.show` route must exist (from Story 4.5)
|
|
- **Models available:**
|
|
- `Timeline` model with `user()` relationship
|
|
- `TimelineUpdate` model with `timeline()` relationship
|
|
|
|
### Implementation Files
|
|
- **Notification:** `app/Notifications/TimelineUpdateNotification.php`
|
|
- **Arabic Template:** `resources/views/emails/timeline/update/ar.blade.php`
|
|
- **English Template:** `resources/views/emails/timeline/update/en.blade.php`
|
|
- **Modify:** Story 4.2 Volt component to trigger notification
|
|
|
|
## Acceptance Criteria
|
|
|
|
### Notification Trigger
|
|
- [ ] Email sent when new update added to timeline
|
|
- [ ] Triggered automatically on TimelineUpdate creation
|
|
- [ ] Queued for performance
|
|
|
|
### Email Content
|
|
- [ ] Case name and reference
|
|
- [ ] Update summary or full text
|
|
- [ ] Date of update
|
|
- [ ] Link to view timeline
|
|
- [ ] Libra branding
|
|
|
|
### Language Support
|
|
- [ ] Email in client's preferred language
|
|
- [ ] Arabic template
|
|
- [ ] English template
|
|
|
|
### Exclusions
|
|
- [ ] No email for archived timeline reactivation
|
|
- [ ] No email if client deactivated
|
|
|
|
### Quality Requirements
|
|
- [ ] Professional email template
|
|
- [ ] Tests for notification sending
|
|
- [ ] Error handling for failed sends
|
|
|
|
## Technical Notes
|
|
|
|
### Notification Class
|
|
```php
|
|
<?php
|
|
|
|
namespace App\Notifications;
|
|
|
|
use App\Models\TimelineUpdate;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Notifications\Notification;
|
|
use Illuminate\Notifications\Messages\MailMessage;
|
|
|
|
class TimelineUpdateNotification extends Notification
|
|
{
|
|
use Queueable;
|
|
|
|
public function __construct(
|
|
public TimelineUpdate $update
|
|
) {}
|
|
|
|
public function via(object $notifiable): array
|
|
{
|
|
return ['mail'];
|
|
}
|
|
|
|
public function toMail(object $notifiable): MailMessage
|
|
{
|
|
$locale = $notifiable->preferred_language ?? 'ar';
|
|
$timeline = $this->update->timeline;
|
|
|
|
return (new MailMessage)
|
|
->subject($this->getSubject($locale, $timeline->case_name))
|
|
->markdown('emails.timeline.update.' . $locale, [
|
|
'update' => $this->update,
|
|
'timeline' => $timeline,
|
|
'user' => $notifiable,
|
|
]);
|
|
}
|
|
|
|
private function getSubject(string $locale, string $caseName): string
|
|
{
|
|
return $locale === 'ar'
|
|
? "تحديث جديد على قضيتك: {$caseName}"
|
|
: "New update on your case: {$caseName}";
|
|
}
|
|
}
|
|
```
|
|
|
|
### Email Templates
|
|
|
|
#### Arabic
|
|
```blade
|
|
<x-mail::message>
|
|
# تحديث جديد على قضيتك
|
|
|
|
عزيزي {{ $user->name }}،
|
|
|
|
تم إضافة تحديث جديد على قضيتك:
|
|
|
|
**القضية:** {{ $timeline->case_name }}
|
|
@if($timeline->case_reference)
|
|
**الرقم المرجعي:** {{ $timeline->case_reference }}
|
|
@endif
|
|
|
|
**تاريخ التحديث:** {{ $update->created_at->translatedFormat('d M Y - g:i A') }}
|
|
|
|
---
|
|
|
|
{!! $update->update_text !!}
|
|
|
|
---
|
|
|
|
<x-mail::button :url="route('client.timelines.show', $timeline)">
|
|
عرض التفاصيل الكاملة
|
|
</x-mail::button>
|
|
|
|
مع أطيب التحيات،
|
|
مكتب ليبرا للمحاماة
|
|
</x-mail::message>
|
|
```
|
|
|
|
#### English
|
|
```blade
|
|
<x-mail::message>
|
|
# New Update on Your Case
|
|
|
|
Dear {{ $user->name }},
|
|
|
|
A new update has been added to your case:
|
|
|
|
**Case:** {{ $timeline->case_name }}
|
|
@if($timeline->case_reference)
|
|
**Reference:** {{ $timeline->case_reference }}
|
|
@endif
|
|
|
|
**Update Date:** {{ $update->created_at->format('M d, Y - g:i A') }}
|
|
|
|
---
|
|
|
|
{!! $update->update_text !!}
|
|
|
|
---
|
|
|
|
<x-mail::button :url="route('client.timelines.show', $timeline)">
|
|
View Full Details
|
|
</x-mail::button>
|
|
|
|
Best regards,
|
|
Libra Law Firm
|
|
</x-mail::message>
|
|
```
|
|
|
|
### Trigger in Update Creation
|
|
```php
|
|
// In Story 4.2 addUpdate method
|
|
$update = $this->timeline->updates()->create([...]);
|
|
|
|
// Check if user is active before notifying
|
|
if ($this->timeline->user->isActive()) {
|
|
$this->timeline->user->notify(new TimelineUpdateNotification($update));
|
|
}
|
|
```
|
|
|
|
### Test Scenarios
|
|
- [ ] Notification sent when timeline update created
|
|
- [ ] No notification to deactivated user (`status === 'deactivated'`)
|
|
- [ ] Arabic template renders with correct content (case name, update text, date)
|
|
- [ ] English template renders with correct content
|
|
- [ ] Uses client's `preferred_language` for template selection (defaults to 'ar')
|
|
- [ ] Email is queued (uses `ShouldQueue` interface)
|
|
- [ ] Email contains valid link to timeline view
|
|
- [ ] Subject line includes case name in correct language
|
|
|
|
### Testing Examples
|
|
```php
|
|
use App\Models\{User, Timeline, TimelineUpdate};
|
|
use App\Notifications\TimelineUpdateNotification;
|
|
use Illuminate\Support\Facades\Notification;
|
|
|
|
it('sends notification when timeline update created', function () {
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->create(['status' => 'active']);
|
|
$timeline = Timeline::factory()->for($user)->create();
|
|
$update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
|
|
|
|
// Simulate the trigger from Story 4.2
|
|
if ($timeline->user->isActive()) {
|
|
$timeline->user->notify(new TimelineUpdateNotification($update));
|
|
}
|
|
|
|
Notification::assertSentTo($timeline->user, TimelineUpdateNotification::class);
|
|
});
|
|
|
|
it('does not send notification to deactivated user', function () {
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->create(['status' => 'deactivated']);
|
|
$timeline = Timeline::factory()->for($user)->create();
|
|
$update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
|
|
|
|
// Simulate the trigger from Story 4.2 - should NOT notify
|
|
if ($timeline->user->isActive()) {
|
|
$timeline->user->notify(new TimelineUpdateNotification($update));
|
|
}
|
|
|
|
Notification::assertNotSentTo($user, TimelineUpdateNotification::class);
|
|
});
|
|
|
|
it('uses arabic template for arabic-preferred user', function () {
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->create(['preferred_language' => 'ar']);
|
|
$timeline = Timeline::factory()->for($user)->create(['case_name' => 'Test Case']);
|
|
$update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
|
|
|
|
$user->notify(new TimelineUpdateNotification($update));
|
|
|
|
Notification::assertSentTo($user, TimelineUpdateNotification::class, function ($notification, $channels, $notifiable) {
|
|
$mail = $notification->toMail($notifiable);
|
|
return str_contains($mail->subject, 'تحديث جديد على قضيتك');
|
|
});
|
|
});
|
|
|
|
it('uses english template for english-preferred user', function () {
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->create(['preferred_language' => 'en']);
|
|
$timeline = Timeline::factory()->for($user)->create(['case_name' => 'Test Case']);
|
|
$update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
|
|
|
|
$user->notify(new TimelineUpdateNotification($update));
|
|
|
|
Notification::assertSentTo($user, TimelineUpdateNotification::class, function ($notification, $channels, $notifiable) {
|
|
$mail = $notification->toMail($notifiable);
|
|
return str_contains($mail->subject, 'New update on your case');
|
|
});
|
|
});
|
|
```
|
|
|
|
## Definition of Done
|
|
|
|
- [x] Email sent on new update
|
|
- [x] Arabic template works
|
|
- [x] English template works
|
|
- [x] Uses client's preferred language
|
|
- [x] Link to timeline works
|
|
- [x] Queued for performance
|
|
- [x] No email to deactivated users
|
|
- [x] Tests pass
|
|
- [x] Code formatted with Pint
|
|
|
|
## Dependencies
|
|
|
|
- **Story 4.2:** Timeline updates management (REQUIRED - notification triggered from addUpdate method)
|
|
- **Story 4.5:** Client timeline view (REQUIRED - provides `client.timelines.show` route used in email link)
|
|
- **Epic 2:** User model with `preferred_language` field and `isActive()` method
|
|
- **Epic 8:** Email infrastructure (SOFT DEPENDENCY - notification will queue but email delivery requires Epic 8)
|
|
|
|
## Estimation
|
|
|
|
**Complexity:** Low-Medium
|
|
**Estimated Effort:** 2-3 hours
|
|
|
|
---
|
|
|
|
## Dev Agent Record
|
|
|
|
### Status
|
|
**Ready for Review**
|
|
|
|
### Agent Model Used
|
|
Claude Opus 4.5
|
|
|
|
### File List
|
|
|
|
| File | Action | Description |
|
|
|------|--------|-------------|
|
|
| `app/Notifications/TimelineUpdateNotification.php` | Modified | Updated notification to use `markdown()` method, renamed property to `update`, added case name to subject line |
|
|
| `resources/views/emails/timeline/update/ar.blade.php` | Created | Arabic email template with case details, update text, and timeline link |
|
|
| `resources/views/emails/timeline/update/en.blade.php` | Created | English email template with case details, update text, and timeline link |
|
|
| `resources/views/livewire/admin/timelines/show.blade.php` | Modified | Added `isActive()` check before sending notification to prevent emails to deactivated users |
|
|
| `tests/Feature/Admin/TimelineUpdatesManagementTest.php` | Modified | Added 8 new tests for notification behavior (deactivated users, language templates, subject lines, queueing) |
|
|
|
|
### Change Log
|
|
|
|
- Updated `TimelineUpdateNotification` class to match story specifications:
|
|
- Changed property name from `$timelineUpdate` to `$update`
|
|
- Updated `toMail()` to use `markdown()` method instead of `view()`
|
|
- Added case name to email subject line for both languages
|
|
- Template path changed from `emails.timeline-update` to `emails.timeline.update.{locale}`
|
|
- Created separate Arabic and English markdown email templates at `resources/views/emails/timeline/update/`
|
|
- Added `isActive()` check in `addUpdate()` method to prevent notifications to deactivated users
|
|
- Added comprehensive tests covering:
|
|
- Deactivated user exclusion
|
|
- Arabic subject with case name
|
|
- English subject with case name
|
|
- Default language behavior
|
|
- Queue implementation verification
|
|
- Correct markdown template selection
|
|
|
|
### Completion Notes
|
|
|
|
1. The notification class already existed and implemented `ShouldQueue`, so emails are automatically queued
|
|
2. The notification trigger already existed in the Volt component but lacked the `isActive()` check - this was added
|
|
3. Used `<x-mail::message>` component syntax in templates as specified in story rather than `@component` syntax used elsewhere in codebase - both work with Laravel's mail markdown system
|
|
4. All 37 tests in `TimelineUpdatesManagementTest.php` pass, including 8 new notification-specific tests
|
|
5. Full test suite passes with 364 tests and 944 assertions
|
|
|
|
### DOD Checklist
|
|
|
|
1. **Requirements Met:**
|
|
- [x] All functional requirements specified in the story are implemented
|
|
- [x] All acceptance criteria defined in the story are met
|
|
|
|
2. **Coding Standards & Project Structure:**
|
|
- [x] Code adheres to Operational Guidelines (class-based Volt, Flux UI patterns)
|
|
- [x] Code aligns with Project Structure (notification in `app/Notifications/`, templates in `resources/views/emails/`)
|
|
- [x] Tech Stack followed (Laravel Notifications, Livewire, Pest)
|
|
- [x] Security best practices applied (HTML sanitization via `clean()` already in place)
|
|
- [x] No new linter errors (Pint passed)
|
|
- [N/A] Code comments - logic is straightforward and self-documenting
|
|
|
|
3. **Testing:**
|
|
- [x] Unit tests implemented (8 new notification tests)
|
|
- [x] Integration tests via Volt component tests
|
|
- [x] All 364 tests pass
|
|
- [x] Test coverage adequate for notification functionality
|
|
|
|
4. **Functionality & Verification:**
|
|
- [x] Tests verify notification sending behavior
|
|
- [x] Edge cases handled (deactivated users, null language defaults to 'ar')
|
|
|
|
5. **Story Administration:**
|
|
- [x] All tasks marked complete
|
|
- [x] Changes documented in this section
|
|
- [x] Story wrap up complete
|
|
|
|
6. **Dependencies, Build & Configuration:**
|
|
- [x] Project builds successfully
|
|
- [x] Linting passes (Pint)
|
|
- [N/A] No new dependencies added
|
|
- [N/A] No new environment variables
|
|
|
|
7. **Documentation:**
|
|
- [N/A] No new public APIs requiring JSDoc
|
|
- [N/A] No user-facing documentation changes needed
|
|
- [N/A] No architectural changes
|
|
|
|
## QA Results
|
|
|
|
### Review Date: 2025-12-27
|
|
|
|
### Reviewed By: Quinn (Test Architect)
|
|
|
|
### Code Quality Assessment
|
|
|
|
The implementation is clean, well-structured, and follows Laravel best practices. The notification class properly implements `ShouldQueue` for performance, uses constructor property promotion, and has clear separation of concerns. Email templates are properly organized by locale and use Laravel's markdown mail component syntax correctly.
|
|
|
|
### Refactoring Performed
|
|
|
|
None required - code quality is high and follows project conventions.
|
|
|
|
### Compliance Check
|
|
|
|
- Coding Standards: ✓ Code follows Laravel conventions, Pint passes
|
|
- Project Structure: ✓ Files in correct locations (Notifications, email views)
|
|
- Testing Strategy: ✓ Comprehensive Pest tests with proper assertions
|
|
- All ACs Met: ✓ All 11 acceptance criteria verified
|
|
|
|
### Improvements Checklist
|
|
|
|
All items already addressed by development:
|
|
|
|
- [x] Notification class implements ShouldQueue for queued delivery
|
|
- [x] isActive() check prevents notifications to deactivated users
|
|
- [x] Arabic and English templates properly render with case details
|
|
- [x] Subject line includes case name in appropriate language
|
|
- [x] HTML sanitization via clean() protects against XSS in update text
|
|
- [x] Email templates handle both individual (full_name) and company (company_name) clients
|
|
|
|
### Security Review
|
|
|
|
**Status: PASS**
|
|
- HTML sanitization using `clean()` function prevents XSS in update_text
|
|
- No sensitive data exposed in email beyond necessary case information
|
|
- Proper authorization through admin middleware on update creation endpoint
|
|
|
|
### Performance Considerations
|
|
|
|
**Status: PASS**
|
|
- Notification implements `ShouldQueue` for async delivery
|
|
- No N+1 queries - timeline relationship is eagerly loaded
|
|
- Email rendering happens in queue worker, not blocking HTTP request
|
|
|
|
### Files Modified During Review
|
|
|
|
None - no modifications were necessary.
|
|
|
|
### Gate Status
|
|
|
|
Gate: PASS → docs/qa/gates/4.6-timeline-update-notifications.yml
|
|
|
|
### Recommended Status
|
|
|
|
✓ Ready for Done - All acceptance criteria met, tests pass, code quality is excellent.
|