285 lines
16 KiB
Markdown
285 lines
16 KiB
Markdown
|
|
# Story 0.1: Rename Folders to Declarations in Database
|
||
|
|
|
||
|
|
Status: done
|
||
|
|
|
||
|
|
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||
|
|
|
||
|
|
## 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
|
||
|
|
|
||
|
|
- [x] Task 1: Create the rename migration file (AC: #1, #2, #3, #4, #5, #6)
|
||
|
|
- [x] 1.1: Create migration `xxxx_rename_folders_to_declarations.php`
|
||
|
|
- [x] 1.2: In `up()` — Drop foreign key constraints: `messages_folder_id_foreign` and `folder_invitations_folder_id_foreign`
|
||
|
|
- [x] 1.3: In `up()` — Rename column `folder_id` → `declaration_id` in `messages` table
|
||
|
|
- [x] 1.4: In `up()` — Drop index `messages_folder_id_created_at_index`, recreate as `[declaration_id, created_at]`
|
||
|
|
- [x] 1.5: In `up()` — Rename column `folder_id` → `declaration_id` in `folder_invitations` table
|
||
|
|
- [x] 1.6: In `up()` — Rename table `folders` → `declarations`
|
||
|
|
- [x] 1.7: In `up()` — Rename table `folder_invitations` → `declaration_invitations`
|
||
|
|
- [x] 1.8: In `up()` — Re-add FK constraints using explicit `->constrained('declarations')->cascadeOnDelete()` on both tables
|
||
|
|
- [x] 1.9: Implement `down()` with explicit reverse steps (see Dev Notes)
|
||
|
|
- [x] Task 2: Verify migration executes successfully (AC: #1, #2, #3, #4, #6)
|
||
|
|
- [x] 2.1: Run `php artisan migrate` — confirm zero errors
|
||
|
|
- [x] 2.2: Verify tables: `php artisan tinker --execute="echo Schema::hasTable('declarations') ? 'OK' : 'FAIL';"`
|
||
|
|
- [x] 2.3: Verify columns: `php artisan tinker --execute="echo Schema::hasColumn('messages', 'declaration_id') ? 'OK' : 'FAIL';"`
|
||
|
|
- [x] 2.4: Verify composite index exists on `messages(declaration_id, created_at)`
|
||
|
|
- [x] 2.5: Verify existing seed/test data is intact after migration
|
||
|
|
- [x] Task 3: Verify rollback works (AC: #5)
|
||
|
|
- [x] 3.1: Run `php artisan migrate:rollback` — confirm tables revert to `folders`, `folder_invitations`
|
||
|
|
- [x] 3.2: Verify foreign key columns revert to `folder_id`
|
||
|
|
- [x] 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_id` → `declaration_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_id` → `folder_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
|
||
|
|
<?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)
|