Files
L-Ami-Fiduciaire/_bmad-output/planning-artifacts/architecture.md

1268 lines
69 KiB
Markdown
Raw Normal View History

---
stepsCompleted: [1, 2, 3, 4, 5, 6, 7, 8]
lastStep: 8
status: 'complete'
completedAt: '2026-03-11'
inputDocuments:
- '_bmad-output/planning-artifacts/product-brief-l-ami-fiduciaire-2026-03-10.md'
- '_bmad-output/planning-artifacts/prd.md'
- '_bmad-output/planning-artifacts/prd-validation-report.md'
- '_bmad-output/planning-artifacts/ux-design-specification.md'
- '_bmad-output/planning-artifacts/research/market-fiduciary-saas-morocco-research-2026-03-10.md'
- '_bmad-output/planning-artifacts/research/domain-moroccan-fiduciary-operations-research-2026-03-10.md'
- '_bmad-output/planning-artifacts/research/ecosystem-partners-morocco-fiduciary-research-2026-03-10.md'
- '_bmad-output/planning-artifacts/research/cloud-adoption-saas-trends-future-outlook-research-2026-03-11.md'
- '_bmad-output/planning-artifacts/research/domain-moroccan-tax-regulation-digital-compliance-research-2026-03-10.md'
- '_bmad-output/project-context.md'
- 'docs/index.md'
- 'docs/project-overview.md'
- 'docs/architecture.md'
- 'docs/development-guide.md'
- 'docs/source-tree-analysis.md'
workflowType: 'architecture'
project_name: "l'ami fiduciaire"
user_name: 'Saad'
date: '2026-03-11'
---
# Architecture Decision Document
_This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together._
## Project Context Analysis
### Requirements Overview
**Functional Requirements (58 FRs across 10 groups):**
The 58 FRs decompose into three architectural tiers:
| Tier | FR Groups | Architectural Impact |
|---|---|---|
| **Foundation** | Workspace & Onboarding (FR1-FR6), Team & Role Management (FR7-FR11), Client Management (FR12-FR15) | Multi-tenant data model, RBAC engine, session-based workspace resolution |
| **Core Domain** | Declaration Lifecycle (FR16-FR23), Dashboard & Visibility (FR24-FR28), Collaboration & Notifications (FR29-FR33), Client Portal & Document Exchange (FR34-FR40) | Declaration state machine, role-scoped queries, notification service, token-based portal |
| **Power Features** | Search/Filtering/Navigation (FR41-FR44), Archive System (FR45-FR51), Activity & Audit (FR52-FR55), Platform Administration (FR56-FR58) | Full-text search, archive storage strategy, audit trail, admin dashboard |
The Foundation tier is partially built (multi-tenant workspaces, client CRUD, basic folder system). Core Domain and Power Features are the MVP build scope.
**Non-Functional Requirements (28 NFRs across 4 categories):**
| Category | Key NFRs | Architectural Driver |
|---|---|---|
| **Performance** (NFR1-6) | 2s page loads, 10s bulk operations (50 items), 1s search, 3s dashboard render for 200 clients | Server-side rendering optimization, query performance, pagination strategy |
| **Security** (NFR7-15) | TLS 1.2+, AES-256 at rest, tenant isolation at every query, token security, 2FA, CNDP compliance, EU hosting | Encryption layer, middleware architecture, hosting infrastructure |
| **Scalability** (NFR16-20) | 200 concurrent workspaces, 1,000 users, 500 clients/workspace, 5,000 declarations/workspace, 1TB storage, 2-3x peak handling | Database indexing, query optimization, storage architecture, caching |
| **Reliability** (NFR21-28) | 99.5% uptime, 1hr RPO, 1hr RTO, >99% email delivery, zero data loss tolerance | Backup strategy, email service selection, monitoring, queue reliability |
**UX-Driven Architectural Requirements:**
- Role-driven sidebar and dashboard content (4 internal roles + client portal = 5 distinct view contexts)
- Inline table actions without page navigation (requires component-level interaction patterns)
- Filter persistence across views (session/URL state management)
- Bulk operations with real-time feedback (progress indication for 50+ item operations)
- Summary cards that filter tables below (client-side or server-side filter linkage)
- Server-side pagination as Inertia.js constraint (no infinite scroll)
- Mobile-first client portal alongside desktop-first internal app
### Scale & Complexity
- **Primary domain:** Full-stack web (Laravel 12 + Vue 3 monolith via Inertia.js 2)
- **Complexity level:** High
- **Estimated architectural components:** ~15-20 major components (auth, RBAC, tenant isolation, declaration engine, notification service, document storage, client portal, dashboard aggregation, archive system, search/filter, audit logging, admin panel, email delivery, file preview, bulk operations)
### Technical Constraints & Dependencies
**Brownfield Constraints (non-negotiable):**
| Constraint | Impact |
|---|---|
| Laravel 12 + PHP 8.2+ | Backend framework, ORM, routing, middleware patterns locked in |
| Vue 3 + TypeScript strict | Frontend framework, component patterns, type system |
| Inertia.js 2.0 | No REST API layer -- controllers render pages directly. Server-driven SPA. All URLs from PHP controllers. |
| shadcn-vue (reka-ui) + Tailwind CSS 4 | UI component library, styling approach. Cannot modify `components/ui/*` |
| MySQL 8.4 | Database engine. Session-based tenant resolution via `current_workspace_id` |
| Spatie Media Library | File upload/download infrastructure |
| Spatie Activity Log | Audit trail infrastructure |
| Laravel Fortify | Authentication + 2FA |
| bensampo/laravel-enum | Enum pattern for business domain values |
| Laravel Wayfinder | Type-safe frontend route generation |
| Pest 4 | Testing framework (PHP-only, no frontend JS tests) |
| Docker/Sail + GitHub Actions | Dev environment + CI/CD |
**Regulatory Constraints:**
| Regulation | Requirement |
|---|---|
| CNDP (Law 09-08) | EU-hosted infrastructure, data retention policies, user consent mechanisms |
| AML (Law 43-05) | Audit trails supporting firms' compliance obligations |
| Tax compliance context | Declaration types aligned with Moroccan fiscal calendar |
| 10-year retention | Archived data must be preserved for legal minimum |
**Infrastructure Constraints:**
| Constraint | Implication |
|---|---|
| Moroccan internet (ADSL/4G) | Page loads must work on slower connections -- server-side rendering, optimized asset delivery |
| Fiscal deadline peaks (15th-20th monthly, Jan-Mar annually) | System must handle 2-3x normal load without degradation |
| Email delivery during peaks | Mission-critical -- failed email = missed filing = financial penalty for client |
### Cross-Cutting Concerns Identified
1. **Tenant Isolation** -- Every database query, every file access, every API response must be scoped to the current workspace. This is not just a middleware concern -- it's a per-model, per-controller, per-query concern. Existing pattern: session-based `current_workspace_id` with `abort(404)` for violations.
2. **Role-Based Authorization** -- Three-tier access (Owner sees all, Manager sees all with configurable actions, Worker sees only assigned items). Affects every controller method, every Eloquent query scope, every Vue component's visible content. Current pattern: custom `authorizeXxx()` methods (no Gates/Policies).
3. **Audit Trail** -- Every data modification logged with actor, timestamp, and change details (FR52-NFR12). Already implemented via Spatie Activity Log trait. Must extend to all new models.
4. **Email Notification Reliability** -- 5 existing email types, more to come (nudge notifications, bulk scheduling). >99% delivery rate required. Queue-based sending with retry logic needed.
5. **File Security** -- Client financial documents (CIN numbers, bank statements, tax documents) require encryption at rest. Access limited to authorized workspace members and specific client via token link.
6. **Search & Filtering** -- Consistent filter bar across Clients, Declarations, Archive views with session persistence. Quick search across views. Affects frontend component architecture and backend query building.
7. **Bulk Operations** -- Creating/notifying/updating 50+ items at once. Must be performant (10s target), provide feedback, and handle partial failures gracefully. Queue-based processing likely needed.
## Starter Template Evaluation
### Primary Technology Domain
Full-stack web monolith (Laravel 12 + Vue 3 via Inertia.js 2.0) -- **brownfield project with established stack**.
### Starter Assessment: Brownfield Context
This project does not require a new starter template. The foundation is an active codebase with:
- **Backend:** Laravel 12, PHP 8.2+ (8.4 runtime), Eloquent ORM, 7 models, 16 migrations
- **Frontend:** Vue 3.5, TypeScript 5.2 strict, 31 page components, shadcn-vue design system (20+ component groups)
- **Bridge:** Inertia.js 2.0 (server-driven SPA -- no REST API layer)
- **Styling:** Tailwind CSS 4.1 with CVA, `cn()` helper
- **Auth:** Laravel Fortify (login, registration, password reset, email verification, 2FA/TOTP)
- **Files:** Spatie Media Library 11 (upload/download)
- **Logging:** Spatie Activity Log 4 (audit trail)
- **Enums:** bensampo/laravel-enum 6 (9 business enums)
- **Routes:** Laravel Wayfinder 0.1.9 (type-safe frontend routes)
- **Icons:** Lucide (lucide-vue-next)
- **Testing:** Pest 4 (PHP-only), 13 feature test files
- **Code Quality:** Laravel Pint, ESLint 9, Prettier 3 (with Tailwind plugin)
- **Build:** Vite 7, SSR enabled
- **Dev Environment:** Docker/Sail (MySQL 8.4, Mailpit, Soketi)
- **CI/CD:** GitHub Actions (lint + test workflows)
### Architectural Decisions Already Established
**Language & Runtime:**
- PHP 8.2+ with strict typing conventions, explicit `$fillable`, Form Request classes, PHPDoc generics on relationships
- TypeScript 5.2 strict mode, `isolatedModules: true`, `import type` for type-only imports, `@/` path alias
**Styling Solution:**
- Tailwind CSS 4.1 via `@tailwindcss/vite` plugin, shadcn-vue components (headless reka-ui primitives), CVA for variants, `cn()` utility
**Build Tooling:**
- Vite 7 with `laravel-vite-plugin` 2.0, SSR support enabled, concurrent dev server (PHP + Queue + Logs + Vite)
**Testing Framework:**
- Pest 4 with `RefreshDatabase` auto-applied, `test()` closures, `expect()` chaining, `route()` helper in tests
**Code Organization:**
- Pages: `resources/js/pages/{domain}/{Action}.vue`
- Components: `resources/js/components/` (app) + `components/ui/` (shadcn-vue, never modify)
- Types: `resources/js/types/` with barrel `index.ts`
- Composables: `resources/js/composables/`
- Controllers: `app/Http/Controllers/`
- Requests: `app/Http/Requests/`
- Models: `app/Models/`
- Enums: `app/Enums/`
- Mail: `app/Mail/`
**Development Experience:**
- `composer dev` runs all services concurrently (server + queue + logs + Vite HMR)
- `composer test` runs Pint lint check then Pest tests
- `npm run lint` + `npm run format` for JS/Vue code quality
- Docker/Sail for consistent local environment
### Stack Gaps for MVP (Requiring Architectural Decisions)
| Gap | Options to Evaluate | Decision Step |
|---|---|---|
| Permission toggle system | JSON column on pivot vs. permissions table | Step 4 |
| Search implementation | MySQL FULLTEXT vs. Laravel Scout + Meilisearch | Step 4 |
| In-app notifications | `DatabaseNotification` vs. custom notifications table | Step 4 |
| Bulk operation processing | Synchronous vs. queued jobs with progress broadcasting | Step 4 |
| Archive strategy | Soft state on declarations vs. separate archive table | Step 4 |
| Filter persistence | URL query params vs. session storage | Step 4 |
| Document preview | Client-side PDF.js vs. server-generated previews | Step 4 |
| Real-time features | Soketi (configured, not wired) -- scope of use for MVP | Step 4 |
**Note:** No new starter initialization needed. First implementation story should be the "Folders to Declarations" terminology migration (Pre-Phase from PRD).
## Core Architectural Decisions
### Decision Priority Analysis
**Critical Decisions (Block Implementation):**
- Permission toggle storage (JSON on pivot) -- blocks Phase 1 RBAC
- Archive strategy (status-based `archived_at`) -- blocks Phase 5 archive system
- Queue driver (Redis) -- blocks reliable email delivery and bulk operations
- Email service provider (Amazon SES EU) -- blocks notification reliability
**Important Decisions (Shape Architecture):**
- Search implementation (MySQL FULLTEXT) -- shapes Phase 4 search/filter
- In-app notifications (Laravel DatabaseNotification) -- shapes Phase 3 nudge system
- Filter persistence (URL query params) -- shapes Phase 4 filtering UX
- Caching strategy (Redis) -- shapes dashboard performance
- File storage (S3-compatible) -- shapes document management scalability
- Hosting provider (EU VPS + Laravel Forge) -- shapes deployment pipeline
**Deferred Decisions (Post-MVP):**
- Real-time WebSocket features (Soketi) -- not needed for MVP launch
- Full observability stack (Grafana/Prometheus) -- Laravel Pulse + Sentry sufficient for MVP
- Meilisearch integration -- MySQL FULLTEXT sufficient at initial scale
- Progress broadcasting for bulk operations -- synchronous sufficient for MVP batch sizes
### Data Architecture
**D1: Permission Toggle Storage**
- **Decision:** JSON column (`permissions`) on the `workspace_user` pivot table
- **Rationale:** 3 fixed roles with ~12 toggle-able permissions is a small, well-bounded set. JSON avoids extra JOINs on every permission check. Permissions are read frequently, written rarely (only when Owner configures Manager permissions).
- **Implementation:** Add `permissions` JSON column to `workspace_user` migration. Default permissions per role defined in a config file or enum. `WorkspaceUser` model casts `permissions` to array.
- **Affects:** FR3, FR4, FR7-FR11 (all role/permission features)
**D2: Archive Strategy**
- **Decision:** Status-based archiving with `archived_at` timestamp column on declarations table
- **Rationale:** Declarations stay in the same table, filtered out of active views via `whereNull('archived_at')` scope. Re-open is trivial (`archived_at = null`). At 5,000 declarations per workspace, a single indexed table performs well. 10-year retention is simply not deleting rows.
- **Implementation:** Add `archived_at` nullable timestamp to declarations migration. Add `scopeActive()` and `scopeArchived()` Eloquent scopes. Auto-set `archived_at` when status becomes "Closed."
- **Affects:** FR21-FR23, FR45-FR51 (entire archive system)
**D3: Search Implementation**
- **Decision:** MySQL FULLTEXT indexes for quick search across clients and declarations
- **Rationale:** No additional service dependency. At 200 workspaces with 500 clients / 5,000 declarations each, MySQL FULLTEXT delivers sub-second results. Workspace-scoped queries limit the search space further. Can migrate to Meilisearch post-MVP if search demands grow.
- **Implementation:** Add FULLTEXT indexes on searchable columns (client name, declaration type/notes, etc.). Use `MATCH...AGAINST` in natural language mode. Laravel Scout can be added later as an abstraction layer without changing the search API.
- **Affects:** FR43, NFR6 (quick search, 1-second response)
**D4: Caching Strategy**
- **Decision:** Redis for application caching
- **Rationale:** Triple-duty Redis (cache + queue + sessions) is the standard Laravel production pattern. Dashboard aggregation queries (counts by status, overdue items) benefit from short-lived cache. Redis is already supported by Laravel Sail.
- **Implementation:** Add Redis service to Docker Compose. Set `CACHE_DRIVER=redis`, `QUEUE_CONNECTION=redis`, `SESSION_DRIVER=redis` in environment. Use `Cache::remember()` for dashboard aggregations with 5-minute TTL. Invalidate on status changes.
- **Affects:** NFR1, NFR5, NFR16-20 (performance, scalability)
### Authentication & Security
**D5: Queue Driver**
- **Decision:** Redis queue driver
- **Rationale:** Already adding Redis for cache/sessions. Redis queues are fast, support delayed jobs, retries (up to 3 within 5 minutes per NFR26), and job batching. Native Laravel support.
- **Implementation:** Set `QUEUE_CONNECTION=redis`. Email mailables dispatched via `dispatch()` or `Mail::queue()`. Failed jobs table for monitoring. `queue:work` process managed by Supervisor in production.
- **Affects:** NFR26 (email delivery reliability), FR32-FR33 (notification scheduling)
**D6: File Encryption at Rest**
- **Decision:** Storage-level encryption (provider-managed AES-256 SSE)
- **Rationale:** Meets NFR7 (AES-256 at rest) transparently without breaking Spatie Media Library or adding application-level encryption complexity. Encryption/decryption is handled by the storage provider with zero performance overhead for the application.
- **Implementation:** Enable SSE (Server-Side Encryption) on the S3-compatible bucket. All objects encrypted automatically. No application code changes needed.
- **Affects:** NFR7 (encryption at rest), NFR14 (document access control)
**D7: Session Storage**
- **Decision:** Redis sessions
- **Rationale:** Consistent with Redis for cache and queue (triple-duty). Fast session reads, supports horizontal scaling if needed later. Session-based workspace resolution (`current_workspace_id`) benefits from Redis speed.
- **Implementation:** Set `SESSION_DRIVER=redis`. No application code changes -- Laravel handles session management transparently.
- **Affects:** Multi-tenant workspace resolution (session-based `current_workspace_id`)
### Communication Patterns
**D8: In-App Notification System**
- **Decision:** Laravel's built-in `DatabaseNotification` system
- **Rationale:** Battle-tested, supports database + mail channels, read/unread tracking built-in, `Notifiable` trait already on User model via Fortify. Workspace scoping added via query constraints when fetching notifications.
- **Implementation:** Run `php artisan notifications:table` migration. Create notification classes (e.g., `NudgeNotification`, `DeclarationOverdueNotification`). Query notifications with workspace scope via the related declaration/client. Mark as read via `markAsRead()`.
- **Affects:** FR29-FR31 (nudge system, notification center)
**D9: Bulk Operation Processing**
- **Decision:** Synchronous processing for bulk creation/updates; individual email sends queued via Redis
- **Rationale:** 50 DB inserts complete in <1 second synchronously. Email notifications for those 50 items are dispatched to Redis queue individually, processing asynchronously. This meets the 10-second NFR target without the complexity of progress broadcasting. Controller returns immediately after DB operations; emails process in background.
- **Implementation:** Bulk creation: `Declaration::insert($records)` or loop with `create()` for model events. Each notification dispatched to queue: `Notification::send($users, new BulkNotification())`. Failed individual sends retry automatically via Redis queue.
- **Affects:** FR17, FR32 (bulk declaration creation, bulk notification scheduling), NFR2-3 (10-second target)
**D10: Real-Time Features (Soketi)**
- **Decision:** Deferred for MVP -- no WebSocket wiring
- **Rationale:** Inertia.js pages refresh data on every navigation, which is sufficient for MVP. The nudge notification center loads on page visit. Soketi remains configured in Docker for post-MVP activation. First post-MVP use case: real-time notification badge updates.
- **Implementation:** No implementation needed for MVP. Soketi stays in Docker Compose for future use.
- **Affects:** Post-MVP enhancement path
### Frontend Architecture
**D11: Filter Persistence**
- **Decision:** URL query parameters
- **Rationale:** Native Inertia.js pattern -- `router.get(url, { status: 'en_cours', assignee: 'me' })`. Filters survive page refresh, browser back/forward, and are shareable/bookmarkable. No server-side session management needed. Consistent with how Inertia handles pagination.
- **Implementation:** Filter components emit changes as URL params via `router.get()`. Controllers read filters from `Request` and apply Eloquent scopes. Default filters (e.g., Worker's "assignee = me") applied when no params present.
- **Affects:** FR41-FR44 (filtering, persistence, search)
**D12: Document Preview**
- **Decision:** Client-side viewers (PDF.js for PDFs, native `<img>` for images)
- **Rationale:** No server processing overhead. PDF.js is mature and handles most PDF files. Images render natively. A shadcn-vue Dialog component wraps the viewer for a modal experience. File is fetched via existing Spatie Media download route.
- **Implementation:** Vue component with `<iframe>` or PDF.js viewer for PDFs, `<img>` for images. Triggered from archive detail page or declaration detail. Falls back to download for unsupported file types.
- **Affects:** FR48 (in-app document preview)
### Infrastructure & Deployment
**D13: Email Service Provider**
- **Decision:** Amazon SES (EU region -- eu-west-1 Ireland)
- **Rationale:** High deliverability (>99%), cost-effective ($0.10/1000 emails), EU region for CNDP compliance, native Laravel SES driver (`aws/aws-sdk-php`). At 100-150 workspaces with ~5-10 emails per client per month, monthly email volume is well within SES free/low-cost tiers.
- **Implementation:** Install `aws/aws-sdk-php`. Set `MAIL_MAILER=ses` with EU region configuration. Configure SPF, DKIM, DMARC for sending domain. Monitor delivery via SES dashboard and CloudWatch.
- **Affects:** NFR26 (>99% email delivery), FR33 (email notifications), NFR13 (CNDP -- EU data processing)
**D14: Hosting Provider**
- **Decision:** EU-based VPS (Hetzner Cloud or DigitalOcean EU) managed via Laravel Forge
- **Rationale:** Cost-effective for MVP scale (100-150 workspaces). EU data centers for CNDP compliance. Laravel Forge handles server provisioning, deployment, SSL, queue workers, and scheduled tasks -- reducing DevOps overhead for a 2-person team. Can scale vertically (bigger VPS) or horizontally (add servers) as needed.
- **Implementation:** Provision VPS via Laravel Forge. Configure Nginx, PHP-FPM, MySQL, Redis, Supervisor (queue workers). Deploy via Forge's GitHub integration (push to deploy). SSL via Let's Encrypt.
- **Affects:** NFR13 (CNDP -- EU hosting), NFR21 (99.5% uptime), NFR22-25 (backup strategy)
**D15: File Storage**
- **Decision:** S3-compatible object storage with AES-256 SSE
- **Rationale:** Scalable to 1TB+ (NFR19), encryption at rest (NFR7), works natively with Spatie Media Library's S3 disk driver. Provider options: AWS S3 (eu-west-1), Hetzner Object Storage, DigitalOcean Spaces, or Cloudflare R2 -- chosen based on hosting provider for network proximity.
- **Implementation:** Configure `filesystems.php` S3 disk with EU endpoint. Set Spatie Media Library to use S3 disk. Enable SSE on bucket. Backup policy: bucket versioning enabled.
- **Affects:** NFR7 (encryption at rest), NFR19 (1TB storage), NFR28 (zero data loss)
**D16: Monitoring & Alerting**
- **Decision:** Laravel Pulse (application monitoring) + Sentry (error tracking/alerting)
- **Rationale:** Laravel Pulse is zero-config for Laravel apps -- provides request performance, queue health, and slow query monitoring out of the box. Sentry catches exceptions with full stack traces, alerting via email/Slack. Both have generous free tiers. Post-MVP: add uptime monitoring (Oh Dear or BetterStack).
- **Implementation:** Install `laravel/pulse` and configure dashboard (admin-only route). Install Sentry PHP SDK and configure DSN. Set up alert rules for error spikes and queue failures.
- **Affects:** NFR21 (99.5% uptime), NFR27 (monitoring and alerting)
### Decision Impact Analysis
**Implementation Sequence:**
1. **Redis integration** (D4/D5/D7) -- foundational service, add to Docker Compose first
2. **Permission toggle migration** (D1) -- blocks Phase 1 RBAC
3. **Archive column migration** (D2) -- blocks Phase 5, but can be added early
4. **FULLTEXT indexes** (D3) -- blocks Phase 4 search
5. **Notification system setup** (D8) -- blocks Phase 3 nudge system
6. **S3 storage configuration** (D15) -- can run parallel to feature development
7. **Email service configuration** (D13) -- needed before production launch
8. **Hosting provisioning** (D14) -- needed before production launch
9. **Monitoring setup** (D16) -- needed before production launch
10. **Document preview component** (D12) -- needed for Phase 5
**Cross-Component Dependencies:**
- Redis (D4/D5/D7) must be configured before queue-dependent features (bulk email, notifications)
- S3 storage (D15) + SSE encryption (D6) must be configured before production file uploads
- SES (D13) must be verified and configured before any email-dependent features go to production
- Permission system (D1) is the foundation for all role-based dashboard and feature access (D8, D9, D11)
## Implementation Patterns & Consistency Rules
### Pattern Categories Defined
**Critical Conflict Points Identified:** 12 areas where AI agents could make different choices when implementing MVP features. The existing project-context.md covers general coding conventions. These patterns cover the **domain-specific implementation decisions** for RBAC, declarations, notifications, bulk operations, archive, search, and filtering.
### Workspace & Tenant Scoping Patterns
**Every model query MUST be workspace-scoped. No exceptions.**
Pattern: Use Eloquent global scopes or explicit `where('workspace_id', $workspaceId)` on every query that touches workspace-owned data.
```php
// CORRECT: Workspace-scoped query
$clients = Client::where('workspace_id', $workspace->id)
->where('status', ClientStatus::Active)
->get();
// CORRECT: Via relationship (inherits workspace scope)
$declarations = $client->declarations()->active()->get();
// WRONG: Unscoped query -- potential data leak
$clients = Client::where('status', ClientStatus::Active)->get();
```
**Workspace resolution:** Always from session helper, never from request params:
```php
// CORRECT
$workspace = auth()->user()->currentWorkspace();
$workspaceId = session('current_workspace_id');
// WRONG
$workspace = Workspace::find($request->workspace_id);
```
### Permission Checking Patterns
**Pattern: Custom `authorize` methods in controllers using the JSON permissions column.**
```php
// In controller base or trait:
protected function authorizePermission(string $permission): void
{
$workspaceUser = auth()->user()->currentWorkspaceUser();
if ($workspaceUser->role === WorkspaceUserRole::Owner) {
return; // Owners can do everything
}
if ($workspaceUser->role === WorkspaceUserRole::Worker) {
abort(404); // Workers never have configurable permissions
}
// Manager: check JSON permissions column
if (!($workspaceUser->permissions[$permission] ?? false)) {
abort(404); // 404 not 403 -- per project convention
}
}
```
**Permission keys follow snake_case:** `can_manage_team`, `can_view_activity_logs`, `can_configure_portal`.
**Default permissions per role defined in a config file:**
```php
// config/permissions.php
return [
'defaults' => [
'owner' => ['*'], // all permissions
'manager' => [
'can_manage_team' => false,
'can_view_activity_logs' => true,
'can_configure_portal' => false,
],
'worker' => [], // no configurable permissions
],
];
```
### Role-Scoped Query Patterns
**Workers see only their assigned items. Owners/Managers see everything in the workspace.**
```php
// Pattern: scopeForUser() on Declaration model
public function scopeForUser(Builder $query, User $user, Workspace $workspace): Builder
{
$role = $user->roleIn($workspace);
if ($role === WorkspaceUserRole::Worker) {
return $query->where('assigned_to', $user->id);
}
// Owner and Manager see all workspace declarations
return $query;
}
// Usage in controller:
$declarations = Declaration::where('workspace_id', $workspace->id)
->forUser(auth()->user(), $workspace)
->active()
->get();
```
**This pattern applies to:** Declarations, Clients (via assigned declarations), Archive items, Activity logs.
### Declaration Status Flow
**Valid status transitions must be enforced. Not every status can transition to every other.**
```
Created → En cours → En attente client → En cours → Terminé → Fermé → [auto-archive]
↘ ↗ ↗
Mise en demeure ──────┘ ───────────┘
Created → En cours → Terminé → Fermé → [auto-archive]
```
**Status enum values and their meanings:**
| Status | Meaning | Who Can Set | Next Valid Statuses |
|---|---|---|---|
| `created` | Declaration just created | System | `en_cours` |
| `en_cours` | Being worked on | Owner/Manager/Worker | `en_attente_client`, `termine` |
| `en_attente_client` | Waiting for client documents | Owner/Manager/Worker | `en_cours`, `mise_en_demeure` |
| `mise_en_demeure` | Formal notice sent to client | Owner/Manager | `en_cours`, `ferme` |
| `termine` | Work completed | Owner/Manager/Worker | `ferme` |
| `ferme` | Closed (triggers auto-archive) | Owner/Manager | (archived) |
**Auto-archive trigger:** When status becomes `ferme`, set `archived_at = now()`.
**Re-open from archive:** Only Owner/Manager. Sets `archived_at = null`, status back to `en_cours`. Audit log entry required with reason.
### Notification Patterns
**All notifications use Laravel's notification system with database + mail channels.**
```php
// Notification class naming: {Action}Notification
class NudgeNotification extends Notification implements ShouldQueue
{
use Queueable;
public function via($notifiable): array
{
return ['database', 'mail'];
}
public function toArray($notifiable): array
{
return [
'type' => 'nudge',
'declaration_id' => $this->declaration->id,
'workspace_id' => $this->declaration->workspace_id,
'sender_id' => $this->sender->id,
'message' => $this->message,
];
}
}
```
**Notification payload always includes `workspace_id`** -- enables workspace-scoped notification queries.
**Notification types follow snake_case:** `nudge`, `declaration_overdue`, `document_uploaded`, `bulk_notification`.
### Bulk Operation Patterns
**Bulk creation: synchronous DB operations, queued notifications.**
```php
// Pattern for bulk declaration creation
public function bulkStore(BulkStoreDeclarationRequest $request)
{
$declarations = [];
DB::transaction(function () use ($request, &$declarations) {
foreach ($request->validated()['items'] as $item) {
$declarations[] = Declaration::create([
'workspace_id' => $workspace->id,
'client_id' => $item['client_id'],
'type' => $item['type'],
'due_date' => $item['due_date'],
'assigned_to' => $item['assigned_to'],
'status' => DeclarationStatus::Created,
]);
}
});
// Queue notifications separately (non-blocking)
if ($request->validated()['notify_clients']) {
foreach ($declarations as $declaration) {
$declaration->client->notify(new DeclarationCreatedNotification($declaration));
}
}
return redirect()->back()->with('success', count($declarations) . ' declarations created.');
}
```
**Bulk operations always wrapped in `DB::transaction()`** -- all-or-nothing.
### Filter & Search Patterns
**Filters via URL query params. Controllers read from Request, apply Eloquent scopes.**
```php
// Pattern: Reusable filter handling in controller
protected function applyFilters(Builder $query, Request $request): Builder
{
return $query
->when($request->status, fn ($q, $status) => $q->where('status', $status))
->when($request->assignee, fn ($q, $assignee) => $q->where('assigned_to', $assignee))
->when($request->client_id, fn ($q, $clientId) => $q->where('client_id', $clientId))
->when($request->type, fn ($q, $type) => $q->where('type', $type))
->when($request->search, fn ($q, $search) =>
$q->whereRaw('MATCH(searchable_columns) AGAINST(? IN NATURAL LANGUAGE MODE)', [$search])
)
->when($request->sort, fn ($q, $sort) =>
$q->orderBy($sort, $request->input('direction', 'asc'))
, fn ($q) => $q->orderBy('due_date', 'asc')); // default sort
}
```
**Frontend: Filters passed as props, emitted as URL params:**
```typescript
// Pattern: Filter component emits changes via Inertia router
function applyFilter(key: string, value: string | null) {
router.get(props.indexUrl, {
...currentFilters,
[key]: value,
page: 1, // reset pagination on filter change
}, {
preserveState: true,
preserveScroll: true,
});
}
```
**Default sort: `due_date ASC`** (most urgent first) for declarations. **`name ASC`** for clients.
### Archive Query Patterns
**Active queries exclude archived items by default. Archive queries include only archived items.**
```php
// Model scopes on Declaration:
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('archived_at');
}
public function scopeArchived(Builder $query): Builder
{
return $query->whereNotNull('archived_at');
}
// Controllers ALWAYS use one or the other:
// DeclarationController: Declaration::active()->forUser()->get()
// ArchiveController: Declaration::archived()->forUser()->get()
```
**Archive detail pages are read-only.** No edit actions, no status changes. Only re-open (Owner/Manager).
### Dashboard Aggregation Patterns
**Dashboard queries use cached aggregation with 5-minute TTL.**
```php
// Pattern: Dashboard data with cache
$dashboardData = Cache::remember(
"dashboard:{$workspace->id}:{$user->id}",
300, // 5 minutes
fn () => [
'total_active' => Declaration::where('workspace_id', $workspace->id)->active()->forUser($user, $workspace)->count(),
'by_status' => Declaration::where('workspace_id', $workspace->id)->active()->forUser($user, $workspace)
->selectRaw('status, count(*) as count')
->groupBy('status')
->pluck('count', 'status'),
'overdue' => Declaration::where('workspace_id', $workspace->id)->active()->forUser($user, $workspace)
->where('due_date', '<', now())
->count(),
'due_this_week' => Declaration::where('workspace_id', $workspace->id)->active()->forUser($user, $workspace)
->whereBetween('due_date', [now(), now()->endOfWeek()])
->count(),
]
);
```
**Cache invalidation:** Bust cache on declaration status change, creation, or deletion:
```php
Cache::forget("dashboard:{$workspace->id}:*"); // pattern-based invalidation
```
### Error Handling Patterns
**Consistent error handling across controllers:**
| Scenario | Response | Code |
|---|---|---|
| Workspace boundary violation | `abort(404)` | 404 |
| Permission denied | `abort(404)` (intentional -- don't reveal existence) | 404 |
| Validation error | Inertia automatic redirect with errors | 422 |
| Resource not found | `abort(404)` | 404 |
| Server error | Sentry captures, generic error page | 500 |
**User-facing flash messages follow this format:**
```php
return redirect()->back()->with('success', 'Declaration created successfully.');
return redirect()->back()->with('error', 'Unable to send notification. Please try again.');
```
**Flash message types:** `success`, `error`, `warning`, `info`.
### Frontend Component Patterns for New Features
**Summary cards above tables:**
```vue
<!-- Pattern: Clickable summary cards that filter the table -->
<SummaryCards
:items="[
{ label: 'En cours', count: stats.en_cours, filter: { status: 'en_cours' }, color: 'blue' },
{ label: 'En retard', count: stats.overdue, filter: { overdue: true }, color: 'red' },
{ label: 'En attente', count: stats.en_attente, filter: { status: 'en_attente_client' }, color: 'amber' },
]"
@filter="applyFilter"
/>
```
**Inline row actions pattern:**
```vue
<!-- Pattern: Actions available at the row level -->
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon"><MoreHorizontal /></Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem @click="router.get(row.showUrl)">View</DropdownMenuItem>
<DropdownMenuItem @click="openNudgeDialog(row)">Nudge</DropdownMenuItem>
<DropdownMenuItem @click="router.get(row.editUrl)">Edit</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
```
**Nudge dialog: Popover, not full modal.** Quick action = minimal UI.
### Enforcement Guidelines
**All AI Agents MUST:**
1. Read `project-context.md` AND this architecture document before implementing any feature
2. Scope every database query to `workspace_id` -- no unscoped queries on workspace-owned models
3. Use `abort(404)` for all authorization failures -- never `abort(403)`
4. Use the role-scoped query pattern (`forUser()` scope) on all declaration/client queries
5. Wrap bulk operations in `DB::transaction()`
6. Use Laravel's notification system for all in-app + email notifications
7. Pass filters via URL query params, not session storage
8. Use `Cache::remember()` for dashboard aggregations with cache invalidation on data changes
9. Follow the declaration status flow -- no arbitrary status transitions
10. Include `workspace_id` in all notification payloads
**Anti-Patterns to Avoid:**
- Querying declarations without workspace scope
- Using `abort(403)` instead of `abort(404)` for permission failures
- Creating custom notification tables instead of using Laravel's `DatabaseNotification`
- Storing filters in session instead of URL params
- Skipping `DB::transaction()` on bulk operations
- Hardcoding dashboard counts instead of using cached aggregations
- Allowing invalid status transitions on declarations
## Project Structure & Boundaries
### Complete Project Directory Structure
The existing project structure is documented in `docs/source-tree-analysis.md`. Below shows the **complete MVP structure** with new additions marked with `← NEW`.
```
l'ami fiduciaire/
├── app/
│ ├── Actions/
│ │ └── Fortify/ # Auth actions (existing)
│ ├── Concerns/ # Shared traits
│ │ ├── PasswordValidationRules.php
│ │ ├── ProfileValidationRules.php
│ │ ├── HasWorkspaceScope.php ← NEW (workspace scoping trait for controllers)
│ │ └── AuthorizesPermissions.php ← NEW (permission checking trait)
│ ├── Enums/
│ │ ├── ClientStatus.php
│ │ ├── DeclarationStatus.php ← NEW (replaces FolderStatus)
│ │ ├── DeclarationType.php ← NEW (replaces FolderType)
│ │ ├── DeclarationPriority.php ← NEW (replaces FolderPriority)
│ │ ├── LegalForm.php
│ │ ├── MessageType.php
│ │ ├── NotificationType.php ← NEW (nudge, overdue, document_uploaded, etc.)
│ │ ├── Permission.php ← NEW (enum of permission keys)
│ │ └── WorkspaceUserRole.php
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Client/ # Public client portal (existing)
│ │ │ │ ├── UploadController.php
│ │ │ │ ├── ConfirmController.php
│ │ │ │ └── RefuseController.php
│ │ │ ├── Settings/ # User settings (existing)
│ │ │ ├── Admin/ ← NEW (SaaS admin controllers)
│ │ │ │ ├── AdminDashboardController.php ← NEW
│ │ │ │ ├── WorkspaceController.php ← MOVED from root
│ │ │ │ └── UserController.php ← MOVED from root
│ │ │ ├── ClientController.php # (existing, extended)
│ │ │ ├── DashboardController.php # (existing, rewritten for Phase 2)
│ │ │ ├── DeclarationController.php ← NEW (replaces FolderController)
│ │ │ ├── DeclarationMediaController.php ← NEW (replaces FolderMediaController)
│ │ │ ├── ArchiveController.php ← NEW (Phase 5)
│ │ │ ├── NotificationController.php # (existing, extended for Phase 3)
│ │ │ ├── TeamController.php ← NEW (Phase 1 team management)
│ │ │ ├── BulkDeclarationController.php ← NEW (Phase 4)
│ │ │ ├── NudgeController.php ← NEW (Phase 3)
│ │ │ └── WorkspaceSwitchController.php # (existing)
│ │ ├── Middleware/
│ │ │ ├── EnsureUserHasWorkspace.php
│ │ │ ├── EnsureUserIsAdmin.php
│ │ │ ├── HandleAppearance.php
│ │ │ ├── HandleInertiaRequests.php
│ │ │ └── ValidateClientPortalToken.php ← NEW (replaces ValidateFolderInvitation)
│ │ └── Requests/
│ │ ├── Settings/
│ │ ├── StoreClientRequest.php
│ │ ├── UpdateClientRequest.php
│ │ ├── StoreDeclarationRequest.php ← NEW
│ │ ├── UpdateDeclarationRequest.php ← NEW
│ │ ├── BulkStoreDeclarationRequest.php ← NEW (Phase 4)
│ │ ├── StoreNudgeRequest.php ← NEW (Phase 3)
│ │ ├── UpdateTeamMemberRequest.php ← NEW (Phase 1)
│ │ ├── InviteTeamMemberRequest.php ← NEW (Phase 1)
│ │ └── UpdatePermissionsRequest.php ← NEW (Phase 1)
│ ├── Mail/
│ │ ├── DeclarationConfirmationMail.php ← NEW (replaces FolderConfirmationMail)
│ │ ├── DeclarationFileRequestMail.php ← NEW (replaces FolderFileRequestMail)
│ │ ├── DeclarationInviteMail.php ← NEW (replaces FolderInviteMail)
│ │ ├── DeclarationSituationMail.php ← NEW (replaces FolderSituationMail)
│ │ ├── DeclarationTextMessageMail.php ← NEW (replaces FolderTextMessageMail)
│ │ └── NudgeNotificationMail.php ← NEW (Phase 3)
│ ├── Models/
│ │ ├── Client.php # (existing, extended with search scope)
│ │ ├── Declaration.php ← NEW (replaces Folder)
│ │ ├── DeclarationInvitation.php ← NEW (replaces FolderInvitation)
│ │ ├── Message.php
│ │ ├── User.php # (existing, add Notifiable extensions)
│ │ ├── Workspace.php
│ │ └── WorkspaceUser.php # (existing, add permissions JSON cast)
│ ├── Notifications/ ← NEW (Phase 3)
│ │ ├── NudgeNotification.php ← NEW
│ │ ├── DeclarationOverdueNotification.php ← NEW
│ │ ├── DocumentUploadedNotification.php ← NEW
│ │ └── BulkNotification.php ← NEW (Phase 4)
│ ├── Observers/ ← NEW
│ │ └── DeclarationObserver.php ← NEW (status transitions, auto-archive, cache bust)
│ └── Providers/
├── config/
│ ├── permissions.php ← NEW (role default permissions)
│ └── ... # Standard Laravel config
├── database/
│ ├── factories/
│ │ ├── ClientFactory.php # (existing)
│ │ ├── DeclarationFactory.php ← NEW (replaces FolderFactory)
│ │ └── WorkspaceUserFactory.php # (existing, extended)
│ ├── migrations/
│ │ ├── ... # Existing 16 migrations
│ │ ├── xxxx_rename_folders_to_declarations.php ← NEW (Pre-Phase)
│ │ ├── xxxx_add_permissions_to_workspace_user.php ← NEW (Phase 1)
│ │ ├── xxxx_add_archived_at_to_declarations.php ← NEW (Phase 5)
│ │ ├── xxxx_add_fulltext_indexes.php ← NEW (Phase 4)
│ │ ├── xxxx_create_notifications_table.php ← NEW (Phase 3)
│ │ └── xxxx_add_searchable_columns.php ← NEW (Phase 4)
│ └── seeders/
│ ├── DatabaseSeeder.php
│ └── PermissionSeeder.php ← NEW (default permission sets)
├── resources/
│ ├── css/
│ ├── js/
│ │ ├── app.ts
│ │ ├── ssr.ts
│ │ ├── components/
│ │ │ ├── ui/ # shadcn-vue (NEVER modify)
│ │ │ ├── app/ ← NEW (app shell components)
│ │ │ │ ├── AppSidebar.vue ← NEW (role-driven sidebar, Phase 2)
│ │ │ │ ├── NotificationBell.vue ← NEW (Phase 3)
│ │ │ │ └── WorkspaceSwitcher.vue # (existing)
│ │ │ ├── declarations/ ← NEW (replaces folders/)
│ │ │ │ ├── DeclarationForm.vue ← NEW
│ │ │ │ ├── DeclarationStatusBadge.vue ← NEW
│ │ │ │ ├── DeclarationRowActions.vue ← NEW
│ │ │ │ └── BulkCreateForm.vue ← NEW (Phase 4)
│ │ │ ├── clients/ # (existing, extended)
│ │ │ │ ├── ClientForm.vue
│ │ │ │ └── ClientStatusBadge.vue ← NEW
│ │ │ ├── dashboard/ ← NEW (Phase 2)
│ │ │ │ ├── SummaryCards.vue ← NEW
│ │ │ │ ├── OwnerDashboard.vue ← NEW
│ │ │ │ ├── WorkerDashboard.vue ← NEW
│ │ │ │ └── OverdueAlerts.vue ← NEW
│ │ │ ├── notifications/ ← NEW (Phase 3)
│ │ │ │ ├── NotificationList.vue ← NEW
│ │ │ │ ├── NudgePopover.vue ← NEW
│ │ │ │ └── NotificationItem.vue ← NEW
│ │ │ ├── archive/ ← NEW (Phase 5)
│ │ │ │ ├── ArchiveDetailView.vue ← NEW
│ │ │ │ └── DocumentPreview.vue ← NEW (PDF.js viewer)
│ │ │ ├── filters/ ← NEW (Phase 4)
│ │ │ │ ├── FilterBar.vue ← NEW
│ │ │ │ ├── FilterSelect.vue ← NEW
│ │ │ │ ├── SearchInput.vue ← NEW
│ │ │ │ └── ActiveFilters.vue ← NEW
│ │ │ ├── team/ ← NEW (Phase 1)
│ │ │ │ ├── TeamMemberRow.vue ← NEW
│ │ │ │ ├── InviteMemberForm.vue ← NEW
│ │ │ │ └── PermissionToggles.vue ← NEW
│ │ │ ├── Pagination.vue # (existing)
│ │ │ └── DataTable.vue ← NEW (reusable table with sort/filter)
│ │ ├── composables/
│ │ │ ├── useAppearance.ts
│ │ │ ├── useCurrentUrl.ts
│ │ │ ├── useInitials.ts
│ │ │ ├── useTwoFactorAuth.ts
│ │ │ ├── useFilters.ts ← NEW (Phase 4, URL param filter management)
│ │ │ ├── usePermissions.ts ← NEW (Phase 1, check user permissions)
│ │ │ └── useBulkSelection.ts ← NEW (Phase 4, table row selection)
│ │ ├── layouts/
│ │ │ ├── app/ # App layouts (existing, modified for role sidebar)
│ │ │ ├── auth/
│ │ │ ├── settings/
│ │ │ ├── AppLayout.vue
│ │ │ └── AuthLayout.vue
│ │ ├── lib/
│ │ │ └── utils.ts # (existing)
│ │ ├── pages/
│ │ │ ├── auth/ # Auth pages (existing)
│ │ │ ├── client/ # Public client portal (existing)
│ │ │ ├── clients/ # Client management (existing)
│ │ │ │ ├── Index.vue # (extended with filters)
│ │ │ │ ├── Show.vue
│ │ │ │ ├── Create.vue
│ │ │ │ └── Edit.vue
│ │ │ ├── declarations/ ← NEW (replaces folders/)
│ │ │ │ ├── Index.vue ← NEW
│ │ │ │ ├── Show.vue ← NEW
│ │ │ │ ├── Create.vue ← NEW
│ │ │ │ ├── Edit.vue ← NEW
│ │ │ │ └── BulkCreate.vue ← NEW (Phase 4)
│ │ │ ├── archive/ ← NEW (Phase 5)
│ │ │ │ ├── Index.vue ← NEW
│ │ │ │ └── Show.vue ← NEW (read-only detail)
│ │ │ ├── team/ ← NEW (Phase 1)
│ │ │ │ ├── Index.vue ← NEW
│ │ │ │ └── Permissions.vue ← NEW
│ │ │ ├── notifications/ ← NEW (Phase 3)
│ │ │ │ └── Index.vue ← NEW
│ │ │ ├── admin/ ← NEW (SaaS admin)
│ │ │ │ ├── Dashboard.vue ← NEW
│ │ │ │ ├── workspaces/ ← MOVED
│ │ │ │ └── users/ ← MOVED
│ │ │ ├── settings/ # (existing)
│ │ │ ├── Dashboard.vue # (existing, rewritten Phase 2)
│ │ │ └── Welcome.vue
│ │ └── types/
│ │ ├── index.ts # (extended with new types)
│ │ ├── declaration.ts ← NEW
│ │ ├── notification.ts ← NEW
│ │ ├── team.ts ← NEW
│ │ ├── archive.ts ← NEW
│ │ ├── filters.ts ← NEW
│ │ └── dashboard.ts ← NEW
│ └── views/
│ ├── app.blade.php
│ └── emails/
│ ├── declarations/ ← NEW (replaces folder email templates)
│ └── nudge.blade.php ← NEW
├── routes/
│ ├── web.php # (extended with new routes)
│ ├── settings.php
│ └── console.php
├── tests/
│ ├── Feature/
│ │ ├── Auth/ # (existing)
│ │ ├── Settings/ # (existing)
│ │ ├── Declarations/ ← NEW
│ │ │ ├── CreateDeclarationTest.php ← NEW
│ │ │ ├── UpdateDeclarationTest.php ← NEW
│ │ │ ├── StatusTransitionTest.php ← NEW
│ │ │ ├── BulkCreateTest.php ← NEW
│ │ │ └── ArchiveTest.php ← NEW
│ │ ├── Team/ ← NEW
│ │ │ ├── ManageTeamTest.php ← NEW
│ │ │ ├── PermissionsTest.php ← NEW
│ │ │ └── RoleScopingTest.php ← NEW
│ │ ├── Notifications/ ← NEW
│ │ │ ├── NudgeTest.php ← NEW
│ │ │ └── NotificationCenterTest.php ← NEW
│ │ ├── Filters/ ← NEW
│ │ │ └── FilterPersistenceTest.php ← NEW
│ │ ├── Dashboard/ ← NEW
│ │ │ ├── OwnerDashboardTest.php ← NEW
│ │ │ └── WorkerDashboardTest.php ← NEW
│ │ ├── DashboardTest.php # (existing)
│ │ └── WorkspaceScopingTest.php ← NEW (cross-cutting)
│ └── Unit/
│ ├── DeclarationStatusTest.php ← NEW (state machine unit tests)
│ └── PermissionCheckTest.php ← NEW
├── .github/workflows/
│ ├── lint.yml # (existing)
│ └── tests.yml # (existing)
├── compose.yaml # (add Redis service)
├── composer.json
├── package.json
├── vite.config.ts
├── tsconfig.json
└── AGENTS.md
```
### Architectural Boundaries
**Layer Boundaries (Request Flow):**
```
Browser → Inertia Request
→ Middleware (auth, workspace, appearance)
→ Controller (authorize, validate, query, render)
→ Model (Eloquent scopes, relationships, casts)
→ Database (MySQL, workspace-scoped)
→ Notification (database + mail channels)
→ Queue (Redis)
→ Cache (Redis)
→ Inertia Response (page component + props)
→ Vue Page (layout + components + composables)
```
**Controller Boundary:**
Controllers are the ONLY layer that orchestrates business logic. No service classes for MVP -- keep logic in controllers with extracted traits for shared patterns (`HasWorkspaceScope`, `AuthorizesPermissions`).
**API Boundaries:**
| Boundary | Pattern | Notes |
|---|---|---|
| **Inertia pages** | `Controller → Inertia::render()` | No REST API. All data via Inertia props. |
| **Client portal** | Token-based public routes | Isolated from authenticated routes. No session. |
| **Admin panel** | `EnsureUserIsAdmin` middleware group | Separate controller namespace `Admin/` |
| **Notifications** | Laravel notification channels | `database` + `mail` channels via queue |
| **File access** | Spatie Media Library routes | Authenticated download via signed URLs |
**Data Boundaries:**
| Model | Workspace-Scoped | Role-Scoped | Cacheable |
|---|---|---|---|
| Client | Yes (`workspace_id`) | No (all roles see all clients) | No |
| Declaration | Yes (`workspace_id`) | Yes (`forUser()` scope) | Dashboard counts only |
| WorkspaceUser | Yes (pivot on workspace) | No | No |
| Notification | Via declaration's `workspace_id` | Yes (per-user) | No |
| Message | Via declaration relationship | Via declaration scope | No |
**Component Boundaries (Frontend):**
| Boundary | Communication | Data Source |
|---|---|---|
| Page → Layout | Slots, layout props | Inertia shared data |
| Page → Feature Component | Props down, events up | Page-level Inertia props |
| Feature Component → UI Component | Props only | Parent component |
| Filter Component → Page | `router.get()` URL params | URL query string |
| Summary Card → Table | `@filter` event → `applyFilter()` | Shared filter state |
### Requirements to Structure Mapping
**Pre-Phase: Terminology Migration**
- Rename: `Folder``Declaration` across all models, controllers, enums, pages, tests, migrations
- Files affected: ~40 files (models, controllers, requests, pages, components, enums, mail, tests)
- New migration: `xxxx_rename_folders_to_declarations.php`
**Phase 1: Role System Foundation (FR3-FR4, FR7-FR11)**
- Backend: `app/Concerns/AuthorizesPermissions.php`, `config/permissions.php`, `app/Enums/Permission.php`
- Backend: `app/Http/Controllers/TeamController.php`, `app/Http/Requests/UpdatePermissionsRequest.php`
- Frontend: `resources/js/pages/team/`, `resources/js/components/team/`
- Composable: `resources/js/composables/usePermissions.ts`
- Migration: `xxxx_add_permissions_to_workspace_user.php`
- Tests: `tests/Feature/Team/`
**Phase 2: Dashboard Separation (FR24-FR28)**
- Backend: `app/Http/Controllers/DashboardController.php` (rewrite)
- Frontend: `resources/js/pages/Dashboard.vue` (rewrite), `resources/js/components/dashboard/`
- Types: `resources/js/types/dashboard.ts`
- Tests: `tests/Feature/Dashboard/`
**Phase 3: Collaboration Features (FR29-FR33)**
- Backend: `app/Http/Controllers/NudgeController.php`, `app/Notifications/`
- Frontend: `resources/js/pages/notifications/`, `resources/js/components/notifications/`
- Frontend: `resources/js/components/app/NotificationBell.vue`
- Migration: `xxxx_create_notifications_table.php`
- Tests: `tests/Feature/Notifications/`
**Phase 4: Workflow Efficiency (FR17, FR32, FR41-FR44)**
- Backend: `app/Http/Controllers/BulkDeclarationController.php`
- Frontend: `resources/js/pages/declarations/BulkCreate.vue`, `resources/js/components/filters/`
- Composables: `resources/js/composables/useFilters.ts`, `useBulkSelection.ts`
- Migration: `xxxx_add_fulltext_indexes.php`
- Tests: `tests/Feature/Declarations/BulkCreateTest.php`, `tests/Feature/Filters/`
**Phase 5: Archive System (FR21-FR23, FR45-FR51)**
- Backend: `app/Http/Controllers/ArchiveController.php`, `app/Observers/DeclarationObserver.php`
- Frontend: `resources/js/pages/archive/`, `resources/js/components/archive/`
- Migration: `xxxx_add_archived_at_to_declarations.php`
- Tests: `tests/Feature/Declarations/ArchiveTest.php`
**Cross-Cutting Concerns:**
| Concern | Location |
|---|---|
| Workspace scoping | `app/Concerns/HasWorkspaceScope.php` → used by all controllers |
| Permission checking | `app/Concerns/AuthorizesPermissions.php` → used by all controllers |
| Role-scoped queries | `scopeForUser()` on Declaration model |
| Audit logging | Spatie Activity Log trait on all models (existing) |
| Cache invalidation | `DeclarationObserver` → busts dashboard cache on status change |
| Filter handling | `useFilters.ts` composable → `applyFilters()` controller method |
| Error handling | `abort(404)` convention → all controllers |
### Integration Points
**Internal Communication:**
- Controller → Notification: Via `$user->notify(new XxxNotification())` (queued)
- Controller → Cache: Via `Cache::remember()` / `Cache::forget()` (Redis)
- Observer → Cache: `DeclarationObserver` fires cache invalidation on model events
- Frontend → Backend: Inertia `router.get()` / `router.post()` / `router.put()` / `router.delete()`
**External Integrations:**
| Service | Integration Point | Configuration |
|---|---|---|
| Amazon SES (eu-west-1) | `Mail` facade via `ses` driver | `config/mail.php`, `.env` |
| S3-compatible storage | `Storage` facade via `s3` disk | `config/filesystems.php`, `.env` |
| Sentry | Error reporting SDK | `config/sentry.php`, `.env` |
| Laravel Pulse | Admin dashboard route | `config/pulse.php` |
**Data Flow (Declaration Lifecycle):**
```
Create → DB::transaction → Declaration model → Activity Log
→ Cache bust (dashboard)
→ Optional: notify client (queued mail)
Status Change → Controller validates transition → Update model → Observer fires
→ Cache bust (dashboard)
→ If "ferme": set archived_at
→ Activity Log
Nudge → NudgeController → NudgeNotification (database + mail channels)
→ Database: notifications table
→ Mail: Redis queue → SES
```
### File Organization Patterns
**New files follow existing conventions:**
- Controllers: `app/Http/Controllers/{Resource}Controller.php` (resource controllers)
- Requests: `app/Http/Requests/{Action}{Resource}Request.php` (Store/Update prefix)
- Models: `app/Models/{Resource}.php` (singular PascalCase)
- Enums: `app/Enums/{ResourceProperty}.php` (PascalCase)
- Notifications: `app/Notifications/{Action}Notification.php`
- Pages: `resources/js/pages/{resource}/{Action}.vue` (lowercase plural folder, PascalCase file)
- Components: `resources/js/components/{feature}/{ComponentName}.vue` (lowercase folder, PascalCase file)
- Types: `resources/js/types/{feature}.ts` (lowercase singular)
- Tests: `tests/Feature/{Resource}/{ActionTest}.php` (PascalCase folder and file)
**Test Organization:**
- Feature tests mirror controller structure: one test file per controller action or user flow
- Unit tests for pure logic: status transitions, permission checks
- All tests use `RefreshDatabase`, `test()` closures, `expect()` assertions
### Development Workflow Integration
**Route Registration (web.php):**
```php
// Authenticated + workspace routes
Route::middleware(['auth', 'verified', 'workspace'])->group(function () {
Route::get('/dashboard', DashboardController::class)->name('dashboard');
Route::resource('clients', ClientController::class);
Route::resource('declarations', DeclarationController::class);
Route::post('declarations/bulk', [BulkDeclarationController::class, 'store'])->name('declarations.bulk.store');
Route::get('archive', [ArchiveController::class, 'index'])->name('archive.index');
Route::get('archive/{declaration}', [ArchiveController::class, 'show'])->name('archive.show');
Route::post('archive/{declaration}/reopen', [ArchiveController::class, 'reopen'])->name('archive.reopen');
Route::get('team', [TeamController::class, 'index'])->name('team.index');
Route::put('team/{workspaceUser}/permissions', [TeamController::class, 'updatePermissions'])->name('team.permissions');
Route::post('nudge', [NudgeController::class, 'store'])->name('nudge.store');
Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index');
Route::post('notifications/{notification}/read', [NotificationController::class, 'markAsRead'])->name('notifications.read');
});
// Admin routes
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () { ... });
// Public client portal
Route::middleware(['client.token'])->prefix('portal')->name('portal.')->group(function () { ... });
```
**Build & Deployment:**
- Development: `composer dev` (PHP server + queue worker + logs + Vite HMR)
- Testing: `composer test` (Pint + Pest)
- Production: Laravel Forge push-to-deploy → Nginx + PHP-FPM + Supervisor (queue:work) + Redis
## Architecture Validation Results
### Coherence Validation
**Decision Compatibility:** PASS
All 16 decisions (D1-D16) are mutually compatible. Redis triple-duty (D4/D5/D7) is standard Laravel production. JSON permissions (D1) + `abort(404)` works cleanly with Inertia.js. MySQL FULLTEXT (D3) + status-based archive (D2) coexist on the same declarations table. S3 SSE (D6) is transparent to Spatie Media Library. SES (D13) + Redis queue (D5) = standard mail pipeline. No version conflicts across the stack (Laravel 12, Vue 3.5, Inertia.js 2.0, PHP 8.4, MySQL 8.4).
**Pattern Consistency:** PASS
All patterns use `workspace_id` scoping consistently. All permission checks use `abort(404)` (never 403). All filters use URL query params (never session). All notifications use Laravel's `DatabaseNotification`. All bulk operations wrapped in `DB::transaction()`. Naming conventions consistent: snake_case PHP/DB, camelCase TypeScript/Vue, PascalCase components.
**Structure Alignment:** PASS
Project structure follows existing Laravel conventions. New files organized in the same pattern as existing files. Phase-to-directory mapping is explicit and non-overlapping. Controller traits (`HasWorkspaceScope`, `AuthorizesPermissions`) provide shared behavior without service layer complexity.
### Requirements Coverage Validation
**Functional Requirements (58 FRs) -- All Covered:**
| FR Group | FRs | Architectural Support |
|---|---|---|
| Workspace & Onboarding | FR1-FR6 | Existing auth + workspace model. No new architecture needed. |
| Team & Role Management | FR7-FR11 | D1 (JSON permissions), `AuthorizesPermissions` trait, `TeamController`, Phase 1 structure |
| Client Management | FR12-FR15 | Existing `ClientController` + extended with filters (Phase 4) |
| Declaration Lifecycle | FR16-FR23 | `DeclarationController`, `DeclarationObserver`, status state machine, D2 (archive) |
| Dashboard & Visibility | FR24-FR28 | `DashboardController` rewrite, dashboard components, D4 (Redis cache), role-scoped queries |
| Collaboration & Notifications | FR29-FR33 | D8 (DatabaseNotification), `NudgeController`, Phase 3 components, D9 (bulk + queue) |
| Client Portal & Documents | FR34-FR40 | Existing client portal controllers, token-based access, Spatie Media Library |
| Search/Filtering/Navigation | FR41-FR44 | D3 (MySQL FULLTEXT), D11 (URL params), `FilterBar` component, `useFilters` composable |
| Archive System | FR45-FR51 | D2 (`archived_at`), `ArchiveController`, `scopeArchived()`, D12 (PDF.js preview) |
| Activity & Audit | FR52-FR55 | Spatie Activity Log (existing), extended to new models |
| Platform Administration | FR56-FR58 | `Admin/` controller namespace, `EnsureUserIsAdmin` middleware |
**Non-Functional Requirements (28 NFRs) -- All Covered:**
| NFR Category | Key NFRs | Architectural Support |
|---|---|---|
| Performance (NFR1-6) | 2s pages, 10s bulk, 1s search, 3s dashboard | SSR via Inertia, Redis cache (D4), FULLTEXT indexes (D3), synchronous bulk (D9) |
| Security (NFR7-15) | TLS, AES-256, tenant isolation, 2FA, CNDP | S3 SSE (D6), workspace scoping pattern, Fortify 2FA (existing), EU hosting (D14) |
| Scalability (NFR16-20) | 200 workspaces, 1K users, 500 clients, 5K declarations | MySQL indexing, Redis cache, pagination, workspace-scoped queries |
| Reliability (NFR21-28) | 99.5% uptime, 1hr RPO/RTO, >99% email | Laravel Forge (D14), SES (D13), Redis queue retries (D5), Sentry + Pulse (D16) |
### Implementation Readiness Validation
**Decision Completeness:** PASS -- All 16 decisions have choice, rationale, implementation details, and affected FRs. Technology versions specified. Implementation sequence defined.
**Structure Completeness:** PASS -- Every new file has a specific location. Phase-to-file mapping is explicit for all 5 phases + pre-phase. Route registration example covers all new endpoints.
**Pattern Completeness:** PASS -- 12 pattern categories with PHP and Vue code examples. Anti-patterns explicitly listed. 10 enforcement guidelines for AI agents.
### Gap Analysis Results
**No Critical Gaps Found.**
**Minor Gaps (non-blocking, addressable during implementation):**
1. **Client portal token rotation** -- Token security pattern (expiry, single-use, revocation) not detailed. Existing `FolderInvitation` model handles this; pattern carries over to `DeclarationInvitation`.
2. **Backup/restore procedure** -- D14 mentions Laravel Forge but no specific backup schedule. Recommendation: daily MySQL dump + S3 versioning. Configure during infrastructure provisioning.
3. **Rate limiting** -- Not explicitly defined. Laravel's built-in `ThrottleRequests` middleware (60 req/min default) is sufficient for MVP.
4. **Email template styling** -- No pattern defined for email HTML structure. Recommendation: use Laravel's built-in Markdown mailables for consistent styling.
### Architecture Completeness Checklist
**Requirements Analysis**
- [x] Project context thoroughly analyzed (58 FRs, 28 NFRs, 6 user journeys)
- [x] Scale and complexity assessed (High complexity, ~15-20 components)
- [x] Technical constraints identified (brownfield, 12 locked-in dependencies)
- [x] Cross-cutting concerns mapped (7 concerns)
**Architectural Decisions**
- [x] Critical decisions documented with versions (16 decisions, D1-D16)
- [x] Technology stack fully specified (Laravel 12, Vue 3.5, MySQL 8.4, Redis, S3, SES)
- [x] Integration patterns defined (Inertia props, notification channels, cache, queue)
- [x] Performance considerations addressed (Redis cache, FULLTEXT, SSR, pagination)
**Implementation Patterns**
- [x] Naming conventions established (snake_case PHP/DB, camelCase TS, PascalCase components)
- [x] Structure patterns defined (controller traits, model scopes, composables)
- [x] Communication patterns specified (Inertia router, notification channels, cache invalidation)
- [x] Process patterns documented (error handling, status transitions, bulk operations)
**Project Structure**
- [x] Complete directory structure defined (~60 new files mapped)
- [x] Component boundaries established (5 boundaries)
- [x] Integration points mapped (4 external services)
- [x] Requirements to structure mapping complete (all 5 phases + pre-phase)
### Architecture Readiness Assessment
**Overall Status:** READY FOR IMPLEMENTATION
**Confidence Level:** High
**Key Strengths:**
- Brownfield foundation eliminates boilerplate -- auth, multi-tenancy, file uploads, audit logging already work
- Every decision leverages Laravel's built-in capabilities (no exotic dependencies)
- Patterns are concrete with code examples, not abstract guidelines
- Phase-to-file mapping gives AI agents unambiguous implementation targets
- 10 enforcement rules prevent the most common cross-agent conflicts
**Areas for Future Enhancement (Post-MVP):**
- Soketi WebSocket integration for real-time notification badges
- Meilisearch migration if search volume exceeds MySQL FULLTEXT capacity
- Full observability stack (Grafana/Prometheus) beyond Pulse + Sentry
- API layer if mobile app or third-party integrations are needed
### Implementation Handoff
**AI Agent Guidelines:**
- Read `project-context.md` AND this architecture document before implementing any feature
- Follow all 16 architectural decisions exactly as documented
- Use implementation patterns consistently across all components
- Respect project structure and boundaries defined in Project Structure section
- Follow the implementation sequence: Redis → Permissions → Archive column → FULLTEXT → Notifications → S3 → SES → Hosting → Monitoring
**First Implementation Priority:**
Pre-Phase: "Folders → Declarations" terminology migration across all models, controllers, enums, pages, tests, and database (migration to rename table + columns). This must complete before any Phase 1-5 work begins.