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

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

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

16 KiB

Story 0.1: Rename Folders to Declarations in Database

Status: done

Story

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

Acceptance Criteria

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

Tasks / Subtasks

  • Task 1: Create the rename migration file (AC: #1, #2, #3, #4, #5, #6)
    • 1.1: Create migration xxxx_rename_folders_to_declarations.php
    • 1.2: In up() — Drop foreign key constraints: messages_folder_id_foreign and folder_invitations_folder_id_foreign
    • 1.3: In up() — Rename column folder_iddeclaration_id in messages table
    • 1.4: In up() — Drop index messages_folder_id_created_at_index, recreate as [declaration_id, created_at]
    • 1.5: In up() — Rename column folder_iddeclaration_id in folder_invitations table
    • 1.6: In up() — Rename table foldersdeclarations
    • 1.7: In up() — Rename table folder_invitationsdeclaration_invitations
    • 1.8: In up() — Re-add FK constraints using explicit ->constrained('declarations')->cascadeOnDelete() on both tables
    • 1.9: Implement down() with explicit reverse steps (see Dev Notes)
  • Task 2: Verify migration executes successfully (AC: #1, #2, #3, #4, #6)
    • 2.1: Run php artisan migrate — confirm zero errors
    • 2.2: Verify tables: php artisan tinker --execute="echo Schema::hasTable('declarations') ? 'OK' : 'FAIL';"
    • 2.3: Verify columns: php artisan tinker --execute="echo Schema::hasColumn('messages', 'declaration_id') ? 'OK' : 'FAIL';"
    • 2.4: Verify composite index exists on messages(declaration_id, created_at)
    • 2.5: Verify existing seed/test data is intact after migration
  • Task 3: Verify rollback works (AC: #5)
    • 3.1: Run php artisan migrate:rollback — confirm tables revert to folders, folder_invitations
    • 3.2: Verify foreign key columns revert to folder_id
    • 3.3: Re-run php artisan migrate to confirm re-apply also works

Dev Notes

Critical Architecture Constraints

  • Database engine: MySQL 8.4 (production) / SQLite for local dev (check .env)
  • Multi-tenant: All tables are workspace-scoped. The migration is schema-only (rename), no data transformation needed
  • Spatie media_library: The media table has a polymorphic model_type column storing App\Models\Folder (FQCN, no morph map registered). NOT addressed in this story — handled in Story 0.2 when the model class is renamed
  • Spatie activity_log: The activity_log table has subject_type storing App\Models\Folder. Same as above — handled in Story 0.2
  • No morph map: The codebase has no Relation::morphMap() registration — polymorphic types store FQCN directly. Data updates for these columns are a Story 0.2 concern
  • Notifications table: Has notifiable_type morphs — not folder-related, no action needed

Migration Strategy — Order of Operations

The order matters to avoid foreign key constraint violations:

up() method:

  1. Drop FK constraints: $table->dropForeign('messages_folder_id_foreign') and $table->dropForeign('folder_invitations_folder_id_foreign')
  2. Rename columns folder_iddeclaration_id via $table->renameColumn('folder_id', 'declaration_id') in messages and folder_invitations
  3. Drop index messages_folder_id_created_at_index, create new $table->index(['declaration_id', 'created_at'])
  4. Schema::rename('folders', 'declarations')
  5. Schema::rename('folder_invitations', 'declaration_invitations')
  6. Re-add FKs: $table->foreign('declaration_id')->references('id')->on('declarations')->cascadeOnDelete() on both messages and declaration_invitations

down() method (explicit reverse):

  1. Drop FK constraints: $table->dropForeign('messages_declaration_id_foreign') and $table->dropForeign('declaration_invitations_declaration_id_foreign')
  2. Schema::rename('declaration_invitations', 'folder_invitations')
  3. Schema::rename('declarations', 'folders')
  4. Drop index on messages(declaration_id, created_at), recreate as $table->index(['folder_id', 'created_at'])
  5. Rename columns declaration_idfolder_id in messages and folder_invitations
  6. Re-add FKs: $table->foreign('folder_id')->references('id')->on('folders')->cascadeOnDelete() on both tables

Critical FK rule: Always use explicit ->on('declarations') (or ->on('folders') in down) — never bare ->constrained() which infers table from column name and can cause errors during rename transitions.

Tables and Columns Affected

Table Column/Index/Constraint Action
folders (entire table) Rename → declarations
folder_invitations (entire table) Rename → declaration_invitations
folder_invitations folder_id column Rename → declaration_id
folder_invitations FK folder_invitations_folder_id_foreign Drop, re-add as declaration_invitations_declaration_id_foreign
messages folder_id column Rename → declaration_id
messages FK messages_folder_id_foreign Drop, re-add as messages_declaration_id_foreign
messages Index messages_folder_id_created_at_index Drop, recreate with declaration_id

SQLite vs MySQL Considerations

  • Laravel 12 handles renameColumn() natively without doctrine/dbal. No additional package installation is needed
  • SQLite (local dev): Schema::rename() and renameColumn() work natively. Laravel handles SQLite's limited ALTER TABLE via internal table recreation
  • MySQL 8.4 (production): Native RENAME TABLE and ALTER TABLE ... RENAME COLUMN are well-supported
  • Recommendation: Use Laravel's Schema facade methods exclusively — never raw SQL — for cross-driver compatibility

Warning: Application Will Break After This Migration

After running this migration, the application will be non-functional until Story 0.2 (backend rename) is completed. Stories 0.1 and 0.2 should be implemented in the same development session.

Why it breaks: The Folder model has no explicit $table property — it relies on Laravel convention to infer folders. After rename, Laravel looks for folders which no longer exists.

Specific breakage points (all fixed in Story 0.2):

  • Folder model → table folders not found
  • FolderInvitation model → table folder_invitations not found, $fillable contains folder_id (column is now declaration_id)
  • Message model → $fillable contains folder_id, folder() relationship references wrong column
  • Middleware alias folder.invitation in bootstrap/app.php and route reference in web.php → Story 0.2 scope

Existing Migration Files for Reference

These are the original migration files that created the tables being renamed:

  • database/migrations/2026_02_27_103316_create_folders_table.php — Creates folders table with all columns
  • database/migrations/2026_03_01_144731_create_folder_invitations_table.php — Creates folder_invitations with folder_id FK
  • database/migrations/2026_03_01_144730_create_messages_table.php — Creates messages with folder_id FK and composite index
  • database/migrations/2026_03_01_144736_add_confirmation_fields_to_folders_table.php — Adds confirmation columns to folders
  • database/migrations/2026_03_09_000003_split_vat_folder_type.php — Data migration on folders table

Testing Standards

  • Use Pest syntax (test() closures), never PHPUnit class-based tests
  • RefreshDatabase is auto-applied via Pest.php — don't add it manually
  • Run tests: composer test
  • After migration, existing tests WILL fail (they reference Folder model). This is expected — Story 0.2 fixes them

Project Structure Notes

  • Migration file goes in: database/migrations/
  • Naming convention: YYYY_MM_DD_HHMMSS_rename_folders_to_declarations.php
  • Migration timestamp MUST be after 2026_03_09_000005 (latest existing migration). Use e.g. 2026_03_11_000001_rename_folders_to_declarations.php
  • This ensures the rename runs after split_vat_folder_type.php (which references DB::table('folders')) on both migrate and migrate:fresh
  • Laravel 12 handles column renames natively — no doctrine/dbal needed

Migration Code Skeleton

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        // 1. Drop FK constraints
        Schema::table('messages', function (Blueprint $table) {
            $table->dropForeign('messages_folder_id_foreign');
        });
        Schema::table('folder_invitations', function (Blueprint $table) {
            $table->dropForeign('folder_invitations_folder_id_foreign');
        });

        // 2. Rename columns
        Schema::table('messages', function (Blueprint $table) {
            $table->renameColumn('folder_id', 'declaration_id');
        });
        Schema::table('folder_invitations', function (Blueprint $table) {
            $table->renameColumn('folder_id', 'declaration_id');
        });

        // 3. Drop old index, create new
        Schema::table('messages', function (Blueprint $table) {
            $table->dropIndex('messages_folder_id_created_at_index');
            $table->index(['declaration_id', 'created_at']);
        });

        // 4-5. Rename tables
        Schema::rename('folders', 'declarations');
        Schema::rename('folder_invitations', 'declaration_invitations');

        // 6. Re-add FK constraints
        Schema::table('messages', function (Blueprint $table) {
            $table->foreign('declaration_id')->references('id')->on('declarations')->cascadeOnDelete();
        });
        Schema::table('declaration_invitations', function (Blueprint $table) {
            $table->foreign('declaration_id')->references('id')->on('declarations')->cascadeOnDelete();
        });
    }

    public function down(): void
    {
        // 1. Drop FK constraints
        Schema::table('messages', function (Blueprint $table) {
            $table->dropForeign('messages_declaration_id_foreign');
        });
        Schema::table('declaration_invitations', function (Blueprint $table) {
            $table->dropForeign('declaration_invitations_declaration_id_foreign');
        });

        // 2-3. Rename tables back
        Schema::rename('declaration_invitations', 'folder_invitations');
        Schema::rename('declarations', 'folders');

        // 4. Drop new index, recreate old
        Schema::table('messages', function (Blueprint $table) {
            $table->dropIndex('messages_declaration_id_created_at_index');
            $table->index(['folder_id', 'created_at']);
        });

        // 5. Rename columns back
        Schema::table('messages', function (Blueprint $table) {
            $table->renameColumn('declaration_id', 'folder_id');
        });
        Schema::table('folder_invitations', function (Blueprint $table) {
            $table->renameColumn('declaration_id', 'folder_id');
        });

        // 6. Re-add FK constraints
        Schema::table('messages', function (Blueprint $table) {
            $table->foreign('folder_id')->references('id')->on('folders')->cascadeOnDelete();
        });
        Schema::table('folder_invitations', function (Blueprint $table) {
            $table->foreign('folder_id')->references('id')->on('folders')->cascadeOnDelete();
        });
    }
};

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Story 0.1]
  • [Source: _bmad-output/planning-artifacts/architecture.md#Pre-Phase Migration]
  • [Source: _bmad-output/planning-artifacts/architecture.md#Declaration Status Flow (lines 453-479)]
  • [Source: _bmad-output/project-context.md#Technology Stack]
  • [Source: database/migrations/2026_02_27_103316_create_folders_table.php]
  • [Source: database/migrations/2026_03_01_144731_create_folder_invitations_table.php]
  • [Source: database/migrations/2026_03_01_144730_create_messages_table.php]

Dev Agent Record

Agent Model Used

Claude Opus 4.6

Debug Log References

  • SQLite does not support dropForeign('constraint_name') — switched to column array syntax dropForeign(['column_name']) for cross-driver compatibility
  • down() method required reordering: must drop index before renaming columns, then recreate index after rename (SQLite validates indexes during column rename)
  • Upgraded Docker Sail container from PHP 8.2 to PHP 8.4 to resolve PHPUnit 12.5 / sebastian/environment 8.0 incompatibility (typed class constants require PHP 8.3+)
  • Fixed phpunit.xml to use DB_CONNECTION=sqlite and DB_DATABASE=:memory: for test isolation

Completion Notes List

  • Created migration 2026_03_11_000001_rename_folders_to_declarations.php implementing full rename of tables, columns, indexes, and FK constraints
  • Migration uses column array syntax for dropForeign() ensuring SQLite (dev) and MySQL (prod) compatibility
  • up() follows exact order: drop FKs → drop index → rename columns → recreate index → rename tables → re-add FKs
  • down() fully reverses all changes: drop FKs → rename tables → drop index → rename columns → recreate index → re-add FKs
  • Added 6 Pest tests verifying table renames, column renames, composite index, and rollback in tests/Feature/Database/RenameFoldersToDeclarationsTest.php
  • All 6 migration tests pass; all non-Folder-related tests pass (40+ tests)
  • Folder-related tests (FolderTypeTest, MediaDownloadTest, FolderMentionTest, NotificationControllerTest) fail as expected — documented in Dev Notes, fixed in Story 0.2
  • Infrastructure fixes: PHP 8.2→8.4 in compose.yaml, WWWGROUP/WWWUSER in .env, phpunit.xml DB config

Senior Developer Review (AI)

Reviewer: Saad (via Claude Opus 4.6) on 2026-03-11

Findings (8 total): 3 High, 3 Medium, 2 Low

# Severity Issue Resolution
H1 HIGH up() dropIndex after column rename — fragile on SQLite Fixed: moved dropIndex before renameColumn
H2 HIGH Missing tests for AC #4 (indexes), AC #5 (reversibility) Fixed: added 2 new Pest tests
H3 HIGH Task 2.4 marked complete but no index test Fixed: composite index test added
M1 MEDIUM Infrastructure changes (compose.yaml, phpunit.xml) bundled in rename story Noted: these are prerequisites for running tests — documented as infra scope in File List
M2 MEDIUM .env modified but not in File List Fixed: added to File List
M3 MEDIUM Inconsistent FK drop/create approach between up() and down() Re-evaluated: both up() and down() use identical pattern (column array drops, explicit references for creates) — actually consistent, no fix needed
L1 LOW split_vat_folder_type.php has implicit dependency on migration order Fixed: added dependency comment to rename migration file
L2 LOW Test directory tests/Feature/Migration/ doesn't mirror controller structure Fixed: renamed to tests/Feature/Database/

Change Log

  • 2026-03-11: Code review follow-up — added migration dependency comment (L1), renamed test dir Migration/ → Database/ (L2), re-evaluated M3 as non-issue. All actionable findings resolved.
  • 2026-03-11: Code review — fixed migration step ordering (H1), added index and rollback tests (H2/H3), updated File List (M2). 3 HIGH and 1 MEDIUM fixed, 2 MEDIUM and 2 LOW noted.
  • 2026-03-11: Implemented Story 0.1 — database rename from folders to declarations terminology. Migration created, verified forward/backward, tests added. Infrastructure upgraded to PHP 8.4.

File List

  • database/migrations/2026_03_11_000001_rename_folders_to_declarations.php (new)
  • tests/Feature/Database/RenameFoldersToDeclarationsTest.php (new)
  • compose.yaml (modified — PHP 8.2 → 8.4)
  • phpunit.xml (modified — DB_CONNECTION=sqlite, DB_DATABASE=:memory:)
  • .env (modified — added WWWGROUP=1000, WWWUSER=1000)