Files
L-Ami-Fiduciaire/_bmad-output/planning-artifacts/epics.md
Saad Ibn-Ezzoubayr 35545c2a8f feat: L'Ami Fiduciaire V1.0.0 — full codebase with Story 0.1 complete
Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12,
Vue 3, Inertia.js 2, and Tailwind CSS 4.

Story 0.1 (rename folders to declarations in database) is implemented and
code-reviewed: migration, rollback, and 6 Pest tests all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 23:33:10 +00:00

94 KiB

stepsCompleted, inputDocuments, workflowType, project_name, user_name, date
stepsCompleted inputDocuments workflowType project_name user_name date
step-01-validate-prerequisites
step-02-design-epics
step-03-create-stories
step-04-final-validation
_bmad-output/planning-artifacts/prd.md
_bmad-output/planning-artifacts/architecture.md
_bmad-output/planning-artifacts/ux-design-specification.md
epics-and-stories l'ami fiduciaire Saad 2026-03-11

l'ami fiduciaire - Epic Breakdown

Overview

This document provides the complete epic and story breakdown for l'ami fiduciaire, decomposing the requirements from the PRD, UX Design, and Architecture into implementable stories.

Requirements Inventory

Functional Requirements

Workspace & Onboarding (FR1-FR6):

  • FR1: Owner can create a new workspace with firm name and basic configuration
  • FR2: Owner can invite team members to the workspace via email
  • FR3: Owner can assign roles (Manager/Chef de Mission, Worker) to team members
  • FR4: Owner can configure per-workspace permission toggles for Manager roles
  • FR5: Owner can manage workspace settings (firm details, firm logo, display name)
  • FR6: New users can sign up for a 14-day trial without credit card

Team & Role Management (FR7-FR11):

  • FR7: Owner can view, add, and remove team members from the workspace
  • FR8: Owner can change a team member's role within the workspace
  • FR9: Manager can invite, remove, and change roles of team members when permission is granted by Owner
  • FR10: System enforces role-based access -- Workers see only assigned items, Managers/Owners see all
  • FR11: Owner can switch between multiple owned workspaces

Client Management (FR12-FR15):

  • FR12: Owner/Manager can create, view, edit, and deactivate client records
  • FR13: Owner/Manager can import clients in bulk
  • FR14: Worker can view and interact with their assigned clients only
  • FR15: System associates each client with a workspace and enforces tenant isolation

Declaration Lifecycle (FR16-FR23):

  • FR16: Owner/Manager can create individual declarations for a client (type, deadline, assignment)
  • FR17: Owner/Manager can bulk-create declarations across multiple clients with type and deadline selection
  • FR18: Owner/Manager/Worker can update declaration status through its lifecycle
  • FR19: Owner/Manager can reassign a declaration to a different team member
  • FR20: Worker can edit declarations assigned to them (type correction, deadline adjustment)
  • FR21: System auto-archives declarations when marked as closed
  • FR22: Owner/Manager can re-open an archived declaration with audit trail
  • FR23: Users can view archived declarations in read-only mode with full history

Dashboard & Visibility (FR24-FR28):

  • FR24: Owner/Manager can view a command center dashboard showing all clients, declaration statuses, and priority alerts
  • FR25: Worker can view a scoped dashboard showing only their assigned clients and declarations
  • FR26: Dashboard surfaces priority alerts (overdue declarations, approaching deadlines, missing client documents)
  • FR27: SaaS Admin can view a platform-level dashboard (workspace count, user count, storage, system health)
  • FR28: SaaS Admin can view and respond to support tickets via an issue/support inbox

Collaboration & Notifications (FR29-FR33):

  • FR29: Owner/Manager can nudge a team member on a specific declaration with one action
  • FR30: Team members receive notifications with direct links to the relevant declaration
  • FR31: Users can view a notification center showing all received nudges and system alerts
  • FR32: Owner/Manager can schedule bulk notifications to clients for document requests
  • FR33: System sends email notifications for key events (document requests, nudges, status changes)

Client Portal & Document Exchange (FR34-FR40):

  • FR34: System generates unique token-based links for client interactions (no account required)
  • FR35: External clients can upload documents via token link from any device including mobile
  • FR36: External clients receive confirmation after successful document upload
  • FR37: Team members can download client-uploaded documents from the declaration detail page
  • FR38: Team members can send messages to clients within a declaration context
  • FR39: External clients can view messages from their fiduciary firm via the portal
  • FR40: Token links expire according to configurable security policies

Search, Filtering & Navigation (FR41-FR44):

  • FR41: Users can filter declarations by status, client, assignee, type, and deadline range
  • FR42: Filter selections persist across views within a session
  • FR43: Users can perform quick search across clients and declarations
  • FR44: Archive section is accessible as a top-level navigation item with its own filters and search

Archive System (FR45-FR51):

  • FR45: System preserves full declaration history upon archiving (documents, messages, status changes, activity log)
  • FR46: Users can browse archived declarations with hybrid filters and search
  • FR47: Users can view an archive detail page as a read-only snapshot of the complete declaration
  • FR48: Users can preview documents in-app from archived declarations
  • FR49: Users can bulk-download archived declaration documents as ZIP
  • FR50: System visually distinguishes archived declarations from active ones
  • FR51: System enforces 10-year retention policy for archived data

Activity & Audit (FR52-FR55):

  • FR52: System logs all data modifications with actor, timestamp, and change details
  • FR53: Owner can view the full activity log for the workspace
  • FR54: Manager can view activity logs when permission is granted
  • FR55: Worker can view activity logs for their own actions only

Platform Administration (FR56-FR58):

  • FR56: SaaS Admin can view all workspaces and their usage metrics
  • FR57: SaaS Admin can manage platform-level configuration (global settings, feature flags, storage limits, email templates)
  • FR58: System enforces subscription tier limits (team members, clients, storage, features)

NonFunctional Requirements

Performance (NFR1-NFR6):

  • NFR1: Page loads and common user actions complete within 2 seconds on standard Moroccan internet connections (ADSL/4G)
  • NFR2: Bulk declaration creation (up to 50 declarations) completes within 10 seconds
  • NFR3: Bulk notification scheduling (up to 50 clients) completes within 10 seconds
  • NFR4: File uploads up to 10 MB complete within 60 seconds on 4G connections
  • NFR5: Dashboard data renders within 3 seconds for workspaces with up to 200 active clients
  • NFR6: Quick search returns results within 1 second

Security (NFR7-NFR15):

  • NFR7: All data encrypted in transit (TLS 1.2+) and at rest (AES-256 for stored documents)
  • NFR8: Multi-tenant data isolation enforced at every query -- no workspace can access another workspace's data
  • NFR9: Authorization boundary violations return 404 (not 403) to prevent tenant existence leakage
  • NFR10: Token-based client portal links are cryptographically secure, single-purpose, and expire after configurable duration
  • NFR11: Two-factor authentication available for all firm users (TOTP-based)
  • NFR12: All data modifications logged with actor, timestamp, and change details (audit trail)
  • NFR13: CNDP (Law 09-08) compliant data handling -- EU-hosted infrastructure
  • NFR14: Client financial documents accessible only to authorized workspace members and the specific client via their token link
  • NFR15: Session management with secure cookie handling, CSRF protection, and session timeout after inactivity

Scalability (NFR16-NFR20):

  • NFR16: System supports up to 200 concurrent workspaces while maintaining performance targets
  • NFR17: System supports up to 1,000 total users across all workspaces
  • NFR18: Individual workspace supports up to 500 active clients and 5,000 active declarations
  • NFR19: File storage architecture supports growth to 1 TB total platform storage
  • NFR20: System handles seasonal peak loads (bilan season Jan-Mar: 2-3x normal usage)

Reliability & Data Protection (NFR21-NFR28):

  • NFR21: Platform targets 99.5% uptime (max ~3.6 hours unplanned downtime per month)
  • NFR22: Automated hourly database backups with 1-hour RPO
  • NFR23: Database binary/transaction logging enabled for point-in-time recovery
  • NFR24: Backup restore procedure documented and tested -- full restore within 1 hour (RTO)
  • NFR25: Backup retention: daily backups for 30 days, weekly for 6 months
  • NFR26: Email notification delivery >99% success rate with retry logic (up to 3 retries within 5 minutes)
  • NFR27: Monitoring and alerting for system health, error rates, and resource utilization
  • NFR28: Zero data loss tolerance for completed transactions

Additional Requirements

From Architecture Document:

  • Pre-Phase Requirement: "Folders → Declarations" terminology migration must complete before any Phase 1-5 work begins (~40 files affected: models, controllers, enums, pages, tests, migrations)
  • Redis Integration (D4/D5/D7): Add Redis service for cache, queue, and sessions -- foundational service required before queue-dependent features
  • Permission Toggle Storage (D1): JSON permissions column on workspace_user pivot table with default permissions per role defined in config
  • Archive Strategy (D2): Status-based archiving with archived_at timestamp column on declarations table, scopeActive() and scopeArchived() Eloquent scopes
  • Search Implementation (D3): MySQL FULLTEXT indexes on searchable columns, MATCH...AGAINST in natural language mode
  • In-App Notifications (D8): Laravel DatabaseNotification system with database + mail channels, workspace_id in all notification payloads
  • Bulk Operation Processing (D9): Synchronous DB operations wrapped in DB::transaction(), individual email sends queued via Redis
  • Filter Persistence (D11): URL query parameters via Inertia router.get(), controllers read from Request and apply Eloquent scopes
  • Document Preview (D12): Client-side PDF.js for PDFs, native <img> for images, shadcn-vue Dialog wrapper
  • Real-Time Features (D10): Deferred for MVP -- no WebSocket wiring. Soketi stays configured for post-MVP
  • Email Service (D13): Amazon SES (eu-west-1 Ireland) with SPF, DKIM, DMARC configuration
  • Hosting (D14): EU-based VPS (Hetzner/DigitalOcean) managed via Laravel Forge with Nginx, PHP-FPM, MySQL, Redis, Supervisor
  • File Storage (D15): S3-compatible object storage with AES-256 SSE, Spatie Media Library S3 disk driver
  • Monitoring (D16): Laravel Pulse (application monitoring) + Sentry (error tracking/alerting)
  • Declaration Status Flow: Created → En cours → En attente client → En cours → Termine → Ferme → [auto-archive]. Invalid transitions must be enforced
  • Workspace Scoping Pattern: Every model query MUST be workspace-scoped. Use abort(404) for all authorization failures (never 403)
  • Role-Scoped Queries: Workers see only their assigned items via scopeForUser() on Declaration model
  • Dashboard Cache: Redis Cache::remember() with 5-minute TTL, invalidated on declaration status change/creation/deletion
  • Controller Traits: HasWorkspaceScope and AuthorizesPermissions for shared patterns across controllers
  • Implementation Sequence: Redis → Permissions → Archive column → FULLTEXT → Notifications → S3 → SES → Hosting → Monitoring

From UX Design Specification:

  • Role-Driven Sidebar: Different nav items based on role. Owner/Manager: Dashboard, Clients, Declarations, Archive, Team, Settings. Worker: Dashboard, My Declarations, Archive, Settings
  • Desktop-First Firm App, Mobile-First Client Portal: Two distinct design targets within the same application
  • Dense Table Design: 13px body text in tables, compact padding (py-2 px-3), showing 25-50 rows without scrolling
  • Summary Cards (StatCard): KPI cards above tables that act as clickable filters (e.g., "3 En retard" filters table to overdue)
  • Inline Row Actions: Status changes, nudge, reassign available directly on table rows via dropdown menu
  • Persistent FilterBar: Always visible above data tables with instant filter application via URL params
  • BulkActionBar: Floating toolbar appears on multi-select with available bulk operations
  • StatusBadge: Consistent color-coded status pills with icon + text (never color alone) across all views
  • Deadline Proximity Indicators: Green (>7d) → Amber (3-7d) → Red (<3d) → Pulsing red (overdue)
  • Toast Notifications (Sonner): Non-blocking feedback for quick actions, positioned top-right, max 3 visible
  • Confirmation Dialogs: Required for delete, archive, bulk actions, reassignment; NOT required for status updates and nudges
  • EmptyState Component: Every list/table has a designed empty state with icon, text, and action button
  • DeclarationStatusStepper: Visual progress indicator in declaration detail showing lifecycle position
  • FileUploadZone: Drag-and-drop + click-to-browse with progress bar, camera-friendly for mobile
  • DataTable with TanStack Table v8: Sortable headers, row selection, sticky header, pagination (25/50/100)
  • Responsive Breakpoints: Tables → card list on mobile (<768px), sidebar → overlay drawer, KPI cards stack single-column
  • WCAG 2.1 AA Compliance: Color contrast, keyboard navigation, screen reader support, 44px min touch targets
  • shadcn-vue Components to Install: table, combobox, calendar, popover, toast/sonner, progress, switch
  • Date Format: DD/MM/YYYY with relative countdown for deadlines ("dans 5 jours")
  • French-Native UI: All labels, copy, and professional terminology in Moroccan French

FR Coverage Map

Epic 0: Foundation Migration & Infrastructure Setup

  • FR16: Epic 0 - Declaration creation (renamed from folders)
  • FR18: Epic 0 - Declaration status updates (renamed from folders)
  • FR19: Epic 0 - Declaration reassignment (renamed from folders)
  • FR20: Epic 0 - Worker declaration editing (renamed from folders)

Epic 1: Team Management & Permission System

  • FR3: Epic 1 - Owner assigns roles to team members
  • FR4: Epic 1 - Owner configures permission toggles for Managers
  • FR7: Epic 1 - Owner views, adds, removes team members
  • FR8: Epic 1 - Owner changes team member roles
  • FR9: Epic 1 - Manager manages team when permitted
  • FR10: Epic 1 - System enforces role-based access
  • FR11: Epic 1 - Owner switches between workspaces

Epic 2: Role-Driven Dashboard & Command Center

  • FR24: Epic 2 - Owner/Manager command center dashboard
  • FR25: Epic 2 - Worker scoped dashboard
  • FR26: Epic 2 - Priority alerts on dashboard

Epic 3: Collaboration, Nudge System & Notifications

  • FR29: Epic 3 - One-click nudge on declarations
  • FR30: Epic 3 - Notifications with direct links
  • FR31: Epic 3 - Notification center for all users
  • FR32: Epic 3 - Bulk notification scheduling for clients
  • FR33: Epic 3 - Email notifications for key events

Epic 4: Bulk Operations, Search & Advanced Filtering

  • FR17: Epic 4 - Bulk declaration creation
  • FR41: Epic 4 - Filter declarations by status, client, assignee, type, deadline
  • FR42: Epic 4 - Filter persistence across views
  • FR43: Epic 4 - Quick search across clients and declarations
  • FR44: Epic 4 - Archive as top-level nav with own filters/search

Epic 5: Archive System & Document Preview

  • FR21: Epic 5 - Auto-archive on close
  • FR22: Epic 5 - Re-open archived declaration with audit trail
  • FR23: Epic 5 - View archived declarations read-only
  • FR45: Epic 5 - Full history preservation on archive
  • FR46: Epic 5 - Browse archived declarations with filters/search
  • FR47: Epic 5 - Archive detail page (read-only snapshot)
  • FR48: Epic 5 - In-app document preview
  • FR49: Epic 5 - Bulk ZIP download of archived documents
  • FR50: Epic 5 - Visual distinction for archived declarations
  • FR51: Epic 5 - 10-year retention policy enforcement

Epic 6: Platform Administration & Subscription Enforcement

  • FR27: Epic 6 - SaaS admin platform dashboard
  • FR28: Epic 6 - Support ticket inbox
  • FR56: Epic 6 - View all workspaces and metrics
  • FR57: Epic 6 - Platform-level configuration management
  • FR58: Epic 6 - Subscription tier limit enforcement

Epic 7: Production Infrastructure & Deployment

  • NFR-driven: NFR7 (encryption), NFR13 (CNDP/EU hosting), NFR14 (document access control), NFR19 (1TB storage), NFR21-NFR28 (uptime, backups, email delivery, monitoring)

Already Built (Existing -- Enhanced by Epics Above):

  • FR1: Existing - Workspace creation
  • FR2: Existing - Team invitations (enhanced by Epic 1)
  • FR5: Existing - Workspace settings
  • FR6: Existing - Trial signup
  • FR12: Existing - Client CRUD (enhanced by Epic 1 role scoping, Epic 4 filtering)
  • FR13: Existing - Bulk client import
  • FR14: Existing - Worker assigned clients (enhanced by Epic 1 role enforcement)
  • FR15: Existing - Client workspace isolation
  • FR34: Existing - Token-based portal links
  • FR35: Existing - Client document upload via portal
  • FR36: Existing - Upload confirmation
  • FR37: Existing - Team document download
  • FR38: Existing - In-declaration messaging
  • FR39: Existing - Client message viewing
  • FR40: Existing - Token expiry policies
  • FR52: Existing - Activity logging (Spatie Activity Log)
  • FR53: Existing - Owner activity log viewing (enhanced by Epic 1 permissions)
  • FR54: Existing - Manager activity log viewing (enhanced by Epic 1 permissions)
  • FR55: Existing - Worker own-action log viewing (enhanced by Epic 1 permissions)

Coverage: All 58 FRs mapped. 0 FRs missing.

Epic List

Epic 0: Foundation Migration & Infrastructure Setup

The platform uses correct professional terminology ("declarations" not "folders") and the technical foundation (Redis for cache/queue/sessions, base database migrations) is ready for all new features. This pre-phase must complete before any feature development begins. FRs covered: Foundation for FR16, FR18, FR19, FR20 (terminology migration) NFRs addressed: NFR8 (workspace scoping patterns), NFR9 (abort(404) convention)

Epic 1: Team Management & Permission System

Firm owners can manage their team, assign roles (Owner/Manager/Worker), and configure per-workspace permission toggles for Managers. The system enforces role-based access across all views -- Workers see only assigned items, Managers/Owners see all. Workspace switching works for multi-workspace Owners. FRs covered: FR3, FR4, FR7, FR8, FR9, FR10, FR11 NFRs addressed: NFR8 (tenant isolation), NFR9 (404 for auth violations), NFR12 (audit trail on role changes)

Epic 2: Role-Driven Dashboard & Command Center

Owners/Managers open the app and see their entire firm's status on one screen -- KPI summary cards, priority alerts, declaration status overview, and activity feed. Workers see a scoped dashboard showing only their assigned workload. The "morning command center" moment that drives trial-to-paid conversion. FRs covered: FR24, FR25, FR26 NFRs addressed: NFR1 (2s page loads), NFR5 (3s dashboard render for 200 clients)

Epic 3: Collaboration, Nudge System & Notifications

Owners/Managers can nudge team members on specific declarations with one click. All users have a notification center with direct links to relevant declarations. The system sends reliable email notifications for key events. Bulk notification scheduling enables document request campaigns to multiple clients. FRs covered: FR29, FR30, FR31, FR32, FR33 NFRs addressed: NFR3 (10s bulk notification), NFR26 (>99% email delivery)

Epic 4: Bulk Operations, Search & Advanced Filtering

Power users can create 50 declarations in one action with type and deadline selection across multiple clients. A persistent filter bar with status, client, assignee, type, and date range filters appears on all list views. Quick search across clients and declarations returns results in under 1 second. Archive has its own top-level navigation with dedicated filters. FRs covered: FR17, FR41, FR42, FR43, FR44 NFRs addressed: NFR2 (10s bulk creation), NFR6 (1s search)

Epic 5: Archive System & Document Preview

Completed declarations are automatically archived when closed, preserving full history (documents, messages, status changes, activity log). Users can browse, filter, and search the archive. Archive detail pages show read-only snapshots with in-app document preview (PDF.js). Owners can re-open archived items with audit trail. Bulk ZIP download and 10-year retention policy ensure compliance. FRs covered: FR21, FR22, FR23, FR45, FR46, FR47, FR48, FR49, FR50, FR51 NFRs addressed: NFR1 (page load performance), NFR12 (audit trail for re-open)

Epic 6: Platform Administration & Subscription Enforcement

SaaS admin can monitor platform health via a dedicated dashboard (workspace count, user count, storage, system health). Admin can view and respond to support tickets, manage platform configuration, and enforce subscription tier limits across all workspaces. FRs covered: FR27, FR28, FR56, FR57, FR58 NFRs addressed: NFR16-NFR20 (scalability targets)

Epic 7: Production Infrastructure & Deployment

The platform runs reliably in production with EU-hosted infrastructure (CNDP compliance), S3-compatible encrypted file storage, Amazon SES email delivery (eu-west-1), Laravel Forge deployment, Pulse + Sentry monitoring, and automated backup procedures. All reliability and security NFRs are met for launch. FRs covered: NFR-driven epic NFRs addressed: NFR7, NFR13, NFR14, NFR19, NFR21, NFR22, NFR23, NFR24, NFR25, NFR26, NFR27, NFR28


Epic 0: Foundation Migration & Infrastructure Setup

The platform uses correct professional terminology ("declarations" not "folders") and the technical foundation (Redis for cache/queue/sessions, base database migrations) is ready for all new features. This pre-phase must complete before any feature development begins.

Story 0.1: Rename Folders to Declarations in Database

As a developer, I want the database tables and columns to use "declaration" terminology instead of "folder", So that the data layer reflects the correct professional terminology before building new features.

Acceptance Criteria:

Given the existing folders table exists with data When the migration runs Then the folders table is renamed to declarations And all folder_id foreign key columns across related tables are renamed to declaration_id And the folder_invitations table is renamed to declaration_invitations And all indexes referencing "folder" are renamed to reference "declaration" And the migration is reversible (rollback renames back to "folder") And existing data is preserved with zero data loss

Story 0.2: Rename Folders to Declarations in Backend

As a developer, I want all backend PHP code to use "Declaration" terminology instead of "Folder", So that the codebase is consistent with the database and professional domain language.

Acceptance Criteria:

Given the database migration from Story 0.1 has been applied When the backend rename is complete Then the Folder model is renamed to Declaration with updated table name, relationships, and fillable attributes And the FolderInvitation model is renamed to DeclarationInvitation And FolderController is renamed to DeclarationController with updated route model binding And FolderMediaController is renamed to DeclarationMediaController And all Form Request classes (StoreFolderRequest, UpdateFolderRequest) are renamed with "Declaration" prefix And all Enums (FolderStatus, FolderType, FolderPriority) are renamed to DeclarationStatus, DeclarationType, DeclarationPriority And all Mail classes (FolderConfirmationMail, FolderFileRequestMail, FolderInviteMail, FolderSituationMail, FolderTextMessageMail) are renamed with "Declaration" prefix And the ValidateFolderInvitation middleware is renamed to ValidateClientPortalToken And all routes in web.php are updated from "folders" to "declarations" And all existing feature tests are updated and passing And FolderFactory is renamed to DeclarationFactory

Story 0.3: Rename Folders to Declarations in Frontend

As a developer, I want all frontend Vue/TypeScript code to use "declaration" terminology instead of "folder", So that the UI codebase is consistent with the backend and domain language.

Acceptance Criteria:

Given the backend rename from Story 0.2 is complete When the frontend rename is complete Then resources/js/pages/folders/ directory is renamed to resources/js/pages/declarations/ And all Vue page components (Index, Show, Create, Edit) reference "declaration" in component names and content And all folder-related components in resources/js/components/ are renamed with "Declaration" prefix And TypeScript types referencing "Folder" are renamed to "Declaration" in resources/js/types/ And all Wayfinder route references are updated from folder routes to declaration routes And all user-facing labels and text display "Declaration" / "Declarations" (French: "Declaration" / "Declarations") And the client portal pages reference "declaration" instead of "folder" in all user-facing copy And the application compiles without TypeScript errors And all pages render correctly with no broken links or references

Story 0.4: Configure Redis for Cache, Queue & Sessions

As a platform operator, I want Redis configured as the driver for caching, job queues, and session storage, So that the application has the infrastructure foundation for queued email delivery, dashboard caching, and reliable session management.

Acceptance Criteria:

Given the Docker/Sail development environment is running When Redis is configured Then a Redis service is added to compose.yaml (or confirmed already present via Sail) And CACHE_STORE=redis is set in the environment configuration And QUEUE_CONNECTION=redis is set in the environment configuration And SESSION_DRIVER=redis is set in the environment configuration And the queue:work process is running and processes test jobs successfully And Cache::put() and Cache::get() operations work correctly And user sessions persist correctly across page navigations And the composer dev command starts the queue worker alongside the existing services And a failed_jobs table migration exists for monitoring failed queue jobs

Story 0.5: Add Foundation Database Migrations and Declaration Status Flow

As a developer, I want the base database migrations and declaration status enforcement in place, So that future epics can build on a solid data foundation without needing to alter the schema themselves.

Acceptance Criteria:

Given the declarations table exists (from Story 0.1) When the foundation migrations and observer are in place Then a permissions JSON column is added to the workspace_user pivot table (nullable, default null) And the WorkspaceUser model casts permissions to array And an archived_at nullable timestamp column is added to the declarations table And the Declaration model has scopeActive() (whereNull archived_at) and scopeArchived() (whereNotNull archived_at) Eloquent scopes And the DeclarationStatus enum includes all lifecycle values: created, en_cours, en_attente_client, termine, ferme And a DeclarationObserver is registered that enforces valid status transitions per the Architecture status flow And the observer auto-sets archived_at = now() when status becomes ferme And invalid status transitions throw a validation error (e.g., created cannot jump to ferme) And all existing tests pass with the new migrations applied


Epic 0 Summary: 5 stories covering terminology migration (database, backend, frontend), Redis infrastructure, and foundation migrations. All FRs for Epic 0 are covered.

Epic 1: Team Management & Permission System

Firm owners can manage their team, assign roles (Owner/Manager/Worker), and configure per-workspace permission toggles for Managers. The system enforces role-based access across all views -- Workers see only assigned items, Managers/Owners see all. Workspace switching works for multi-workspace Owners.

Story 1.1: Permission Configuration & Controller Traits

As a developer, I want a centralized permission configuration and reusable controller traits for workspace scoping and permission checking, So that all future controllers can enforce role-based access consistently with minimal code duplication.

Acceptance Criteria:

Given the permissions JSON column exists on workspace_user (from Story 0.5) When the permission system is configured Then a config/permissions.php file defines default permissions per role: Owner gets all (['*']), Manager gets configurable defaults (can_manage_team: false, can_view_activity_logs: true, can_configure_portal: false), Worker gets none And a Permission enum exists in app/Enums/Permission.php with all permission keys as snake_case values And a HasWorkspaceScope trait exists in app/Concerns/ that provides currentWorkspace() and authorizeWorkspaceAccess() helper methods using session-based current_workspace_id And an AuthorizesPermissions trait exists in app/Concerns/ that provides authorizePermission(string $permission) method: Owners always pass, Workers always fail (abort 404), Managers check JSON permissions column And authorization failures return abort(404) (never 403) per Architecture convention And unit tests verify permission checking logic for all three roles And unit tests verify that unknown permission keys default to false

Story 1.2: Team Management Page -- View & Invite Members

As a firm owner, I want to view all team members in my workspace and invite new members via email, So that I can build my team and see who has access to my firm's data.

Acceptance Criteria:

Given the owner is logged in and viewing the Team page When they navigate to /team Then a team index page displays all workspace members in a table showing: name, email, role, join date, and status (active/pending) And pending invitations are shown with "Pending" status badge And an "Invite Member" button opens a form with email input and role selection dropdown (Manager or Worker) And submitting the invite form sends an invitation email to the specified address And the invitation appears in the team list as "Pending" immediately And the page uses the HasWorkspaceScope trait to scope members to the current workspace And Workers cannot access the Team page (abort 404) And Managers can view the team list but only see the "Invite Member" button if they have can_manage_team permission And the page follows the existing AppLayout with breadcrumbs: Dashboard / Team And an EmptyState is shown when the workspace has only the owner ("Invite your first team member")

Story 1.3: Role Assignment & Member Removal

As a firm owner, I want to change a team member's role or remove them from the workspace, So that I can adjust team structure as my firm's needs evolve.

Acceptance Criteria:

Given the owner is viewing the Team management page When they click the action menu on a team member row Then a dropdown menu shows "Change Role" and "Remove from Workspace" options And "Change Role" opens a popover with role selection (Manager/Worker) and a confirm button And changing a role updates the WorkspaceUser role and resets permissions to role defaults And "Remove from Workspace" shows a confirmation dialog with the member's name and a destructive "Remove" button And removing a member detaches them from the workspace (does not delete their user account) And the owner cannot change their own role or remove themselves And a Manager with can_manage_team permission can also change roles and remove members (except the Owner) And all role changes and removals are logged via Spatie Activity Log with actor, target user, old role, and new role And a success toast confirms each action ("Role updated" / "Member removed") And the team list updates immediately after each action

Story 1.4: Manager Permission Toggle Matrix

As a firm owner, I want to configure which specific permissions each Manager has in my workspace, So that I can grant or restrict Manager capabilities based on my trust level and firm structure.

Acceptance Criteria:

Given the owner is viewing the Team management page When they click "Manage Permissions" on a Manager team member Then a permissions page (or slide-over panel) displays all configurable permission toggles with descriptive labels And each toggle shows the permission name in French (e.g., "Gerer l'equipe", "Voir les journaux d'activite", "Configurer le portail client") And toggles reflect the Manager's current permission state (from JSON column) And toggling a permission immediately saves via PUT request to /team/{workspaceUser}/permissions And a success toast confirms "Permissions updated" And the permission toggles are only visible for Manager role members (not Workers or Owners) And only Owners can access the permissions management (Managers with can_manage_team cannot modify other Managers' permissions) And permission changes are logged via Spatie Activity Log And if a Manager's can_manage_team is toggled off, the "Invite Member" button disappears from their Team page view on next load

Story 1.5: Role-Based Access Enforcement Across Views

As a firm worker, I want to see only my assigned clients and declarations when I navigate the platform, So that I can focus on my work without being overwhelmed by the entire firm's data.

Acceptance Criteria:

Given a Worker is logged in to the workspace When they navigate to the Clients page Then they see only clients that have at least one declaration assigned to them And the client count reflects their scoped view (not the total workspace count)

Given a Worker is logged in to the workspace When they navigate to the Declarations page Then they see only declarations where assigned_to equals their user ID And the Declaration model's scopeForUser() method is applied: Workers get where('assigned_to', $user->id), Owners/Managers get all workspace declarations

Given an Owner or Manager is logged in When they navigate to any page (Clients, Declarations) Then they see all items in the workspace with no scoping restrictions

Given a Worker tries to access a declaration not assigned to them via direct URL When the controller loads the declaration Then the system returns 404 (not 403)

And the DeclarationController, ClientController, and all existing controllers apply HasWorkspaceScope and role scoping consistently And activity log viewing is scoped: Owners see all, Managers see all if can_view_activity_logs is true (else 404), Workers see only their own actions And the sidebar navigation adapts per role: Owner/Manager sees "Dashboard, Clients, Declarations, Archive, Team, Settings"; Worker sees "Dashboard, My Declarations, Archive, Settings"

Story 1.6: Workspace Switching for Multi-Workspace Owners

As a firm owner with multiple workspaces, I want to switch between my workspaces from the sidebar, So that I can manage multiple cabinets without logging out and back in.

Acceptance Criteria:

Given an Owner has multiple workspaces When they click the workspace switcher in the sidebar Then a dropdown displays all workspaces they own with the current one highlighted And selecting a different workspace updates the session current_workspace_id and redirects to the dashboard And all data (team, clients, declarations) now reflects the selected workspace

Given a Manager or Worker belongs to a single workspace When they view the sidebar Then the workspace name is displayed but the switcher dropdown is not available (or shows only one entry)

And the existing WorkspaceSwitchController is enhanced if needed to support the improved UX And workspace switching is logged in the activity log


Epic 1 Summary: 6 stories covering permission configuration, team management (view, invite, role change, remove), Manager permission toggles, role-based access enforcement across all views, and workspace switching. All FRs for Epic 1 (FR3, FR4, FR7, FR8, FR9, FR10, FR11) are covered.

Epic 2: Role-Driven Dashboard & Command Center

Owners/Managers open the app and see their entire firm's status on one screen -- KPI summary cards, priority alerts, declaration status overview, and activity feed. Workers see a scoped dashboard showing only their assigned workload. The "morning command center" moment that drives trial-to-paid conversion.

Story 2.1: Owner/Manager Command Center Dashboard

As a firm owner or manager, I want to see my entire firm's operational status on one screen when I open the app, So that I can instantly identify what needs my attention without drilling into individual clients or declarations.

Acceptance Criteria:

Given an Owner or Manager is logged in When they navigate to the Dashboard (/dashboard) Then the page displays a row of KPI summary cards (StatCard components) in a 4-column CSS Grid layout:

  • "En retard" (overdue) -- red, count of declarations past deadline
  • "Cette semaine" (due this week) -- amber, count of declarations due within 7 days
  • "En attente client" (waiting for client) -- blue, count of declarations with en_attente_client status
  • "En cours" (on track) -- green, count of declarations with en_cours status

And each StatCard is clickable and navigates to the Declarations list page with the corresponding filter pre-applied via URL query params (e.g., /declarations?status=en_attente_client) And a declarations summary table appears below the KPI cards showing the most urgent declarations (sorted by deadline ascending, limited to 10-15 rows) with columns: Client, Type, Deadline (with proximity color), Assignee, Status badge And each table row is clickable and navigates to the declaration detail page And inline row actions are available via a dropdown menu (View, Nudge, Reassign) And dashboard data is served from Redis cache (Cache::remember() with 5-minute TTL, key: dashboard:{workspace_id}:{user_id}) And the DashboardController is rewritten to aggregate declaration counts by status, overdue counts, and due-this-week counts using role-scoped queries And the page renders within 3 seconds for workspaces with up to 200 active clients (NFR5) And the page uses the AppLayout with role-driven sidebar navigation

Story 2.2: Priority Alerts Panel

As a firm owner or manager, I want to see a prioritized list of alerts for items requiring immediate attention, So that I can act on the most urgent issues before they become missed deadlines.

Acceptance Criteria:

Given the Owner/Manager dashboard is loaded When the priority alerts panel renders (below or alongside the KPI cards) Then alerts are displayed as a list sorted by urgency with the following categories:

  • Critical (red): Overdue declarations (past deadline) -- shows client name, declaration type, days overdue
  • Warning (amber): Approaching deadlines (due within 3 days) -- shows client name, type, days remaining
  • Info (blue): Missing client documents (status en_attente_client for >3 days) -- shows client name, days waiting

And each alert includes a direct link to the relevant declaration detail page And the alert count is capped at 20 most urgent items with a "View all" link to the filtered declarations list And deadline proximity uses the color gradient: green (>7d) → amber (3-7d) → red (<3d) → pulsing red (overdue) And alerts that have been acted on (status changed, declaration reassigned) disappear on next dashboard refresh And if there are no alerts, an encouraging message is shown: "Aucune alerte -- tout est en ordre" And alert data is included in the same cached dashboard query (no separate API call)

Story 2.3: Worker Scoped Dashboard

As a firm worker, I want to see only my assigned declarations and their statuses when I open the app, So that I can quickly identify what I need to work on today without information overload.

Acceptance Criteria:

Given a Worker is logged in When they navigate to the Dashboard Then the page displays the same KPI card layout as the Owner dashboard but counts reflect only declarations assigned to the Worker And the summary table shows only the Worker's assigned declarations sorted by deadline ascending And the priority alerts panel shows only alerts for the Worker's assigned declarations And StatCards are clickable and navigate to /declarations?assignee=me&status={status} And the page title or subtitle indicates the scoped view (e.g., "Mes declarations" or the Worker's name)

Given a Worker has no assigned declarations When they view the Dashboard Then an EmptyState is displayed: "Aucune declaration assignee -- contactez votre responsable"

And the DashboardController uses the same code path for all roles but applies forUser() scope, resulting in scoped data for Workers And Workers do NOT see team workload distribution or activity from other team members And the Worker dashboard renders within 3 seconds (NFR5)

Story 2.4: Dashboard Activity Feed

As a firm owner or manager, I want to see a stream of recent workspace activity on my dashboard, So that I can stay aware of team actions, client uploads, and status changes without checking individual declarations.

Acceptance Criteria:

Given the Owner/Manager dashboard is loaded When the activity feed panel renders (right column on desktop, below main content on mobile) Then the feed shows the 20 most recent workspace events in reverse chronological order And each entry displays: actor avatar/initials, action description, target entity link, and relative timestamp ("il y a 2 heures") And event types include: declaration status changes, document uploads by clients, declaration reassignments, team member role changes, new declarations created And clicking an event navigates to the relevant entity (declaration detail, client page, team page) And the feed uses data from Spatie Activity Log, filtered to the current workspace

Given a Worker views the dashboard When the activity feed renders Then the feed shows only activity related to the Worker's assigned declarations (not the entire workspace)

And the activity feed is included in the Inertia page props (server-rendered, no separate API call) And on mobile viewports (<768px), the activity feed is accessible via a tab or expandable section (not hidden entirely) And if there is no recent activity, a neutral message is shown: "Aucune activite recente"


Epic 2 Summary: 4 stories covering the Owner/Manager command center with KPI cards and declarations table, priority alerts panel with deadline color coding, Worker scoped dashboard, and activity feed. All FRs for Epic 2 (FR24, FR25, FR26) are covered.

Epic 3: Collaboration, Nudge System & Notifications

Owners/Managers can nudge team members on specific declarations with one click. All users have a notification center with direct links to relevant declarations. The system sends reliable email notifications for key events. Bulk notification scheduling enables document request campaigns to multiple clients.

Story 3.1: Notification Infrastructure Setup

As a developer, I want the Laravel notification system configured with database and mail channels, So that all future notification features (nudges, alerts, bulk notifications) have a reliable foundation to build on.

Acceptance Criteria:

Given Redis queue is configured (from Story 0.4) When the notification infrastructure is set up Then the notifications table migration is created and run (php artisan notifications:table) And the User model confirms the Notifiable trait is present (already via Fortify) And a NotificationType enum is created in app/Enums/ with values: nudge, declaration_overdue, document_uploaded, bulk_notification, status_changed And a base notification pattern is established: all notification classes implement ShouldQueue, use Queueable trait, and include workspace_id in the toArray() payload And a NudgeNotification class is created in app/Notifications/ with database and mail channels And a DocumentUploadedNotification class is created with database channel only And a DeclarationOverdueNotification class is created with database and mail channels And notification queries are workspace-scoped (filtered via the workspace_id in the notification data payload) And all notification classes have queued email sending with up to 3 retries (NFR26)

Story 3.2: One-Click Nudge System

As a firm owner or manager, I want to nudge a team member about a specific declaration with one click, So that I can quickly signal that something needs attention without composing a message or making a phone call.

Acceptance Criteria:

Given an Owner or Manager is viewing a declaration list (dashboard table or declarations index) When they click the nudge icon on a declaration row Then a NudgePopover appears showing the assigned worker's name and a "Send Nudge" button And clicking "Send Nudge" dispatches a NudgeNotification to the assigned worker via NudgeController@store And the notification is saved to the database (in-app) AND queued as an email And a success toast confirms: "Nudge envoye a [worker name]" And the popover closes automatically after sending And no message composition is required -- it is a pure signal ("Your attention is needed on this declaration")

Given a Worker or a user without nudge permission When they view declaration rows Then the nudge icon is not displayed

And the NudgeController validates that the sender is Owner or Manager And the NudgeController validates that the declaration belongs to the current workspace And the nudge email contains: declaration type, client name, deadline, and a direct link to the declaration detail page And the nudge is logged via Spatie Activity Log (actor: sender, target: declaration, action: "nudged") And duplicate nudges on the same declaration within 1 hour are prevented (debounce) with a user-friendly message

Story 3.3: Notification Center & Bell

As a team member, I want to see a notification bell with unread count and access a notification center listing all my notifications, So that I never miss a nudge, alert, or important event and can navigate directly to the relevant declaration.

Acceptance Criteria:

Given a user is logged in and has unread notifications When the page loads Then a NotificationBell component in the app header/sidebar shows a badge with the unread notification count And clicking the bell opens a dropdown panel showing the 10 most recent notifications And each notification displays: type icon, description text, relative timestamp ("il y a 5 min"), and read/unread visual state And clicking a notification navigates to the relevant declaration detail page and marks the notification as read And a "Mark all as read" action is available in the dropdown header And a "View all notifications" link at the bottom navigates to /notifications

Given a user navigates to /notifications When the notification index page loads Then all notifications are listed in reverse chronological order with pagination (25 per page) And each notification shows: type icon, full description, timestamp, and read/unread state And notifications can be marked as read individually or in bulk ("Mark all as read") And notifications are scoped to the current workspace (via workspace_id in notification data)

Given a user has zero notifications When they click the bell or visit the notifications page Then an EmptyState is shown: "Aucune notification" And the bell badge is hidden (no "0" displayed)

Story 3.4: Bulk Client Notification Scheduling

As a firm worker or manager, I want to send document request notifications to multiple clients at once, So that I can efficiently request missing documents without emailing each client individually.

Acceptance Criteria:

Given a user (Owner/Manager/Worker with assigned declarations) is viewing the declarations list When they select multiple declarations with status en_attente_client using row checkboxes Then a BulkActionBar appears at the bottom/top showing: "[N] selected" and a "Notify Clients" button

Given the user clicks "Notify Clients" on the BulkActionBar When the confirmation dialog appears Then it shows the count of clients to be notified and a "Send Notifications" confirmation button And clicking "Send" dispatches a POST request to a bulk notification endpoint And each client receives a personalized email with their token-based portal link for document upload And emails are queued individually via Redis (non-blocking -- controller returns immediately) And a success toast confirms: "[N] notifications envoyees"

Given 50 client notifications are scheduled When the queue processes them Then all 50 emails are sent within 10 seconds (NFR3) And failed individual sends retry automatically (up to 3 retries per NFR26) And the bulk operation is logged via Spatie Activity Log with the count and actor

And Workers can only bulk-notify clients for their own assigned declarations And the existing email templates (DeclarationFileRequestMail) are reused for the notification content

Story 3.5: Email Notification Enhancement for Key Events

As a platform user, I want to receive email notifications for important events (nudges, document uploads, status changes), So that I stay informed about critical actions even when I'm not actively using the platform.

Acceptance Criteria:

Given a worker receives a nudge from a manager When the NudgeNotification processes via queue Then the worker receives an email with: sender name, declaration details (client, type, deadline), and a direct link button ("Voir la declaration") And the email uses a NudgeNotificationMail Markdown mailable with professional French copy

Given a client uploads a document via the portal When the upload is confirmed Then the assigned worker receives a DocumentUploadedNotification in-app (database channel only -- no email for uploads to avoid noise)

Given a declaration's status changes When the DeclarationObserver fires Then if the new status is en_attente_client, the client receives the existing document request email via their token link And if the new status is ferme, dashboard cache is invalidated (from Story 2.1)

Given email delivery fails When the queue retries Then the system retries up to 3 times within 5 minutes (NFR26) And permanently failed emails are logged to the failed_jobs table for monitoring And all queued emails use ShouldQueue and Queueable traits

And email notifications respect business context: no email is sent for minor status transitions (e.g., createden_cours) And all email templates include the workspace firm name and logo in the header


Epic 3 Summary: 5 stories covering notification infrastructure, one-click nudge system, notification center with bell, bulk client notification scheduling, and email enhancement for key events. All FRs for Epic 3 (FR29, FR30, FR31, FR32, FR33) are covered.

Epic 4: Bulk Operations, Search & Advanced Filtering

Power users can create 50 declarations in one action with type and deadline selection across multiple clients. A persistent filter bar with status, client, assignee, type, and date range filters appears on all list views. Quick search across clients and declarations returns results in under 1 second. Archive has its own top-level navigation with dedicated filters.

Story 4.1: FilterBar Component & useFilters Composable

As a firm user, I want a persistent filter bar above data tables that lets me filter by status, client, assignee, type, and date range, So that I can quickly narrow down the data to exactly what I need without losing my filter selections when navigating.

Acceptance Criteria:

Given a user is viewing any list page (Declarations, Clients, Archive) When the page loads Then a FilterBar component is rendered above the data table containing:

  • Status filter (Select/Combobox with declaration status options)
  • Client filter (Combobox with searchable client list)
  • Assignee filter (Combobox with workspace team members -- Owner/Manager only, hidden for Workers)
  • Type filter (Select with declaration type options)
  • Date range filter (DateRangePicker with presets: "Cette semaine", "Ce mois", "Ce trimestre")
  • Search input (for quick text search -- wired in Story 4.3)

And selecting any filter immediately updates the table data via Inertia router.get() with URL query params And no "Apply" button is needed -- filters are instant on change And active filters are shown as removable chips/badges below the filter bar with an "X" to remove each And a "Clear all filters" link appears when any filter is active And filter state persists in the URL: navigating away and pressing browser back restores filters And a useFilters composable in resources/js/composables/useFilters.ts manages filter state, URL param serialization, and reset logic And pagination resets to page 1 when any filter changes And the FilterBar uses preserveState: true and preserveScroll: true on Inertia requests And on mobile (<768px), the FilterBar collapses behind an expandable toggle ("Filtres") And shadcn-vue combobox, select, calendar, and popover components are installed if not already present

Story 4.2: Apply FilterBar to Declarations & Clients Pages

As a firm user, I want filtering and sorting on the Declarations and Clients list pages, So that I can find specific declarations by status, deadline, or assignee and sort by the column that matters most.

Acceptance Criteria:

Given the FilterBar component exists (from Story 4.1) When the Declarations index page (/declarations) loads Then the FilterBar is integrated above the declarations table And the DeclarationController@index reads filter params from the Request and applies Eloquent scopes via an applyFilters() method:

  • statuswhere('status', $status)
  • assigneewhere('assigned_to', $assignee) (or forUser() scope for Workers)
  • client_idwhere('client_id', $clientId)
  • typewhere('type', $type)
  • deadline_from / deadline_towhereBetween('deadline', [...]) And default sort is deadline ASC (most urgent first) And column headers are clickable for sorting (ascending → descending → unsorted) with arrow indicators And the table uses server-side pagination (25 rows per page default, configurable 25/50/100) And a "Showing X of Y declarations" count is displayed below the table

Given the Clients index page (/clients) loads When filters are applied Then the FilterBar shows relevant client filters (status, name search) And default sort is name ASC And the ClientController@index applies the same applyFilters() pattern

And Workers see the assignee filter pre-set to themselves (non-removable) And empty filter results show an EmptyState: "Aucun resultat pour ces filtres" with a "Clear filters" button

Story 4.3: Quick Search with MySQL FULLTEXT

As a firm user, I want to type a few characters into a search box and instantly find clients or declarations matching my query, So that I can quickly locate a specific client or declaration without scrolling through tables.

Acceptance Criteria:

Given a user types into the SearchInput field in the FilterBar When they type at least 2 characters Then the search query is sent as a URL param (?search=query) via the useFilters composable And the backend uses MATCH...AGAINST in natural language mode on FULLTEXT indexed columns And results are returned within 1 second (NFR6)

Given the FULLTEXT indexes need to be created When the migration runs Then FULLTEXT indexes are added on:

  • declarations table: composite index on declaration type and any notes/description columns
  • clients table: composite index on client name, company name, and any searchable text fields And the migration uses DB::statement() for MySQL FULLTEXT index creation

Given a search returns no results When the table updates Then an EmptyState is shown: "Aucun resultat pour '[query]'" with a suggestion to adjust the search

And search works in combination with other active filters (additive filtering) And the SearchInput has a debounce of 300ms to avoid excessive requests while typing And a clear (X) button inside the search input clears the search term And search is workspace-scoped (always combined with workspace_id filter)

Story 4.4: Bulk Declaration Creation

As a firm owner or manager, I want to create declarations for multiple clients at once by selecting clients, choosing a type, and setting a deadline, So that I can set up 50 TVA declarations in minutes instead of creating them one by one.

Acceptance Criteria:

Given an Owner or Manager navigates to /declarations/bulk-create When the bulk creation page loads Then a multi-step form is displayed:

  1. Select Clients: Searchable client list with checkboxes, "Select all" toggle, selected count displayed
  2. Configure Declaration: Type selection (Combobox with declaration types), deadline date picker, optional assignee selection (Combobox with team members)
  3. Review & Confirm: Summary showing count of declarations to be created, type, deadline, assignee

Given the user confirms the bulk creation When they click "Create [N] Declarations" Then all declarations are created within a DB::transaction() (all-or-nothing: if any single declaration fails validation or creation, the entire batch is rolled back and zero declarations are created — no partial creation) And each declaration is created with: workspace_id, client_id, type, deadline, assigned_to, status: created And the controller returns immediately with a redirect and success toast: "[N] declarations creees" And if notify_clients is checked, client notification emails are queued via Redis (non-blocking)

Given 50 declarations are being created When the transaction completes Then all 50 are created within 10 seconds (NFR2) And the dashboard cache is invalidated for the workspace

Given a Worker navigates to the bulk creation page When the page loads Then they receive a 404 (Workers cannot bulk-create)

And the BulkStoreDeclarationRequest validates: at least 1 client selected, valid type, valid deadline (not in the past), valid assignee (belongs to workspace) And the bulk creation is logged via Spatie Activity Log with count, type, and actor And a "Bulk Create" button is accessible from the Declarations index page header (Owner/Manager only)

Story 4.5: Archive Navigation with Dedicated Filters

As a firm user, I want the Archive section accessible as a top-level navigation item with its own dedicated filters and search, So that I can browse historical declarations separately from active ones with appropriate filtering options.

Acceptance Criteria:

Given a user is logged in When they view the sidebar navigation Then "Archive" appears as a top-level nav item (below Declarations, above Team/Settings) And clicking "Archive" navigates to /archive

Given the user is on the Archive index page When the page loads Then the FilterBar is displayed with filters relevant to archived declarations: client, type, date range (archive date), original assignee, search And the table shows archived declarations using Declaration::archived()->forUser() scope And archived rows have a visually muted/desaturated styling to distinguish them from active declarations (FR50) And each row shows: Client, Type, Original Deadline, Archived Date, Status badge ("Archive" in gray) And default sort is archived_at DESC (most recently archived first) And server-side pagination with the same 25/50/100 options

Given a Worker views the Archive When the page loads Then they see only archived declarations that were originally assigned to them

And the ArchiveController@index reads filters from Request and applies scopes identically to the declarations filter pattern And the Archive page uses the same FilterBar component and useFilters composable (reusable) And an EmptyState is shown if no archived declarations exist: "Aucune declaration archivee"


Epic 4 Summary: 5 stories covering the FilterBar component with URL persistence, filter integration on Declarations and Clients pages, quick search with MySQL FULLTEXT, bulk declaration creation, and Archive as a top-level nav with dedicated filters. All FRs for Epic 4 (FR17, FR41, FR42, FR43, FR44) are covered.

Epic 5: Archive System & Document Preview

Completed declarations are automatically archived when closed, preserving full history (documents, messages, status changes, activity log). Users can browse, filter, and search the archive. Archive detail pages show read-only snapshots with in-app document preview (PDF.js). Owners can re-open archived items with audit trail. Bulk ZIP download and 10-year retention policy ensure compliance.

Story 5.1: Auto-Archive on Close & History Preservation

As a firm owner or manager, I want declarations to be automatically archived when I mark them as closed, with their full history preserved, So that completed work is moved out of active views while retaining a complete audit trail for future reference.

Acceptance Criteria:

Given a declaration has status ferme (closed) When the system processes the closure (via DeclarationObserver or a scheduled command) Then the declaration's archived_at timestamp is set to now() And the declaration's status transitions to archive And the declaration disappears from the active declarations list (/declarations) and appears in the archive (/archive)

Given a declaration is being archived When the archive process runs Then all related data is preserved in place (no data movement to a separate table):

  • All Spatie Media Library attachments remain linked to the declaration
  • All Spatie Activity Log entries remain linked to the declaration
  • All status change history is preserved
  • All assignment history is preserved
  • The archived_at timestamp is recorded And dashboard cache is invalidated for the workspace (reusing the cache invalidation from Story 2.1)

Given a declaration is already archived When a user attempts to edit it via API Then a 404 response is returned (per NFR9: authorization violations return 404 to prevent tenant/state leakage) And a DeclarationPolicy@update check rejects any update on archived declarations

And the archive transition is logged via Spatie Activity Log (actor: user who closed it, action: "archived", target: declaration) And the migration adds an archived_at nullable timestamp column to the declarations table And an Eloquent scope scopeArchived() filters whereNotNull('archived_at') and scopeActive() filters whereNull('archived_at')

Story 5.2: Archive Detail Page — Read-Only Snapshot

As a firm user, I want to view an archived declaration's complete details in a read-only page showing all historical data, So that I can review past work, verify documents, and trace the full lifecycle without risk of accidental edits.

Acceptance Criteria:

Given a user navigates to /archive/{declaration} for an archived declaration When the page loads Then the ArchiveShow page renders a read-only view with:

  • Client name and details (linked to client profile)
  • Declaration type and original deadline
  • Current status badge ("Archive" in gray/muted styling per FR50)
  • A DeclarationStatusStepper component showing the full lifecycle progression (Created → En cours → En attente client → Termine → Ferme → Archive) with timestamps for each transition
  • Assigned worker name and reassignment history
  • All attached documents listed with file name, size, upload date, and uploader name
  • Activity log timeline showing all actions taken on this declaration (status changes, assignments, nudges, uploads)

Given the page is read-only When a user views the archive detail Then all edit buttons, status change dropdowns, and action buttons are hidden And the page header shows a banner: "Declaration archivee — consultation uniquement" And document download links remain functional (individual file downloads)

Given a Worker views an archived declaration When it was not originally assigned to them Then they receive a 404 (per NFR9: Workers cannot discover existence of declarations not assigned to them)

And the ArchiveController@show loads the declaration with eager-loaded relations: client, assignedTo, media, activities And the DeclarationStatusStepper is a reusable Vue component that renders a horizontal step indicator with status labels and timestamps And the page uses the muted/desaturated visual styling established in Story 4.5 for archived items

Story 5.3: In-App Document Preview

As a firm user, I want to preview documents (PDFs and images) directly within the application without downloading them, So that I can quickly review attached files while staying in context on the declaration detail page.

Acceptance Criteria:

Given a user is viewing a declaration detail page (active or archived) that has attached documents When they click on a document in the attachments list Then a modal/overlay opens showing an in-app preview of the document And PDF files are rendered using PDF.js (pdfjs-dist npm package) in a <canvas> element within a DocumentPreview component And image files (JPG, PNG, WEBP) are rendered in an <img> tag with zoom controls And the preview modal includes: file name in the header, page navigation for multi-page PDFs (prev/next/page count), zoom in/out controls, a "Download" button, and a close (X) button

Given a document format is not previewable (e.g., .xlsx, .docx, .zip) When the user clicks on it Then the file downloads directly instead of opening a preview And the attachment list shows a "preview" icon only for previewable formats (PDF, JPG, PNG, WEBP)

Given a PDF has more than 1 page When the preview modal opens Then the first page is displayed by default And page navigation shows "Page 1 of N" with next/prev buttons And the user can jump to a specific page via a page number input

And the DocumentPreview component is located at resources/js/Components/DocumentPreview.vue And PDF.js is loaded lazily (dynamic import) to avoid increasing the main bundle size And a loading spinner is shown while the PDF renders And preview works on both active declaration detail pages and archive detail pages (from Story 5.2)

Story 5.4: Re-Open Archived Declaration

As a firm owner, I want to re-open an archived declaration and return it to active status, So that I can resume work on a declaration that was prematurely closed or needs additional attention.

Acceptance Criteria:

Given an Owner is viewing an archived declaration detail page (/archive/{declaration}) When they click the "Reouvrir" button Then a confirmation dialog appears: "Etes-vous sur de vouloir reouvrir cette declaration? Elle sera remise dans la liste active avec le statut 'En cours'." And the dialog has "Confirmer" and "Annuler" buttons

Given the Owner confirms the re-open action When the request is processed Then the declaration's archived_at is set to null And the declaration's status is set to en_cours And the declaration disappears from the Archive list and reappears in the active Declarations list And a success toast confirms: "Declaration rouverte avec succes" And the user is redirected to the active declaration detail page (/declarations/{declaration})

Given a Manager or Worker attempts to re-open an archived declaration When they view the archive detail page Then the "Reouvrir" button is not displayed And if they attempt via API, a 404 response is returned (per NFR9: authorization violations return 404)

And the re-open action is logged via Spatie Activity Log with: actor (Owner), action "reopened", target (declaration), and a properties JSON including previous_archived_at timestamp And dashboard cache is invalidated for the workspace And the DeclarationPolicy@reopen method checks $user->isOwner() in the current workspace And the re-open endpoint is POST /archive/{declaration}/reopen

Story 5.5: Bulk ZIP Download & Retention Policy

As a firm user, I want to download all documents from an archived declaration as a single ZIP file, and trust that archived data is retained for at least 10 years, So that I can easily export complete declaration records and comply with Moroccan regulatory retention requirements.

Acceptance Criteria:

Given a user is viewing an archived declaration detail page with multiple attached documents When they click "Telecharger tout (ZIP)" Then a ZIP file is generated server-side containing all attached documents from the declaration And the ZIP file name follows the pattern: {client_name}_{declaration_type}_{deadline_date}.zip And the file downloads via a streamed response (ZipStream or ZipArchive with StreamedResponse) And a loading indicator is shown while the ZIP is being prepared

Given an archived declaration has no documents attached When the user views the detail page Then the "Telecharger tout (ZIP)" button is hidden/disabled

Given the ZIP file exceeds 100MB When the generation starts Then the download uses streaming (chunked response) to avoid memory issues And the server does not buffer the entire ZIP in memory

Given the 10-year retention policy (FR51) When archived declarations are stored Then no automated deletion mechanism exists -- archived data is retained indefinitely And the archived_at timestamp enables future retention policy queries (e.g., where archived_at < now() - 10 years) And a declarations:check-retention Artisan command is scaffolded (no-op implementation) that can be scheduled in the future to flag declarations past the 10-year mark And the retention policy is documented in a code comment on the command class

And the ZIP download endpoint is GET /archive/{declaration}/download-zip And the endpoint validates that the user has access to the declaration (workspace + role scoping) And the download action is logged via Spatie Activity Log (actor, action: "downloaded_zip", target: declaration)


Epic 5 Summary: 5 stories covering auto-archive with history preservation, read-only archive detail with status stepper, in-app document preview via PDF.js, owner-only re-open with audit trail, and bulk ZIP download with 10-year retention scaffolding. All FRs for Epic 5 (FR21, FR22, FR23, FR45, FR46, FR47, FR48, FR49, FR50, FR51) are covered.

Epic 6: Platform Administration & Subscription Enforcement

SaaS admin can monitor platform health via a dedicated dashboard (workspace count, user count, storage, system health). Admin can view and respond to support tickets, manage platform configuration, and enforce subscription tier limits across all workspaces.

Story 6.1: SaaS Admin Role & Admin Guard

As a platform administrator, I want a dedicated admin role and protected admin area separate from workspace roles, So that only authorized platform administrators can access system-wide management features.

Acceptance Criteria:

Given the admin role needs to be established When the migration runs Then an is_admin boolean column (default false) is added to the users table And this column is independent of workspace roles (a user can be both a workspace Owner and a platform admin)

Given an admin user navigates to /admin When the request is processed Then an AdminMiddleware checks auth()->user()->is_admin === true And if the user is an admin, they access the admin area And if the user is not an admin, they receive a 404 response (not 403, to avoid revealing the admin area exists)

Given an admin accesses the admin area When the admin layout loads Then a dedicated AdminLayout.vue is rendered with its own sidebar navigation containing: Dashboard, Workspaces, Tickets, Settings And the admin layout is visually distinct from the workspace layout (e.g., dark sidebar, "Admin" badge in header) And admin routes are grouped under Route::middleware(['auth', 'admin'])->prefix('admin')->group()

Given an admin wants to return to their workspace When they click "Back to Workspace" in the admin header Then they are redirected to their default workspace dashboard

And the AdminMiddleware is registered in bootstrap/app.php And a database seeder creates at least one admin user for development/staging And the is_admin attribute is hidden from API responses ($hidden on User model)

Story 6.2: Platform Dashboard & Workspace Overview

As a platform administrator, I want a dashboard showing platform-wide metrics and a detailed workspace listing, So that I can monitor platform health, track growth, and identify workspaces that need attention.

Acceptance Criteria:

Given an admin navigates to /admin When the dashboard loads Then summary cards are displayed showing:

  • Total workspaces (active + trial)
  • Total users across all workspaces
  • Total storage used (sum of all Spatie Media Library files) with percentage of capacity
  • Active workspaces vs trial workspaces (with counts and percentages)
  • System health indicator (queue size, failed jobs count) And each metric card shows the current value and a trend indicator (vs last 30 days)

Given an admin navigates to /admin/workspaces When the workspace index page loads Then a table displays all workspaces with columns: Firm Name, Owner Email, Plan Tier, Team Count, Client Count, Declaration Count, Storage Used, Created Date, Status (active/trial/expired) And the table is sortable by any column (click column header) And a search input filters workspaces by firm name or owner email And server-side pagination with 25/50/100 rows per page

Given an admin clicks on a workspace row When the workspace detail page loads (/admin/workspaces/{workspace}) Then a detailed view shows: firm name, owner details, plan tier, all team members with roles, client count, declaration count (by status), storage breakdown, creation date, last activity date And usage metrics are displayed as progress bars against tier limits (e.g., "5/10 team members")

And dashboard metrics are cached in Redis with a 5-minute TTL to avoid expensive aggregate queries on every page load And the AdminDashboardController uses DB::table() aggregate queries (COUNT, SUM) rather than loading all models And the workspace detail uses eager loading to avoid N+1 queries

Story 6.3: Support Ticket Inbox

As a platform administrator, I want to view and respond to support tickets submitted by workspace owners, So that I can provide timely support and track issue resolution across the platform.

Acceptance Criteria:

Given the support ticket system needs to be created When the migration runs Then a support_tickets table is created with: id, workspace_id, user_id, subject, body (text), status (enum: open, in_progress, resolved, closed), priority (enum: low, normal, high), timestamps And a ticket_replies table is created with: id, support_ticket_id, user_id, body (text), is_admin_reply (boolean), timestamps

Given a workspace Owner navigates to Settings When they click "Support" or "Contacter le support" Then a support ticket form is displayed with: Subject (text input), Message (textarea), Priority selector (low/normal/high) And submitting the form creates a new ticket with status open And a success toast confirms: "Ticket envoye — nous vous repondrons sous 24h" And the Owner can view their submitted tickets and replies in a "Mes tickets" section

Given an admin navigates to /admin/tickets When the ticket inbox loads Then all tickets are listed with: Ticket ID, Subject, Workspace (firm name), Submitter, Status badge, Priority badge, Created Date, Last Reply Date And tickets are filterable by status and priority And default sort is: open tickets first, then by priority (high → low), then by created date (oldest first)

Given an admin clicks on a ticket When the ticket detail page loads (/admin/tickets/{ticket}) Then the full conversation thread is displayed (original message + all replies in chronological order) And the admin can type a reply and submit it And the admin can change the ticket status via a dropdown (open → in_progress → resolved → closed) And when the admin replies, the ticket owner receives an email notification with the reply content and a link to view the ticket

And Workers and Managers cannot submit support tickets (Owner-only feature) And the SupportTicketPolicy ensures workspace-scoped access (Owners see only their tickets, admins see all)

Story 6.4: Platform Configuration Management

As a platform administrator, I want to manage global platform settings from a dedicated admin page, So that I can adjust trial durations, storage limits, feature flags, and other configuration without code deployments.

Acceptance Criteria:

Given the platform settings need a storage mechanism When the migration runs Then a platform_settings table is created with: key (string, unique, primary), value (JSON), description (string, nullable), timestamps And a PlatformSetting model is created with a static helper: PlatformSetting::get('key', $default) that reads from cache-first (Redis) and falls back to database And PlatformSetting::set('key', $value) updates the database and busts the cache

Given an admin navigates to /admin/settings When the settings page loads Then configurable settings are displayed in grouped sections:

  • Trial: Default trial duration (days), trial features list
  • Storage: Storage limit per tier (GB), maximum file upload size (MB)
  • Features: Feature flags as toggles (e.g., bulk operations enabled, document preview enabled, client portal enabled)
  • Email: Default sender name, support email address And each setting shows its current value with an inline edit control (input, toggle, or select as appropriate)

Given an admin changes a setting value When they click "Save" (per section or individual save) Then the setting is updated in the database and Redis cache is invalidated And a success toast confirms: "Parametre mis a jour" And the change is logged via Spatie Activity Log (actor: admin, action: "updated_setting", properties: key, old_value, new_value)

And default settings are seeded via a PlatformSettingsSeeder with sensible defaults And the PlatformSetting::get() method handles missing keys gracefully by returning the provided default And settings are cached with a "platform_settings" cache tag for easy bulk invalidation

Story 6.5: Subscription Tier Limit Enforcement

As a platform operator, I want the system to enforce subscription tier limits on team members, clients, storage, and features, So that workspaces operate within their plan's boundaries and are prompted to upgrade when they reach limits.

Acceptance Criteria:

Given subscription tiers need to be defined When the migration runs Then a subscription_tiers table is created with: id, slug (unique: trial, starter, professional, enterprise), name, limits (JSON column containing: max_team_members, max_clients, max_storage_gb, features array), price_monthly (nullable, for display), timestamps And a tier_id foreign key is added to the workspaces table (default: trial tier) And a SubscriptionTier model is created with a hasLimit('key') helper and a getLimit('key') accessor

Given a workspace Owner tries to add a team member When the current team count equals or exceeds the tier's max_team_members limit Then the action is blocked with a 422 response: "Limite atteinte — votre plan autorise [N] membres maximum" And on the frontend, a LimitReachedModal component is displayed showing: current usage, tier limit, and an "Upgrade" CTA button linking to the billing/plan page

Given a workspace tries to create a new client When the current client count equals or exceeds the tier's max_clients limit Then the same limit enforcement pattern applies (422 + LimitReachedModal)

Given a workspace tries to upload a document When the total storage used equals or exceeds the tier's max_storage_gb limit Then the upload is blocked with a 422 response: "Limite de stockage atteinte" And the LimitReachedModal shows current storage usage vs limit

Given a tier restricts certain features (e.g., bulk operations only on Professional+) When a user in a lower tier attempts to access a restricted feature Then the feature button/link shows a lock icon with "Pro" badge And clicking it displays the LimitReachedModal with feature comparison

Given an admin navigates to /admin/workspaces/{workspace}/plan When the plan management page loads Then the admin can change the workspace's tier via a dropdown And changing the tier takes effect immediately And the change is logged via Spatie Activity Log

And limit checks are implemented via a SubscriptionLimitService that is called in relevant controllers/policies And a workspace.tier relationship is eager-loaded on the Workspace model (cached per request) And the LimitReachedModal is a reusable Vue component at resources/js/Components/LimitReachedModal.vue And tier definitions are seeded via SubscriptionTierSeeder with default limits matching the PRD's subscription tiers


Epic 6 Summary: 5 stories covering admin role/guard setup, platform dashboard with workspace overview, support ticket inbox with threaded replies, platform configuration management with cached settings, and subscription tier limit enforcement with upgrade prompts. All FRs for Epic 6 (FR27, FR28, FR56, FR57, FR58) are covered.

Epic 7: Production Infrastructure & Deployment

The platform runs reliably in production with EU-hosted infrastructure (CNDP compliance), S3-compatible encrypted file storage, Amazon SES email delivery (eu-west-1), Laravel Forge deployment, Pulse + Sentry monitoring, and automated backup procedures. All reliability and security NFRs are met for launch.

Story 7.1: Encrypted File Storage with S3-Compatible Backend

As a platform operator, I want all uploaded documents stored in an S3-compatible bucket with server-side encryption and secure access controls, So that client documents are protected at rest and in transit, meeting CNDP compliance and data security requirements.

Acceptance Criteria:

Given the file storage needs to be configured for production When the storage configuration is set up Then config/filesystems.php defines an s3 disk pointing to an EU-hosted S3-compatible bucket (e.g., Scaleway Object Storage eu-west or AWS S3 eu-west-1) And server-side encryption is enabled (AES-256 / ServerSideEncryption: AES256) on the bucket policy And the FILESYSTEM_DISK environment variable is set to s3 in production

Given Spatie Media Library is configured When files are uploaded via the application Then Media Library stores files on the s3 disk And the media table stores the disk name and path but never the full URL And all file access goes through signed URLs (time-limited, default 30 minutes) generated via $media->getTemporaryUrl(now()->addMinutes(30))

Given a user requests a document When the application generates a download/preview URL Then a MediaPolicy validates: the user belongs to the same workspace as the declaration, and role-based access is enforced (Workers only access their assigned declarations' documents) And the signed URL expires after 30 minutes And direct bucket access is blocked (bucket policy denies public access)

Given the storage needs to handle production scale When files are uploaded Then uploads use multipart upload for files >5MB And the maximum upload size is enforced at 20MB per file (configurable via PlatformSetting from Story 6.4) And CORS is configured on the bucket to allow uploads from the application domain only

And the .env.example is updated with all required S3 configuration keys (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, AWS_ENDPOINT) And local development continues to use the local disk (no S3 dependency for development) And a storage:verify Artisan command is created that tests bucket connectivity and encryption settings

Story 7.2: Email Delivery via Amazon SES

As a platform operator, I want email delivery configured through Amazon SES with proper authentication and monitoring, So that transactional emails (nudges, notifications, client portal links) are delivered reliably with >99% success rate.

Acceptance Criteria:

Given email delivery needs production configuration When the mail driver is configured Then config/mail.php uses the ses driver with region eu-west-1 And the MAIL_MAILER environment variable is set to ses in production And the AWS SES credentials are configured via environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, shared with S3 if same account)

Given the sending domain needs verification When SES is set up Then SPF, DKIM, and DMARC DNS records are documented in a docs/email-setup.md checklist And the MAIL_FROM_ADDRESS and MAIL_FROM_NAME are set to the platform's verified sending identity And SES is moved out of sandbox mode (production access requested)

Given emails are sent from the application When the queue processes email jobs Then all emails use ShouldQueue and are dispatched via the Redis queue (non-blocking) And failed email jobs retry up to 3 times with exponential backoff (NFR26) And permanently failed emails land in the failed_jobs table

Given SES sends bounce or complaint notifications When an SNS webhook receives the event Then a SesWebhookController processes bounce/complaint notifications at POST /webhooks/ses And bounced email addresses are logged for review And complaint addresses are flagged to prevent future sends And the webhook endpoint validates the SNS message signature for security

And email rate limiting is configured to stay within SES sending limits (default: 14 emails/second) And local development uses the log or mailtrap driver (no SES dependency) And a mail:test Artisan command sends a test email to verify SES configuration

Story 7.3: Laravel Forge Deployment & CI Pipeline

As a platform operator, I want automated zero-downtime deployments via Laravel Forge on an EU-hosted VPS, So that code changes are deployed reliably without interrupting active users, and the infrastructure is CNDP-compliant.

Acceptance Criteria:

Given a production server needs provisioning When Laravel Forge provisions the server Then the VPS is hosted in an EU data center (e.g., Scaleway Paris, Hetzner Helsinki, OVH Gravelines) for CNDP compliance (NFR13) And the server runs: PHP 8.3+, MySQL 8.0+, Redis 7+, Nginx, Node.js 20+, Supervisor (for queue workers) And SSL is provisioned via Let's Encrypt with auto-renewal

Given a deployment is triggered (via Forge deploy button or git push to main) When the deployment script runs Then zero-downtime deployment is achieved via the following sequence:

  1. git pull origin main
  2. composer install --no-dev --optimize-autoloader
  3. npm ci && npm run build
  4. php artisan migrate --force
  5. php artisan config:cache && php artisan route:cache && php artisan view:cache
  6. php artisan queue:restart (graceful restart)
  7. Symlink swap (Forge's built-in zero-downtime) And if any step fails, the deployment aborts and the previous release remains active And deployment notifications are sent to a configured channel (email or Slack webhook)

Given the production environment needs configuration When .env is set on the server Then all environment variables are configured: APP_ENV=production, APP_DEBUG=false, database credentials, Redis connection, S3 credentials, SES credentials, Sentry DSN And APP_KEY is generated once and securely stored And session, cache, and queue drivers are all set to redis

Given Nginx needs configuration When the server is set up Then Nginx is configured with: gzip compression, proper cache headers for static assets (30-day max-age for versioned assets), security headers (X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security) And request body size limit is set to 25MB (to accommodate file uploads)

And a Supervisor configuration runs at least 2 queue workers (php artisan queue:work --tries=3 --timeout=90) And a cron entry runs php artisan schedule:run every minute And the deployment script is documented in docs/deployment.md

Story 7.4: Monitoring, Alerting & Error Tracking

As a platform operator, I want real-time monitoring of application health, performance, and errors, So that I can detect and respond to issues before they impact users, maintaining 99.5% uptime (NFR21).

Acceptance Criteria:

Given error tracking needs to be configured When Sentry is integrated Then the sentry/sentry-laravel package is installed and configured with a DSN via SENTRY_DSN environment variable And all unhandled exceptions are reported to Sentry with: stack trace, user context (id, workspace_id), request data, and environment tags And performance monitoring is enabled with a sample rate of 0.2 (20% of transactions) in production And sensitive data (passwords, tokens) is scrubbed from Sentry reports via before_send callback

Given application health needs monitoring When a health check endpoint is accessed at GET /health Then the endpoint returns JSON with status checks for: database connectivity, Redis connectivity, queue health (pending job count), storage accessibility And if all checks pass: HTTP 200 with {"status": "healthy"} And if any check fails: HTTP 503 with {"status": "unhealthy", "checks": {...}} detailing which check failed And the endpoint is publicly accessible (no auth) for external uptime monitoring services And a HealthCheckController runs each check with a 5-second timeout

Given Laravel Pulse is installed for internal monitoring When an admin navigates to /admin/pulse (or Pulse's default route, protected by admin middleware) Then the Pulse dashboard shows: slow queries (>100ms), cache hit/miss rates, queue throughput and wait times, active users, and exception counts And Pulse data is stored in its own database table (default configuration) And Pulse data retention is set to 7 days

And structured logging is configured: LOG_CHANNEL=stack with daily channel in production (14-day retention, max 500MB) And critical log events include workspace_id and user_id context via a LogContextMiddleware And alert thresholds are documented: error rate >1% triggers Sentry alert, queue depth >100 triggers notification, p95 response time >2s triggers review

Story 7.5: Automated Backups & Disaster Recovery

As a platform operator, I want automated daily backups of the database and uploaded files with a tested restore procedure, So that I can recover from data loss or corruption with zero data loss for completed transactions (NFR28).

Acceptance Criteria:

Given the backup system needs to be configured When spatie/laravel-backup is installed Then config/backup.php is configured to:

  • Back up the MySQL database (full dump)
  • Back up the S3 media files (or reference them if S3 versioning is enabled)
  • Store backups in a separate S3 bucket (backups bucket, EU-hosted)
  • Encrypt backup archives with a backup password (via backup.backup.password)

Given the backup schedule is configured When the Laravel scheduler runs Then backup:run executes daily at 02:00 UTC (low-traffic period) And backup:clean runs daily at 03:00 UTC to enforce retention policy And retention policy: 30 daily backups, 12 monthly backups (first of month), 5 yearly backups (January 1st) And backup:monitor runs daily at 04:00 UTC and sends a notification if the latest backup is older than 26 hours

Given a backup fails When backup:run encounters an error Then a notification is sent via mail to the configured admin email And the failure is logged to Sentry as a warning-level event And the BackupFailedNotification includes: error message, server name, timestamp

Given a disaster recovery scenario When a restore is needed Then a documented restore procedure exists in docs/disaster-recovery.md covering:

  1. Provision new server via Forge (or restore existing)
  2. Download latest backup from S3 backup bucket
  3. Decrypt and extract backup archive
  4. Restore MySQL dump: mysql -u root database < dump.sql
  5. Verify data integrity (row counts, latest timestamps)
  6. Update DNS if server IP changed
  7. Verify application health via /health endpoint And the restore procedure is tested at least once on staging before launch

Given zero data loss is required for completed transactions (NFR28) When the database is configured Then MySQL binary logging is enabled (binlog_format=ROW) for point-in-time recovery between daily backups And binary logs are retained for 7 days And the point-in-time recovery procedure is documented in docs/disaster-recovery.md

And backup file naming follows: {app_name}_{date}_{time}.zip And the backup S3 bucket has a lifecycle policy matching the retention configuration And local development does not run backup schedules


Epic 7 Summary: 5 stories covering S3-compatible encrypted file storage with signed URLs, Amazon SES email delivery with bounce handling, Laravel Forge zero-downtime deployment on EU-hosted infrastructure, Pulse + Sentry monitoring with health checks, and automated backups with documented disaster recovery. All NFRs for Epic 7 (NFR7, NFR13, NFR14, NFR19, NFR21-NFR28) are covered.