Stories 0.2-0.5: rename folders→declarations (backend+frontend), configure Redis for cache/queue/sessions, add foundation database migrations (permissions, archived_at), replace DeclarationStatus enum with architecture lifecycle values, create DeclarationObserver for status transition validation and auto-archive, fix controller status transitions to respect observer rules. 93 tests pass (240 assertions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
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
- Given the declarations table exists (from Story 0.1), When the foundation migrations are applied, Then a
permissionsJSON column is added to theworkspace_userpivot table (nullable, default null) - And the
WorkspaceUsermodel castspermissionsto array - And an
archived_atnullable timestamp column is added to thedeclarationstable - And the
Declarationmodel hasscopeActive()(whereNull('archived_at')) andscopeArchived()(whereNotNull('archived_at')) Eloquent scopes - And the
DeclarationStatusenum includes all lifecycle values:created,en_cours,en_attente_client,termine,ferme - And a
DeclarationObserveris registered that enforces valid status transitions per the Architecture status flow - And the observer auto-sets
archived_at = now()when status becomesferme - And invalid status transitions throw a validation error (e.g.,
createdcannot jump toferme) - And all existing tests pass with the new migrations applied
Tasks / Subtasks
-
Task 1: Create migration to add
permissionsJSON column toworkspace_usertable (AC: #1)- 1.1: Create migration
xxxx_add_permissions_to_workspace_user.phpadding nullable JSONpermissionscolumn with default null - 1.2: Add
down()method to drop thepermissionscolumn
- 1.1: Create migration
-
Task 2: Update
WorkspaceUserpivot model (AC: #2)- 2.1: Add
'permissions'to$fillablearray inapp/Models/WorkspaceUser.php - 2.2: Add
'permissions' => 'array'to thecasts()method (use method-based casts per project conventions)
- 2.1: Add
-
Task 3: Create migration to add
archived_attodeclarationstable (AC: #3)- 3.1: Create migration
xxxx_add_archived_at_to_declarations.phpadding nullable timestamparchived_atcolumn (afterdeleted_at) - 3.2: Add index on
archived_atfor efficient scope queries - 3.3: Add
down()method to drop the column and index
- 3.1: Create migration
-
Task 4: Add archive scopes to
Declarationmodel (AC: #4)- 4.1: Add
scopeActive(Builder $query)returning$query->whereNull('archived_at') - 4.2: Add
scopeArchived(Builder $query)returning$query->whereNotNull('archived_at')
- 4.1: Add
-
Task 5: Replace
DeclarationStatusenum values (AC: #5)- 5.1: Replace ALL existing enum values in
app/Enums/DeclarationStatus.phpwith the 5 architecture-specified values:Created = 'created',EnCours = 'en_cours',EnAttenteClient = 'en_attente_client',Termine = 'termine',Ferme = 'ferme' - 5.2: Add a
labels(): arraystatic method returning French display labels for each status - 5.3: Add a
allowedTransitions(): arraymethod returning the valid next statuses per the architecture status flow table - 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) - 5.5: Update all test files that reference old DeclarationStatus values to use the new enum constants
- 5.1: Replace ALL existing enum values in
-
Task 6: Create
DeclarationObserver(AC: #6, #7, #8)- 6.1: Create
app/Observers/DeclarationObserver.phpdirectory and file - 6.2: Implement
updating(Declaration $declaration)method that validates status transitions:- Check if
statusattribute 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
ValidationExceptionwith descriptive message
- Check if
- 6.3: Implement auto-archive in the same
updating()method: if new status isferme, set$declaration->archived_at = now() - 6.4: Register the observer in
app/Providers/AppServiceProvider.phpboot()method usingDeclaration::observe(DeclarationObserver::class)
- 6.1: Create
-
Task 7: Update existing tests and add new tests (AC: #9)
- 7.1: Fix any test failures caused by the DeclarationStatus enum change (update factory defaults, assertions)
- 7.2: Update
DeclarationFactoryto useDeclarationStatus::Createdas the default status - 7.3: Create
tests/Feature/Declaration/DeclarationStatusFlowTest.phpwith 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:
fermestatus setsarchived_at - Scope tests:
active()excludes archived,archived()includes only archived
- Valid transition:
- 7.4: Create
tests/Feature/Database/FoundationMigrationsTest.phpwith tests for:workspace_usertable haspermissionscolumndeclarationstable hasarchived_atcolumn
- 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.testprefix. - DeclarationStatus Enum REPLACEMENT: The current
DeclarationStatusenum 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 inDeclarationPriority.phpandDeclarationType.php. - Model casts: Use
protected function casts(): arraymethod, NOT the$castsproperty (per project conventions). - Mass assignment: Always use explicit
$fillable, never$guarded = []. - Observer registration: Register in
AppServiceProvider::boot()usingDeclaration::observe(), NOT attribute-based registration. - Testing: Use Pest syntax (
test()closures).RefreshDatabaseis auto-applied viaPest.php. Run tests withcomposer 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:
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$castsproperty WorkspaceUserextendsPivot(notModel) — verify$fillableworks with Pivot models- The
declarationstable already hasSoftDeletes(deleted_at) —archived_atis a separate concept from soft delete - Existing
DeclarationFactoryusesDeclarationStatus::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 testwhich clears config, runs Pint lint check, thenphp 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 RefreshDatabaseis auto-applied viaPest.php— don't add manually- Assertions: prefer Pest's
expect()chaining over PHPUnitassert*()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_coursto respect observer transition rules (client confirms = back to accountant) - [C2] DeclarationMessageController: added
created → en_coursintermediate transition before settingen_attente_client - [H1] UploadController: guarded status update to only fire when transition is valid
- [H2] Declaration model: added
archived_attocasts()asdatetime - [M1] Declaration model: added
archived_atto$fillable - [M2] DatabaseSeeder: set
archived_at = now()forfermedeclarations 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
- [C1] ConfirmController: changed
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
--stepfrom 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