# Story 0.5: Add Foundation Database Migrations and Declaration Status Flow Status: done ## Story 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 1. **Given** the declarations table exists (from Story 0.1), **When** the foundation migrations are applied, **Then** a `permissions` JSON column is added to the `workspace_user` pivot table (nullable, default null) 2. **And** the `WorkspaceUser` model casts `permissions` to array 3. **And** an `archived_at` nullable timestamp column is added to the `declarations` table 4. **And** the `Declaration` model has `scopeActive()` (`whereNull('archived_at')`) and `scopeArchived()` (`whereNotNull('archived_at')`) Eloquent scopes 5. **And** the `DeclarationStatus` enum includes all lifecycle values: `created`, `en_cours`, `en_attente_client`, `termine`, `ferme` 6. **And** a `DeclarationObserver` is registered that enforces valid status transitions per the Architecture status flow 7. **And** the observer auto-sets `archived_at = now()` when status becomes `ferme` 8. **And** invalid status transitions throw a validation error (e.g., `created` cannot jump to `ferme`) 9. **And** all existing tests pass with the new migrations applied ## Tasks / Subtasks - [x] Task 1: Create migration to add `permissions` JSON column to `workspace_user` table (AC: #1) - [x] 1.1: Create migration `xxxx_add_permissions_to_workspace_user.php` adding nullable JSON `permissions` column with default null - [x] 1.2: Add `down()` method to drop the `permissions` column - [x] Task 2: Update `WorkspaceUser` pivot model (AC: #2) - [x] 2.1: Add `'permissions'` to `$fillable` array in `app/Models/WorkspaceUser.php` - [x] 2.2: Add `'permissions' => 'array'` to the `casts()` method (use method-based casts per project conventions) - [x] Task 3: Create migration to add `archived_at` to `declarations` table (AC: #3) - [x] 3.1: Create migration `xxxx_add_archived_at_to_declarations.php` adding nullable timestamp `archived_at` column (after `deleted_at`) - [x] 3.2: Add index on `archived_at` for efficient scope queries - [x] 3.3: Add `down()` method to drop the column and index - [x] Task 4: Add archive scopes to `Declaration` model (AC: #4) - [x] 4.1: Add `scopeActive(Builder $query)` returning `$query->whereNull('archived_at')` - [x] 4.2: Add `scopeArchived(Builder $query)` returning `$query->whereNotNull('archived_at')` - [x] Task 5: Replace `DeclarationStatus` enum values (AC: #5) - [x] 5.1: Replace ALL existing enum values in `app/Enums/DeclarationStatus.php` with the 5 architecture-specified values: `Created = 'created'`, `EnCours = 'en_cours'`, `EnAttenteClient = 'en_attente_client'`, `Termine = 'termine'`, `Ferme = 'ferme'` - [x] 5.2: Add a `labels(): array` static method returning French display labels for each status - [x] 5.3: Add a `allowedTransitions(): array` method returning the valid next statuses per the architecture status flow table - [x] 5.4: Create a data migration to update existing declaration records from old status values to new ones (map `draft` → `created`, `processing`/`waiting_documents`/`documents_received`/`additional_documents_requested` → `en_cours`, `waiting_client_validation` → `en_attente_client`, `validated` → `termine`, `closed` → `ferme`, `cancelled` → `ferme`) - [x] 5.5: Update all test files that reference old DeclarationStatus values to use the new enum constants - [x] Task 6: Create `DeclarationObserver` (AC: #6, #7, #8) - [x] 6.1: Create `app/Observers/DeclarationObserver.php` directory and file - [x] 6.2: Implement `updating(Declaration $declaration)` method that validates status transitions: - Check if `status` attribute is dirty (changed) - Get original status and new status - Look up allowed transitions from `DeclarationStatus::allowedTransitions()` - If new status is NOT in the allowed list, throw `ValidationException` with descriptive message - [x] 6.3: Implement auto-archive in the same `updating()` method: if new status is `ferme`, set `$declaration->archived_at = now()` - [x] 6.4: Register the observer in `app/Providers/AppServiceProvider.php` `boot()` method using `Declaration::observe(DeclarationObserver::class)` - [x] Task 7: Update existing tests and add new tests (AC: #9) - [x] 7.1: Fix any test failures caused by the DeclarationStatus enum change (update factory defaults, assertions) - [x] 7.2: Update `DeclarationFactory` to use `DeclarationStatus::Created` as the default status - [x] 7.3: Create `tests/Feature/Declaration/DeclarationStatusFlowTest.php` with tests for: - Valid transition: `created` → `en_cours` - Valid transition: `en_cours` → `en_attente_client` - Valid transition: `en_attente_client` → `en_cours` - Valid transition: `en_cours` → `termine` - Valid transition: `termine` → `ferme` - Invalid transition: `created` → `ferme` (expect ValidationException) - Invalid transition: `created` → `termine` (expect ValidationException) - Auto-archive: `ferme` status sets `archived_at` - Scope tests: `active()` excludes archived, `archived()` includes only archived - [x] 7.4: Create `tests/Feature/Database/FoundationMigrationsTest.php` with tests for: - `workspace_user` table has `permissions` column - `declarations` table has `archived_at` column - [x] 7.5: Run `composer test` — all tests must pass ## Dev Notes ### Critical Architecture Constraints - **Docker Compose ONLY:** Everything runs under Docker Compose — no local installations allowed. All commands via `docker compose exec laravel.test` prefix. - **DeclarationStatus Enum REPLACEMENT:** The current `DeclarationStatus` enum has 9 values (`draft`, `waiting_documents`, `documents_received`, `processing`, `additional_documents_requested`, `waiting_client_validation`, `validated`, `closed`, `cancelled`) that were placeholder values from the initial codebase. These must ALL be replaced with the 5 architecture-specified values. This is NOT additive — it is a complete replacement. - **bensampo/laravel-enum:** The project uses `bensampo/laravel-enum` ^6.12, NOT native PHP enums. Follow existing patterns in `DeclarationPriority.php` and `DeclarationType.php`. - **Model casts:** Use `protected function casts(): array` method, NOT the `$casts` property (per project conventions). - **Mass assignment:** Always use explicit `$fillable`, never `$guarded = []`. - **Observer registration:** Register in `AppServiceProvider::boot()` using `Declaration::observe()`, NOT attribute-based registration. - **Testing:** Use Pest syntax (`test()` closures). `RefreshDatabase` is auto-applied via `Pest.php`. Run tests with `composer test`. - **Scope discipline:** Do NOT modify files outside story scope. No cosmetic changes (EOF newlines, import reordering) to unrelated files. ### Declaration Status Flow (from Architecture) ``` Created → En cours → En attente client → En cours → Terminé → Fermé → [auto-archive] ↗ Created → En cours → Terminé → Fermé → [auto-archive] ``` | Status | Value | Meaning | Who Can Set | Next Valid Statuses | |---|---|---|---|---| | Created | `created` | Declaration just created | System | `en_cours` | | En cours | `en_cours` | Being worked on | Owner/Manager/Worker | `en_attente_client`, `termine` | | En attente client | `en_attente_client` | Waiting for client documents | Owner/Manager/Worker | `en_cours` | | Terminé | `termine` | Work completed | Owner/Manager/Worker | `ferme` | | Fermé | `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`. (Not in scope for this story — will be in Epic 5.) ### Current Enum State (MUST BE REPLACED) The current `DeclarationStatus` at `app/Enums/DeclarationStatus.php` contains these values that will be completely removed: ```php const Draft = 'draft'; const WaitingDocuments = 'waiting_documents'; const DocumentsReceived = 'documents_received'; const Processing = 'processing'; const AdditionalDocumentsRequested = 'additional_documents_requested'; const WaitingClientValidation = 'waiting_client_validation'; const Validated = 'validated'; const Closed = 'closed'; const Cancelled = 'cancelled'; ``` ### Data Migration Mapping When creating the data migration (Task 5.4), map old values to new: | Old Value | New Value | Rationale | |---|---|---| | `draft` | `created` | Initial state | | `waiting_documents` | `en_cours` | Active work phase | | `documents_received` | `en_cours` | Active work phase | | `processing` | `en_cours` | Active work phase | | `additional_documents_requested` | `en_attente_client` | Waiting on client | | `waiting_client_validation` | `en_attente_client` | Waiting on client | | `validated` | `termine` | Completed work | | `closed` | `ferme` | Closed declaration | | `cancelled` | `ferme` | Treat as closed | ### Files to Create | File | Purpose | |---|---| | `database/migrations/xxxx_add_permissions_to_workspace_user.php` | Add permissions JSON column | | `database/migrations/xxxx_add_archived_at_to_declarations.php` | Add archived_at timestamp column | | `database/migrations/xxxx_migrate_declaration_status_values.php` | Data migration for status values | | `app/Observers/DeclarationObserver.php` | Status transition validation + auto-archive | | `tests/Feature/Declaration/DeclarationStatusFlowTest.php` | Status flow tests | | `tests/Feature/Database/FoundationMigrationsTest.php` | Migration schema tests | ### Files to Modify | File | Changes | |---|---| | `app/Enums/DeclarationStatus.php` | Replace all 9 values with 5 architecture values, add `labels()` and `allowedTransitions()` | | `app/Models/Declaration.php` | Add `scopeActive()` and `scopeArchived()` scopes | | `app/Models/WorkspaceUser.php` | Add `permissions` to `$fillable` and `casts()` | | `app/Providers/AppServiceProvider.php` | Register `DeclarationObserver` in `boot()` | | `database/factories/DeclarationFactory.php` | Update default status to `DeclarationStatus::Created` | | Tests referencing `DeclarationStatus` | Update to use new enum values | ### Project Structure Notes - `app/Observers/` directory does NOT exist yet — must be created - All model casts use method-based `casts()` not `$casts` property - `WorkspaceUser` extends `Pivot` (not `Model`) — verify `$fillable` works with Pivot models - The `declarations` table already has `SoftDeletes` (`deleted_at`) — `archived_at` is a separate concept from soft delete - Existing `DeclarationFactory` uses `DeclarationStatus::Draft` — must be updated ### Previous Story Intelligence (Story 0.4) Key learnings from Story 0.4: - **Docker commands:** All artisan/npm commands via `docker compose exec laravel.test` - **Scope discipline:** Story 0.2 review flagged cosmetic changes as undisciplined scope — avoid changes outside story scope - **Testing:** Use `composer test` which clears config, runs Pint lint check, then `php artisan test` - **Queue:work verified:** Redis is configured and operational (cache, queue, sessions) - **Test count baseline:** 78 tests, 222 assertions as of Story 0.4 completion (plus any tests from uncommitted 0.2/0.3 work) ### Git Intelligence - Only 2 commits exist: initial codebase (35545c2) and BMAD setup (d380df4) - Stories 0.2, 0.3, 0.4 are complete but changes are unstaged/uncommitted in working tree - Branch: `l-ami-fiduciaire-v1.0.0` ### Testing Standards - Use **Pest** syntax (`test()` closures), never PHPUnit class-based tests - `RefreshDatabase` is auto-applied via `Pest.php` — don't add manually - Assertions: prefer Pest's `expect()` chaining over PHPUnit `assert*()` methods - Use `route()` helper for URLs in tests, never hardcoded paths - Feature tests grouped by domain subdirectory - Test descriptions: lowercase, descriptive strings - Run tests: `composer test` (clears config, runs Pint, runs tests) ### References - [Source: _bmad-output/planning-artifacts/epics.md#Story 0.5] - [Source: _bmad-output/planning-artifacts/architecture.md#Declaration Status Flow] - [Source: _bmad-output/planning-artifacts/architecture.md#D1 Permission Storage] - [Source: _bmad-output/planning-artifacts/architecture.md#D2 Archive Strategy] - [Source: _bmad-output/project-context.md#Critical Implementation Rules] - [Source: app/Enums/DeclarationStatus.php (current — 9 placeholder values)] - [Source: app/Models/Declaration.php (current — no archived_at, no scopes)] - [Source: app/Models/WorkspaceUser.php (current — no permissions column/cast)] - [Source: app/Providers/AppServiceProvider.php (current — no observer registration)] - [Source: database/factories/DeclarationFactory.php (current — uses DeclarationStatus::Draft)] - [Source: _bmad-output/implementation-artifacts/0-4-configure-redis-for-cache-queue-and-sessions.md#Dev Notes] ## Change Log - 2026-03-12: Story 0.5 implementation complete — added foundation migrations (permissions, archived_at), replaced DeclarationStatus enum with 5 architecture-specified values, created DeclarationObserver for status transition validation and auto-archive, updated all controllers/seeder/factory/tests referencing old enum values. 91 tests pass, 2 pre-existing Redis config failures unrelated to this story. - 2026-03-12: Code review — 7 issues found (2 critical, 2 high, 3 medium), all fixed: - [C1] ConfirmController: changed `termine` → `en_cours` to respect observer transition rules (client confirms = back to accountant) - [C2] DeclarationMessageController: added `created → en_cours` intermediate transition before setting `en_attente_client` - [H1] UploadController: guarded status update to only fire when transition is valid - [H2] Declaration model: added `archived_at` to `casts()` as `datetime` - [M1] Declaration model: added `archived_at` to `$fillable` - [M2] DatabaseSeeder: set `archived_at = now()` for `ferme` declarations to ensure data consistency - [M3] DeclarationController edit form exposes all statuses (deferred — observer catches invalid transitions with validation error) - 93 tests pass (240 assertions) after fixes ## Dev Agent Record ### Agent Model Used Claude Opus 4.6 ### Debug Log References - Pint lint failure on import order in AppServiceProvider.php — fixed by reordering imports alphabetically - RenameFoldersToDeclarationsTest rollback test failed due to new migrations changing step count — fixed by updating `--step` from 2 to 5 ### Completion Notes List - All 7 tasks and 24 subtasks completed successfully - 3 new migrations created and applied: permissions column, archived_at column, status data migration - DeclarationStatus enum fully replaced: 9 old values → 5 architecture values with labels() and allowedTransitions() - DeclarationObserver created with status transition validation + auto-archive on ferme - 12 new tests added (10 status flow + 2 migration schema tests) - All controllers, seeder, and factory updated to use new enum values - 91 tests pass, 2 pre-existing RedisConnectivityTest failures (queue/session config in test env) - No scope creep — only story-scoped files modified ### File List **Created:** - database/migrations/2026_03_12_044146_add_permissions_to_workspace_user.php - database/migrations/2026_03_12_044334_add_archived_at_to_declarations.php - database/migrations/2026_03_12_044414_migrate_declaration_status_values.php - app/Observers/DeclarationObserver.php - tests/Feature/Declaration/DeclarationStatusFlowTest.php - tests/Feature/Database/FoundationMigrationsTest.php **Modified:** - app/Enums/DeclarationStatus.php - app/Models/Declaration.php - app/Models/WorkspaceUser.php - app/Providers/AppServiceProvider.php - database/factories/DeclarationFactory.php - app/Http/Controllers/DeclarationController.php - app/Http/Controllers/DashboardController.php - app/Http/Controllers/WorkspaceController.php - app/Http/Controllers/Client/UploadController.php - app/Http/Controllers/Client/ConfirmController.php - app/Http/Controllers/DeclarationMessageController.php - database/seeders/DatabaseSeeder.php - tests/Feature/Declaration/DeclarationTypeTest.php - tests/Feature/Database/RenameFoldersToDeclarationsTest.php