# 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 1. **AC1: Notifications table migration exists and runs** - Given Redis queue is configured (from Story 0.4) - When `php artisan migrate` is run - Then the `notifications` table exists with Laravel's standard schema (uuid id, type, morphs notifiable, text data, read_at, timestamps) 2. **AC2: User model confirms Notifiable trait** - Then `User` model uses `Illuminate\Notifications\Notifiable` trait 3. **AC3: NotificationType enum created** - Then `app/Enums/NotificationType.php` exists using `bensampo/laravel-enum` - And values include: `nudge`, `declaration_overdue`, `document_uploaded`, `bulk_notification`, `status_changed` 4. **AC4: Base notification pattern established** - Then all new notification classes implement `ShouldQueue`, use `Queueable` trait - And all notification `toArray()`/`toDatabase()` payloads include `workspace_id` - And all notification `toArray()`/`toDatabase()` payloads include `notification_type` from the `NotificationType` enum 5. **AC5: NudgeNotification class created** - Then `app/Notifications/NudgeNotification.php` exists with `database` and `mail` channels - And payload includes: `workspace_id`, `declaration_id`, `sender_id`, `notification_type` = `nudge` - And `toMail()` returns a queued `NudgeNotificationMail` Markdown mailable 6. **AC6: DocumentUploadedNotification class created** - Then `app/Notifications/DocumentUploadedNotification.php` exists with `database` channel only (no email — reduces noise) - And payload includes: `workspace_id`, `declaration_id`, `client_id`, `notification_type` = `document_uploaded` 7. **AC7: DeclarationOverdueNotification class created** - Then `app/Notifications/DeclarationOverdueNotification.php` exists with `database` and `mail` channels - And payload includes: `workspace_id`, `declaration_id`, `notification_type` = `declaration_overdue` - And queued email sending with up to 3 retries (`$tries = 3` per NFR26) 8. **AC8: NudgeNotificationMail mailable created** - Then `app/Mail/NudgeNotificationMail.php` exists 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 9. **AC9: Notification queries are workspace-scoped** - Then when fetching notifications, they are filtered by `workspace_id` in the JSON data payload - And `NotificationController` is extended with an `index` method returning workspace-scoped notifications 10. **AC10: Routes registered for notification endpoints** - Then `GET /notifications` route exists → `NotificationController@index` (Inertia page) - And `POST /notifications/mark-all-read` route exists → `NotificationController@markAllAsRead` - And existing `POST /notifications/{id}/read` route remains functional 11. **AC11: All notification classes have retry configuration** - Then notification classes with mail channel set `public $tries = 3` (NFR26) ## Tasks / Subtasks - [x] Task 1: Verify existing notifications infrastructure (AC: #1, #2) - [x] 1.1 Confirm `notifications` table migration already exists (`2026_03_09_000004_create_notifications_table.php`) — do NOT create a duplicate - [x] 1.2 Confirm `User` model already has `Notifiable` trait — already present, no changes needed - [x] 1.3 Run `php artisan migrate` to confirm table exists - [x] Task 2: Create NotificationType enum (AC: #3) - [x] 2.1 Create `app/Enums/NotificationType.php` using `bensampo/laravel-enum` pattern - [x] 2.2 Values: `nudge`, `declaration_overdue`, `document_uploaded`, `bulk_notification`, `status_changed` - [x] 2.3 Follow existing enum patterns (see `DeclarationStatus.php` for reference) - [x] Task 3: Create NudgeNotification class (AC: #4, #5, #11) - [x] 3.1 Create `app/Notifications/NudgeNotification.php` - [x] 3.2 Implement `ShouldQueue`, use `Queueable` trait, set `$tries = 3` - [x] 3.3 Channels: `['database', 'mail']` - [x] 3.4 `toDatabase()` payload: `workspace_id`, `declaration_id`, `sender_id`, `notification_type` → `nudge` - [x] 3.5 `toMail()` returns `NudgeNotificationMail` mailable - [x] Task 4: Create DocumentUploadedNotification class (AC: #4, #6) - [x] 4.1 Create `app/Notifications/DocumentUploadedNotification.php` - [x] 4.2 Implement `ShouldQueue`, use `Queueable` trait - [x] 4.3 Channels: `['database']` only — no mail - [x] 4.4 `toDatabase()` payload: `workspace_id`, `declaration_id`, `client_id`, `notification_type` → `document_uploaded` - [x] Task 5: Create DeclarationOverdueNotification class (AC: #4, #7, #11) - [x] 5.1 Create `app/Notifications/DeclarationOverdueNotification.php` - [x] 5.2 Implement `ShouldQueue`, use `Queueable` trait, set `$tries = 3` - [x] 5.3 Channels: `['database', 'mail']` - [x] 5.4 `toDatabase()` payload: `workspace_id`, `declaration_id`, `notification_type` → `declaration_overdue` - [x] 5.5 `toMail()` returns MailMessage with declaration details and link - [x] Task 6: Create NudgeNotificationMail mailable (AC: #8) - [x] 6.1 Create `app/Mail/NudgeNotificationMail.php` as Markdown mailable - [x] 6.2 Create Markdown template `resources/views/emails/nudge-notification.blade.php` - [x] 6.3 Include: sender name, declaration client name, declaration type, due_date, direct link button ("Voir la declaration") - [x] 6.4 Professional French copy, include workspace firm name in header - [x] Task 7: Extend NotificationController with index (AC: #9, #10) - [x] 7.1 Add `index()` method to existing `NotificationController` - [x] 7.2 Fetch user notifications, filter by `workspace_id` in JSON data matching current workspace - [x] 7.3 Paginate 25 per page, reverse chronological order - [x] 7.4 Return Inertia render to `notifications/Index` page - [x] 7.5 Register `GET /notifications` route in `web.php` - [x] Task 8: Clean up legacy notification (no AC — housekeeping) - [x] 8.1 Evaluate `FolderMentionNotification.php` — this references the deprecated `Folder` model and `folders.show` route. Flag for removal or skip if out of scope - [x] Task 9: Create notification type definitions for frontend (AC: #9) - [x] 9.1 Create `resources/js/types/notification.ts` with TypeScript types for notification data - [x] Task 10: Write tests (all ACs) - [x] 10.1 Create `tests/Feature/Notifications/NotificationInfrastructureTest.php` - [x] 10.2 Test: NotificationType enum has all expected values - [x] 10.3 Test: NudgeNotification sends via database and mail channels (`Notification::fake()`) - [x] 10.4 Test: DocumentUploadedNotification sends via database channel only - [x] 10.5 Test: DeclarationOverdueNotification sends via database and mail channels - [x] 10.6 Test: All notification payloads include `workspace_id` and `notification_type` - [x] 10.7 Test: NotificationController@index returns paginated, workspace-scoped notifications - [x] 10.8 Test: NotificationController@markAsRead marks notification as read - [x] 10.9 Test: NotificationController@markAllAsRead marks all as read - [x] 10.10 Test: Unauthenticated users cannot access notification routes ## Retrospective Intelligence **From Epic 2 Retrospective (2026-03-24):** - **CRITICAL — Pre-existing test failures:** 15 tests were failing across 3 retrospectives. Commit `716e9fc` resolved 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 `NudgeNotification` they depend on. - **Architecture doc drift was corrected** in commit `6956f7b` — `due_date` (not `deadline`), no `Declaration::workspace()` scope, `mise_en_demeure` status documented. - **`withoutVite()` global workaround** in `tests/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) - **`notifications` table 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 run `php artisan notifications:table` or create a duplicate. - **`User` model ALREADY has `Notifiable` trait:** Line 19 of `app/Models/User.php` — `use HasFactory, LogsActivity, Notifiable, SoftDeletes, TwoFactorAuthenticatable;` - **`NotificationController` ALREADY EXISTS:** `app/Http/Controllers/NotificationController.php` with `markAsRead()` and `markAllAsRead()` methods. Extend this controller, do not replace it. - **Existing routes:** `POST /notifications/{id}/read` and `POST /notifications/read-all` already registered in `web.php`. Add the new `GET /notifications` route alongside them. - **Existing notification classes:** `DeclarationMentionNotification.php` (database + mail, ShouldQueue, Queueable — good pattern to follow) and `FolderMentionNotification.php` (legacy — references deprecated `Folder` model). ### 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 via `route()` helper - `toMail()` returns `MailMessage` with `->markdown()` template - French subject line pattern: `'Vous avez été mentionné - '.$this->declaration->title` ### Architecture Constraints - **D8:** Use Laravel's built-in `DatabaseNotification` system — 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_id` in the JSON `data` column, not by a direct relationship. Use `whereJsonContains('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_date`** NOT `deadline` — use `$declaration->due_date` everywhere - **Declaration scoping:** No `Declaration::workspace()` scope — use `Declaration::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()->currentWorkspace` or session `current_workspace_id` - **Enum pattern:** Use `bensampo/laravel-enum` — look at `DeclarationStatus.php` for 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 dispatch - `Mail::fake()` if testing mail separately - Feature tests in `tests/Feature/Notifications/` - Use `route()` helper for URLs, never hardcoded paths - `RefreshDatabase` is auto-applied via `Pest.php` — don't add manually - Run `composer test` to verify all tests pass (currently 222+ tests) ### Project Structure Notes **New files to create:** - `app/Enums/NotificationType.php` — follows existing enum pattern in `app/Enums/` - `app/Notifications/NudgeNotification.php` — follows existing `DeclarationMentionNotification.php` pattern - `app/Notifications/DocumentUploadedNotification.php` — database channel only - `app/Notifications/DeclarationOverdueNotification.php` — database + mail channels - `app/Mail/NudgeNotificationMail.php` — Markdown mailable, follows existing mail pattern in `app/Mail/` - `resources/views/emails/nudge-notification.blade.php` — Markdown mail template - `resources/js/types/notification.ts` — TypeScript type definitions - `tests/Feature/Notifications/NotificationInfrastructureTest.php` **Existing files to modify:** - `app/Http/Controllers/NotificationController.php` — add `index()` method - `routes/web.php` — add `GET /notifications` route **Files NOT to touch:** - `database/migrations/2026_03_09_000004_create_notifications_table.php` — already exists - `app/Models/User.php` — already has `Notifiable` trait - `resources/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.vue` page 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