Set up Laravel notification system with NotificationType enum (5 types), NudgeNotification, DocumentUploadedNotification, and DeclarationOverdueNotification classes with database + mail channels. Add email templates, infrastructure tests, and fix existing NotificationController tests for workspace compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16 KiB
Story 3.1: Notification Infrastructure Setup
Status: review
Story
As a developer, I want the Laravel notification system configured with database and mail channels, a NotificationType enum, and foundational notification classes, so that all future notification features (nudges, alerts, bulk notifications) have a reliable, consistent foundation to build on.
Acceptance Criteria
-
AC1: Notifications table migration exists and runs
- Given Redis queue is configured (from Story 0.4)
- When
php artisan migrateis run - Then the
notificationstable exists with Laravel's standard schema (uuid id, type, morphs notifiable, text data, read_at, timestamps)
-
AC2: User model confirms Notifiable trait
- Then
Usermodel usesIlluminate\Notifications\Notifiabletrait
- Then
-
AC3: NotificationType enum created
- Then
app/Enums/NotificationType.phpexists usingbensampo/laravel-enum - And values include:
nudge,declaration_overdue,document_uploaded,bulk_notification,status_changed
- Then
-
AC4: Base notification pattern established
- Then all new notification classes implement
ShouldQueue, useQueueabletrait - And all notification
toArray()/toDatabase()payloads includeworkspace_id - And all notification
toArray()/toDatabase()payloads includenotification_typefrom theNotificationTypeenum
- Then all new notification classes implement
-
AC5: NudgeNotification class created
- Then
app/Notifications/NudgeNotification.phpexists withdatabaseandmailchannels - And payload includes:
workspace_id,declaration_id,sender_id,notification_type=nudge - And
toMail()returns a queuedNudgeNotificationMailMarkdown mailable
- Then
-
AC6: DocumentUploadedNotification class created
- Then
app/Notifications/DocumentUploadedNotification.phpexists withdatabasechannel only (no email — reduces noise) - And payload includes:
workspace_id,declaration_id,client_id,notification_type=document_uploaded
- Then
-
AC7: DeclarationOverdueNotification class created
- Then
app/Notifications/DeclarationOverdueNotification.phpexists withdatabaseandmailchannels - And payload includes:
workspace_id,declaration_id,notification_type=declaration_overdue - And queued email sending with up to 3 retries (
$tries = 3per NFR26)
- Then
-
AC8: NudgeNotificationMail mailable created
- Then
app/Mail/NudgeNotificationMail.phpexists as a Markdown mailable - And it renders with sender name, declaration details (client name, type, due_date), and a direct link button ("Voir la declaration")
- And uses professional French copy
- Then
-
AC9: Notification queries are workspace-scoped
- Then when fetching notifications, they are filtered by
workspace_idin the JSON data payload - And
NotificationControlleris extended with anindexmethod returning workspace-scoped notifications
- Then when fetching notifications, they are filtered by
-
AC10: Routes registered for notification endpoints
- Then
GET /notificationsroute exists →NotificationController@index(Inertia page) - And
POST /notifications/mark-all-readroute exists →NotificationController@markAllAsRead - And existing
POST /notifications/{id}/readroute remains functional
- Then
-
AC11: All notification classes have retry configuration
- Then notification classes with mail channel set
public $tries = 3(NFR26)
- Then notification classes with mail channel set
Tasks / Subtasks
-
Task 1: Verify existing notifications infrastructure (AC: #1, #2)
- 1.1 Confirm
notificationstable migration already exists (2026_03_09_000004_create_notifications_table.php) — do NOT create a duplicate - 1.2 Confirm
Usermodel already hasNotifiabletrait — already present, no changes needed - 1.3 Run
php artisan migrateto confirm table exists
- 1.1 Confirm
-
Task 2: Create NotificationType enum (AC: #3)
- 2.1 Create
app/Enums/NotificationType.phpusingbensampo/laravel-enumpattern - 2.2 Values:
nudge,declaration_overdue,document_uploaded,bulk_notification,status_changed - 2.3 Follow existing enum patterns (see
DeclarationStatus.phpfor reference)
- 2.1 Create
-
Task 3: Create NudgeNotification class (AC: #4, #5, #11)
- 3.1 Create
app/Notifications/NudgeNotification.php - 3.2 Implement
ShouldQueue, useQueueabletrait, set$tries = 3 - 3.3 Channels:
['database', 'mail'] - 3.4
toDatabase()payload:workspace_id,declaration_id,sender_id,notification_type→nudge - 3.5
toMail()returnsNudgeNotificationMailmailable
- 3.1 Create
-
Task 4: Create DocumentUploadedNotification class (AC: #4, #6)
- 4.1 Create
app/Notifications/DocumentUploadedNotification.php - 4.2 Implement
ShouldQueue, useQueueabletrait - 4.3 Channels:
['database']only — no mail - 4.4
toDatabase()payload:workspace_id,declaration_id,client_id,notification_type→document_uploaded
- 4.1 Create
-
Task 5: Create DeclarationOverdueNotification class (AC: #4, #7, #11)
- 5.1 Create
app/Notifications/DeclarationOverdueNotification.php - 5.2 Implement
ShouldQueue, useQueueabletrait, set$tries = 3 - 5.3 Channels:
['database', 'mail'] - 5.4
toDatabase()payload:workspace_id,declaration_id,notification_type→declaration_overdue - 5.5
toMail()returns MailMessage with declaration details and link
- 5.1 Create
-
Task 6: Create NudgeNotificationMail mailable (AC: #8)
- 6.1 Create
app/Mail/NudgeNotificationMail.phpas Markdown mailable - 6.2 Create Markdown template
resources/views/emails/nudge-notification.blade.php - 6.3 Include: sender name, declaration client name, declaration type, due_date, direct link button ("Voir la declaration")
- 6.4 Professional French copy, include workspace firm name in header
- 6.1 Create
-
Task 7: Extend NotificationController with index (AC: #9, #10)
- 7.1 Add
index()method to existingNotificationController - 7.2 Fetch user notifications, filter by
workspace_idin JSON data matching current workspace - 7.3 Paginate 25 per page, reverse chronological order
- 7.4 Return Inertia render to
notifications/Indexpage - 7.5 Register
GET /notificationsroute inweb.php
- 7.1 Add
-
Task 8: Clean up legacy notification (no AC — housekeeping)
- 8.1 Evaluate
FolderMentionNotification.php— this references the deprecatedFoldermodel andfolders.showroute. Flag for removal or skip if out of scope
- 8.1 Evaluate
-
Task 9: Create notification type definitions for frontend (AC: #9)
- 9.1 Create
resources/js/types/notification.tswith TypeScript types for notification data
- 9.1 Create
-
Task 10: Write tests (all ACs)
- 10.1 Create
tests/Feature/Notifications/NotificationInfrastructureTest.php - 10.2 Test: NotificationType enum has all expected values
- 10.3 Test: NudgeNotification sends via database and mail channels (
Notification::fake()) - 10.4 Test: DocumentUploadedNotification sends via database channel only
- 10.5 Test: DeclarationOverdueNotification sends via database and mail channels
- 10.6 Test: All notification payloads include
workspace_idandnotification_type - 10.7 Test: NotificationController@index returns paginated, workspace-scoped notifications
- 10.8 Test: NotificationController@markAsRead marks notification as read
- 10.9 Test: NotificationController@markAllAsRead marks all as read
- 10.10 Test: Unauthenticated users cannot access notification routes
- 10.1 Create
Retrospective Intelligence
From Epic 2 Retrospective (2026-03-24):
- CRITICAL — Pre-existing test failures: 15 tests were failing across 3 retrospectives. Commit
716e9fcresolved them. Verify all 222+ tests still pass before starting and after finishing this story. - Team Agreement — Load retro as context: This story was created with full retrospective intelligence (this section proves it).
- Team Agreement — Pre-existing test failures must not carry: Run full test suite before declaring this story done. Zero failures tolerance.
- Deferred Item D-1: Nudge/Reassign dropdown items in dashboard are unconditionally disabled — Story 3.2 will enable them. This story creates the
NudgeNotificationthey depend on. - Architecture doc drift was corrected in commit
6956f7b—due_date(notdeadline), noDeclaration::workspace()scope,mise_en_demeurestatus documented. withoutVite()global workaround intests/Pest.php— confirmed compatible with notification tests (no Vite manifest needed for backend tests).- Context management is the team's superpower — rich story specs with previous intelligence prevent errors across stories.
From Epic 1 Retrospective:
- withPivot gotchas now documented in project-context.md (commit
6956f7b). When loading workspace members, always chain->withPivot('role', 'permissions').
Dev Notes
Existing Infrastructure (DO NOT recreate)
notificationstable migration ALREADY EXISTS:database/migrations/2026_03_09_000004_create_notifications_table.php— standard Laravel schema with uuid id, type, morphs notifiable, text data, read_at, timestamps. Do NOT runphp artisan notifications:tableor create a duplicate.Usermodel ALREADY hasNotifiabletrait: Line 19 ofapp/Models/User.php—use HasFactory, LogsActivity, Notifiable, SoftDeletes, TwoFactorAuthenticatable;NotificationControllerALREADY EXISTS:app/Http/Controllers/NotificationController.phpwithmarkAsRead()andmarkAllAsRead()methods. Extend this controller, do not replace it.- Existing routes:
POST /notifications/{id}/readandPOST /notifications/read-allalready registered inweb.php. Add the newGET /notificationsroute alongside them. - Existing notification classes:
DeclarationMentionNotification.php(database + mail, ShouldQueue, Queueable — good pattern to follow) andFolderMentionNotification.php(legacy — references deprecatedFoldermodel).
Existing Notification Pattern to Follow
The DeclarationMentionNotification is the reference implementation:
- Constructor with promoted properties:
public Declaration $declaration, public User $mentionedBy, public string $message via()returns['database', 'mail']toDatabase()returns array with declaration details + URL viaroute()helpertoMail()returnsMailMessagewith->markdown()template- French subject line pattern:
'Vous avez été mentionné - '.$this->declaration->title
Architecture Constraints
- D8: Use Laravel's built-in
DatabaseNotificationsystem — no custom notification tables - D5: Redis queue driver — all notification classes implement
ShouldQueue+Queueable - D10: No real-time features for MVP — notifications load on page visit only
- Workspace scoping: Filter notifications by
workspace_idin the JSONdatacolumn, not by a direct relationship. UsewhereJsonContains('data->workspace_id', $workspace->id)or filter after fetching. - Notification type enum values: snake_case (
nudge,declaration_overdue, etc.) - Route pattern from architecture:
Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index');
Critical Gotchas
- Declaration column is
due_dateNOTdeadline— use$declaration->due_dateeverywhere - Declaration scoping: No
Declaration::workspace()scope — useDeclaration::where('workspace_id', $workspace->id) - Declaration statuses: 6 values —
created,en_cours,en_attente_client,mise_en_demeure,termine,ferme - Authorization returns 404 not 403 for workspace boundary violations
- Workspace is session-based — resolve from
$request->user()->currentWorkspaceor sessioncurrent_workspace_id - Enum pattern: Use
bensampo/laravel-enum— look atDeclarationStatus.phpfor exact syntax - All URLs passed as props from PHP controllers — never hardcode in Vue
- Use
route()helper for URLs in notification payloads and tests
Testing Standards
- Pest syntax with
test()closures Notification::fake()for testing notification dispatchMail::fake()if testing mail separately- Feature tests in
tests/Feature/Notifications/ - Use
route()helper for URLs, never hardcoded paths RefreshDatabaseis auto-applied viaPest.php— don't add manually- Run
composer testto verify all tests pass (currently 222+ tests)
Project Structure Notes
New files to create:
app/Enums/NotificationType.php— follows existing enum pattern inapp/Enums/app/Notifications/NudgeNotification.php— follows existingDeclarationMentionNotification.phppatternapp/Notifications/DocumentUploadedNotification.php— database channel onlyapp/Notifications/DeclarationOverdueNotification.php— database + mail channelsapp/Mail/NudgeNotificationMail.php— Markdown mailable, follows existing mail pattern inapp/Mail/resources/views/emails/nudge-notification.blade.php— Markdown mail templateresources/js/types/notification.ts— TypeScript type definitionstests/Feature/Notifications/NotificationInfrastructureTest.php
Existing files to modify:
app/Http/Controllers/NotificationController.php— addindex()methodroutes/web.php— addGET /notificationsroute
Files NOT to touch:
database/migrations/2026_03_09_000004_create_notifications_table.php— already existsapp/Models/User.php— already hasNotifiabletraitresources/js/components/ui/*— shadcn-vue, never modify
References
- [Source: _bmad-output/planning-artifacts/architecture.md#D8 — In-App Notification System]
- [Source: _bmad-output/planning-artifacts/architecture.md#D5 — Queue Driver]
- [Source: _bmad-output/planning-artifacts/architecture.md#Notification Patterns]
- [Source: _bmad-output/planning-artifacts/architecture.md#Phase 3 File Structure]
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.1]
- [Source: _bmad-output/planning-artifacts/prd.md#FR29-FR33]
- [Source: _bmad-output/implementation-artifacts/epic-2-retro-2026-03-24.md#Action Items]
- [Source: _bmad-output/project-context.md#Critical Implementation Rules]
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
- Baseline test suite: 223 tests passed before implementation
- Final test suite: 237 tests passed (14 new notification tests added), zero failures
- One test fix: created
notifications/Index.vuepage component to satisfy Inertia page existence check in test
Completion Notes List
- Verified existing infrastructure: notifications migration, User Notifiable trait, NotificationController with markAsRead/markAllAsRead
- Created NotificationType enum with 5 values following bensampo/laravel-enum pattern
- Created NudgeNotification (database+mail, $tries=3, toMail returns NudgeNotificationMail)
- Created DocumentUploadedNotification (database only, no mail to reduce noise)
- Created DeclarationOverdueNotification (database+mail, $tries=3, French subject line)
- Created NudgeNotificationMail as Markdown mailable with professional French copy
- Created declaration-overdue email template
- Extended NotificationController with workspace-scoped index() using whereJsonContains
- Added GET /notifications and POST /notifications/mark-all-read routes inside workspace middleware
- Created notifications/Index.vue page with basic notification list UI
- Flagged FolderMentionNotification as legacy (references deprecated Folder model) — skipped removal as out of scope
- Created TypeScript notification types in resources/js/types/notification.ts
- 14 comprehensive tests covering all ACs
File List
app/Enums/NotificationType.php(new)app/Notifications/NudgeNotification.php(new)app/Notifications/DocumentUploadedNotification.php(new)app/Notifications/DeclarationOverdueNotification.php(new)app/Mail/NudgeNotificationMail.php(new)resources/views/emails/nudge-notification.blade.php(new)resources/views/emails/declaration-overdue.blade.php(new)resources/js/pages/notifications/Index.vue(new)resources/js/types/notification.ts(new)resources/js/types/index.ts(modified — added notification export)app/Http/Controllers/NotificationController.php(modified — added index method)routes/web.php(modified — added GET /notifications and POST /notifications/mark-all-read routes)tests/Feature/Notifications/NotificationInfrastructureTest.php(new)
Change Log
- 2026-03-24: Story 3.1 implementation complete — notification infrastructure with enum, 3 notification classes, mailable, controller index, routes, frontend types, and 14 tests