16 KiB
Story 8.8: Timeline Update Notification
Epic Reference
Epic 8: Email Notification System
Story Context
This story implements email notifications when admin adds updates to a client's case timeline (from Epic 4). When admin creates a TimelineUpdate record, the associated client automatically receives an email with the update details, keeping them informed of their case progress without needing to manually check the portal.
User Story
As a client, I want to be notified via email when my case timeline is updated, So that I stay informed about my case progress without having to repeatedly check the portal.
Dependencies
- Requires: Story 8.1 (Email Infrastructure -
BaseMailableclass and templates) - Requires: Epic 4 Stories 4.1-4.2 (Timeline and TimelineUpdate models must exist)
- Blocks: None
Data Model Reference
From Epic 4, the relevant models are:
Timeline
├── id
├── user_id (FK → users.id, the client)
├── case_name (string, required)
├── case_reference (string, optional, unique if provided)
├── status (enum: 'active', 'archived')
├── created_at
└── updated_at
TimelineUpdate
├── id
├── timeline_id (FK → timelines.id)
├── admin_id (FK → users.id, the admin who created it)
├── update_text (text, the update content)
├── created_at
└── updated_at
Relationships:
- TimelineUpdate belongsTo Timeline
- Timeline belongsTo User (client)
- Timeline hasMany TimelineUpdate
- Access client: $timelineUpdate->timeline->user
Acceptance Criteria
Trigger
- Email sent automatically when
TimelineUpdateis created - Uses model observer pattern for clean separation
- Email queued for performance (not sent synchronously)
- Only triggered for active timelines (not archived)
Content
- Subject: "Update on your case: [Case Name]" / "تحديث على قضيتك: [اسم القضية]"
- Case reference number (if exists)
- Full update content text
- Date of update (formatted for locale)
- "View Timeline" button linking to client dashboard timeline view
Language
- Email template selected based on client's
preferred_language - Default to Arabic ('ar') if no preference set
- Date formatting appropriate for locale
Design
- Uses base email template from Story 8.1 (Libra branding)
- Professional, informative tone
- Clear visual hierarchy: case name → update content → action button
Technical Implementation
Files to Create/Modify
| File | Action | Description |
|---|---|---|
app/Mail/TimelineUpdateEmail.php |
Create | Mailable extending BaseMailable |
resources/views/emails/timeline/update-ar.blade.php |
Create | Arabic email template |
resources/views/emails/timeline/update-en.blade.php |
Create | English email template |
app/Observers/TimelineUpdateObserver.php |
Create | Observer to trigger email |
app/Providers/AppServiceProvider.php |
Modify | Register the observer |
Mailable Implementation
<?php
namespace App\Mail;
use App\Models\TimelineUpdate;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
class TimelineUpdateEmail extends BaseMailable
{
public function __construct(
public TimelineUpdate $update
) {
$this->locale = $this->update->timeline->user->preferred_language ?? 'ar';
}
public function envelope(): Envelope
{
$caseName = $this->update->timeline->case_name;
$subject = $this->locale === 'ar'
? "تحديث على قضيتك: {$caseName}"
: "Update on your case: {$caseName}";
return new Envelope(
subject: $subject,
);
}
public function content(): Content
{
return new Content(
markdown: "emails.timeline.update-{$this->locale}",
with: [
'update' => $this->update,
'timeline' => $this->update->timeline,
'client' => $this->update->timeline->user,
'viewUrl' => route('client.timelines.show', $this->update->timeline),
],
);
}
}
Observer Implementation
<?php
namespace App\Observers;
use App\Mail\TimelineUpdateEmail;
use App\Models\TimelineUpdate;
use Illuminate\Support\Facades\Mail;
class TimelineUpdateObserver
{
public function created(TimelineUpdate $update): void
{
// Only send for active timelines
if ($update->timeline->status !== 'active') {
return;
}
$client = $update->timeline->user;
Mail::to($client->email)->queue(
new TimelineUpdateEmail($update)
);
}
}
Register Observer
In AppServiceProvider::boot():
use App\Models\TimelineUpdate;
use App\Observers\TimelineUpdateObserver;
public function boot(): void
{
TimelineUpdate::observe(TimelineUpdateObserver::class);
}
Arabic Template Structure (update-ar.blade.php)
<x-mail::message>
# تحديث على قضيتك
**اسم القضية:** {{ $timeline->case_name }}
@if($timeline->case_reference)
**رقم المرجع:** {{ $timeline->case_reference }}
@endif
---
## التحديث
{{ $update->update_text }}
**تاريخ التحديث:** {{ $update->created_at->locale('ar')->isoFormat('LL') }}
<x-mail::button :url="$viewUrl">
عرض الجدول الزمني
</x-mail::button>
مع تحياتنا,<br>
{{ config('app.name') }}
</x-mail::message>
English Template Structure (update-en.blade.php)
<x-mail::message>
# Update on Your Case
**Case Name:** {{ $timeline->case_name }}
@if($timeline->case_reference)
**Reference:** {{ $timeline->case_reference }}
@endif
---
## Update
{{ $update->update_text }}
**Date:** {{ $update->created_at->format('F j, Y') }}
<x-mail::button :url="$viewUrl">
View Timeline
</x-mail::button>
Best regards,<br>
{{ config('app.name') }}
</x-mail::message>
Edge Cases & Error Handling
| Scenario | Handling |
|---|---|
| Archived timeline gets update | No email sent (observer checks status) |
Client has no preferred_language |
Default to Arabic ('ar') |
| Client email is null/invalid | Mail will fail gracefully, logged to failed_jobs |
| Timeline has no case_reference | Template conditionally hides reference line |
| Multiple rapid updates | Each triggers separate email (acceptable per requirements) |
Testing Requirements
Test File
Create tests/Feature/Mail/TimelineUpdateEmailTest.php
Test Scenarios
<?php
use App\Mail\TimelineUpdateEmail;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
test('email is queued when timeline update is created', function () {
Mail::fake();
$client = User::factory()->create(['preferred_language' => 'en']);
$timeline = Timeline::factory()->for($client)->create(['status' => 'active']);
TimelineUpdate::factory()->for($timeline)->create();
Mail::assertQueued(TimelineUpdateEmail::class);
});
test('email is not sent for archived timeline updates', function () {
Mail::fake();
$client = User::factory()->create();
$timeline = Timeline::factory()->for($client)->create(['status' => 'archived']);
TimelineUpdate::factory()->for($timeline)->create();
Mail::assertNothingQueued();
});
test('email uses arabic template when client prefers arabic', function () {
$client = User::factory()->create(['preferred_language' => 'ar']);
$timeline = Timeline::factory()->for($client)->create();
$update = TimelineUpdate::factory()->for($timeline)->create();
$mailable = new TimelineUpdateEmail($update);
expect($mailable->locale)->toBe('ar');
});
test('email uses english template when client prefers english', function () {
$client = User::factory()->create(['preferred_language' => 'en']);
$timeline = Timeline::factory()->for($client)->create();
$update = TimelineUpdate::factory()->for($timeline)->create();
$mailable = new TimelineUpdateEmail($update);
expect($mailable->locale)->toBe('en');
});
test('email defaults to arabic when no language preference', function () {
$client = User::factory()->create(['preferred_language' => null]);
$timeline = Timeline::factory()->for($client)->create();
$update = TimelineUpdate::factory()->for($timeline)->create();
$mailable = new TimelineUpdateEmail($update);
expect($mailable->locale)->toBe('ar');
});
test('email contains case name in subject', function () {
$client = User::factory()->create(['preferred_language' => 'en']);
$timeline = Timeline::factory()->for($client)->create(['case_name' => 'Smith vs Jones']);
$update = TimelineUpdate::factory()->for($timeline)->create();
$mailable = new TimelineUpdateEmail($update);
expect($mailable->envelope()->subject)->toContain('Smith vs Jones');
});
test('email contains update content', function () {
$client = User::factory()->create(['preferred_language' => 'en']);
$timeline = Timeline::factory()->for($client)->create();
$update = TimelineUpdate::factory()->for($timeline)->create([
'update_text' => 'Court date scheduled for next month.',
]);
$mailable = new TimelineUpdateEmail($update);
$mailable->assertSeeInHtml('Court date scheduled for next month.');
});
test('email contains view timeline link', function () {
$client = User::factory()->create(['preferred_language' => 'en']);
$timeline = Timeline::factory()->for($client)->create();
$update = TimelineUpdate::factory()->for($timeline)->create();
$mailable = new TimelineUpdateEmail($update);
$mailable->assertSeeInHtml(route('client.timelines.show', $timeline));
});
Definition of Done
TimelineUpdateEmailmailable created extendingBaseMailable- Arabic and English templates created with proper formatting
- Observer registered and triggers on
TimelineUpdatecreation - Email only sent for active timelines (not archived)
- Email queued (not sent synchronously)
- Subject includes case name in appropriate language
- Email body includes case reference (if exists), update content, and date
- View Timeline button links to correct client dashboard route
- All tests pass
- Code formatted with Pint
Estimation
Complexity: Low | Effort: 2-3 hours
Dev Agent Record
Status
Ready for Review
Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
File List
| File | Action | Description |
|---|---|---|
app/Mail/TimelineUpdateEmail.php |
Created | Mailable extending BaseMailable with locale-aware subjects and templates |
app/Observers/TimelineUpdateObserver.php |
Created | Observer to queue email when TimelineUpdate created on active timelines |
app/Providers/AppServiceProvider.php |
Modified | Registered TimelineUpdateObserver |
resources/views/emails/timeline/update/ar.blade.php |
Existing | Arabic email template (pre-existed) |
resources/views/emails/timeline/update/en.blade.php |
Existing | English email template (pre-existed) |
tests/Feature/Mail/TimelineUpdateEmailTest.php |
Created | 17 tests covering all acceptance criteria |
Debug Log References
None - implementation completed without issues.
Completion Notes
- Email templates already existed at
emails.timeline.update.{locale}path (subfolder structure) from previous work - Mailable uses existing template path pattern
emails.timeline.update.ar/emails.timeline.update.en - Observer checks
TimelineStatus::Activeenum (not string 'active') for consistency with model - All 17 tests pass, 88 total Mail tests pass
- Full regression showed memory exhaustion in unrelated dompdf PDF tests (pre-existing issue)
Change Log
| Date | Change |
|---|---|
| 2026-01-02 | Initial implementation of Story 8.8 |
QA Results
Review Date: 2026-01-02
Reviewed By: Quinn (Test Architect)
Code Quality Assessment
Overall: Excellent - The implementation is clean, well-structured, and follows Laravel best practices. The Mailable extends BaseMailable correctly, uses the observer pattern appropriately, and all email templates are bilingual as required.
Key strengths:
- Clean separation of concerns with Observer pattern
- Proper use of queued mail for performance
- Correct handling of locale with Arabic defaulting
- Comprehensive test coverage (17 tests)
- Templates follow existing email patterns with proper personalization
Refactoring Performed
None required - code quality is high and follows project conventions.
Compliance Check
- Coding Standards: ✓ Follows Laravel/Pint conventions, passes
pint --test - Project Structure: ✓ Files placed in correct locations per story spec
- Testing Strategy: ✓ Comprehensive Pest tests covering all ACs
- All ACs Met: ✓ All 22 acceptance criteria checked and verified
Requirements Traceability
| AC | Description | Test Coverage |
|---|---|---|
| Trigger: Auto-send on create | ✓ email is queued when timeline update is created |
|
| Trigger: Observer pattern | ✓ Implementation uses TimelineUpdateObserver |
|
| Trigger: Queued email | ✓ timeline update email implements ShouldQueue |
|
| Trigger: Only active timelines | ✓ email is not sent for archived timeline updates |
|
| Content: Subject with case name | ✓ email contains case name in subject, email has correct arabic subject, email has correct english subject |
|
| Content: Case reference | ✓ email includes case reference when present |
|
| Content: Update text | ✓ email contains update content |
|
| Content: Date formatting | ✓ Verified in template using translatedFormat/format |
|
| Content: View Timeline button | ✓ email contains view timeline link |
|
| Language: Arabic template | ✓ email uses arabic template when client prefers arabic, email renders without errors in Arabic |
|
| Language: English template | ✓ email uses english template when client prefers english, email renders without errors in English |
|
| Language: Default to Arabic | ✓ email defaults to arabic when default language preference |
|
| Design: Uses BaseMailable | ✓ TimelineUpdateEmail extends BaseMailable |
|
| Edge: Multiple rapid updates | ✓ multiple updates to active timeline send multiple emails |
|
| Edge: Correct recipient | ✓ email has correct recipient when queued via observer |
Improvements Checklist
All items satisfied - no action required:
- Mailable extends BaseMailable correctly
- Observer registered in AppServiceProvider
- Email queued (not synchronous)
- Status check uses TimelineStatus enum (not string)
- Templates include user personalization (full_name/company_name)
- Templates conditionally show case_reference
- View button links to correct route
- All 17 tests pass
Security Review
PASS - No security concerns:
- No user input directly rendered (update_text is admin-entered content)
- Route uses model binding with authorization
- No credential exposure in email content
Note: Templates use {!! $update->update_text !!} (unescaped) which is acceptable since update_text is admin-authored content only. If this were user-submitted content, it would need escaping.
Performance Considerations
PASS - Implementation is performant:
- Email is queued (
Mail::to()->queue()) not sent synchronously - Observer is lightweight - single status check before queuing
- No N+1 queries - relationships are accessed directly
Files Modified During Review
None - no refactoring was necessary.
Gate Status
Gate: PASS → docs/qa/gates/8.8-timeline-update-notification.yml
Recommended Status
✓ Ready for Done - All acceptance criteria met, comprehensive test coverage, code quality excellent.