# Story 0.5: Add Foundation Database Migrations and Declaration Status Flow

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## 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
