libra/docs/stories/story-8.8-timeline-update-n...

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 - BaseMailable class 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 TimelineUpdate is 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

  • TimelineUpdateEmail mailable created extending BaseMailable
  • Arabic and English templates created with proper formatting
  • Observer registered and triggers on TimelineUpdate creation
  • 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::Active enum (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

Ready for Done - All acceptance criteria met, comprehensive test coverage, code quality excellent.