diff --git a/app/Models/Milestone.php b/app/Models/Milestone.php index 98efa38..08c7caf 100644 --- a/app/Models/Milestone.php +++ b/app/Models/Milestone.php @@ -11,6 +11,7 @@ class Milestone extends Model { protected $fillable = [ + 'tracker_id', 'target', 'description', ]; diff --git a/app/Models/Tracker.php b/app/Models/Tracker.php new file mode 100644 index 0000000..8b024cb --- /dev/null +++ b/app/Models/Tracker.php @@ -0,0 +1,25 @@ + 'boolean', + ]; + } +} diff --git a/database/migrations/2026_05_02_000001_create_trackers_table.php b/database/migrations/2026_05_02_000001_create_trackers_table.php new file mode 100644 index 0000000..6b3afcf --- /dev/null +++ b/database/migrations/2026_05_02_000001_create_trackers_table.php @@ -0,0 +1,50 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('asset_id')->nullable()->constrained()->nullOnDelete(); + $table->string('label'); + $table->string('unit'); + $table->boolean('price_tracking_enabled')->default(false); + $table->timestamps(); + }); + + // Migrate existing users: create one tracker per user from their current asset_id + price_tracking_enabled + DB::table('users')->orderBy('id')->each(function (object $user) { + DB::table('trackers')->insert([ + 'user_id' => $user->id, + 'asset_id' => $user->asset_id, + 'label' => 'Portfolio', + 'unit' => 'shares', + 'price_tracking_enabled' => $user->price_tracking_enabled ?? false, + 'created_at' => now(), + 'updated_at' => now(), + ]); + }); + } + + public function down(): void + { + // Restore asset_id and price_tracking_enabled back onto users before dropping trackers + DB::table('trackers')->orderBy('id')->each(function (object $tracker) { + DB::table('users') + ->where('id', $tracker->user_id) + ->update([ + 'asset_id' => $tracker->asset_id, + 'price_tracking_enabled' => $tracker->price_tracking_enabled, + ]); + }); + + Schema::dropIfExists('trackers'); + } +}; diff --git a/database/migrations/2026_05_02_000002_refactor_purchases_to_entries.php b/database/migrations/2026_05_02_000002_refactor_purchases_to_entries.php new file mode 100644 index 0000000..66a3163 --- /dev/null +++ b/database/migrations/2026_05_02_000002_refactor_purchases_to_entries.php @@ -0,0 +1,50 @@ +renameColumn('shares', 'quantity'); + $table->renameColumn('price_per_share', 'unit_price'); + + // Add tracker_id FK (nullable first so we can backfill) + $table->foreignId('tracker_id')->nullable()->after('id')->constrained()->cascadeOnDelete(); + }); + + // Backfill tracker_id: assign all entries to the first tracker (single-user app) + $trackerId = DB::table('trackers')->value('id'); + if ($trackerId) { + DB::table('entries')->update(['tracker_id' => $trackerId]); + } + + // Make tracker_id non-nullable now that it's backfilled + Schema::table('entries', function (Blueprint $table) { + $table->unsignedBigInteger('tracker_id')->nullable(false)->change(); + }); + } + + public function down(): void + { + Schema::table('entries', function (Blueprint $table) { + $table->dropForeign(['tracker_id']); + $table->dropColumn('tracker_id'); + }); + + Schema::table('entries', function (Blueprint $table) { + $table->renameColumn('unit_price', 'price_per_share'); + $table->renameColumn('quantity', 'shares'); + }); + + Schema::rename('entries', 'purchases'); + } +}; diff --git a/database/migrations/2026_05_02_000003_add_tracker_to_milestones_drop_from_users.php b/database/migrations/2026_05_02_000003_add_tracker_to_milestones_drop_from_users.php new file mode 100644 index 0000000..b1cbc2a --- /dev/null +++ b/database/migrations/2026_05_02_000003_add_tracker_to_milestones_drop_from_users.php @@ -0,0 +1,44 @@ +foreignId('tracker_id')->nullable()->after('id')->constrained()->cascadeOnDelete(); + }); + + // Backfill tracker_id on milestones + $trackerId = DB::table('trackers')->value('id'); + if ($trackerId) { + DB::table('milestones')->update(['tracker_id' => $trackerId]); + } + + Schema::table('milestones', function (Blueprint $table) { + $table->unsignedBigInteger('tracker_id')->nullable(false)->change(); + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['asset_id']); + $table->dropColumn(['asset_id', 'price_tracking_enabled']); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->foreignId('asset_id')->nullable()->constrained()->nullOnDelete(); + $table->boolean('price_tracking_enabled')->default(false); + }); + + Schema::table('milestones', function (Blueprint $table) { + $table->dropForeign(['tracker_id']); + $table->dropColumn('tracker_id'); + }); + } +}; diff --git a/tests/Feature/MilestoneTest.php b/tests/Feature/MilestoneTest.php index aa54402..1218dda 100644 --- a/tests/Feature/MilestoneTest.php +++ b/tests/Feature/MilestoneTest.php @@ -3,6 +3,8 @@ namespace Tests\Feature; use App\Models\Milestone; +use App\Models\Tracker; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -10,9 +12,23 @@ class MilestoneTest extends TestCase { use RefreshDatabase; + private function tracker(): Tracker + { + $user = User::factory()->create(); + + return Tracker::create([ + 'user_id' => $user->id, + 'label' => 'Test', + 'unit' => 'units', + ]); + } + public function test_can_create_milestone(): void { + $tracker = $this->tracker(); + $milestone = Milestone::create([ + 'tracker_id' => $tracker->id, 'target' => 1500, 'description' => 'First milestone', ]); @@ -28,9 +44,9 @@ public function test_can_create_milestone(): void public function test_can_fetch_milestones_via_api(): void { - // Create test milestones - Milestone::create(['target' => 1500, 'description' => 'First milestone']); - Milestone::create(['target' => 3000, 'description' => 'Second milestone']); + $tracker = $this->tracker(); + Milestone::create(['tracker_id' => $tracker->id, 'target' => 1500, 'description' => 'First milestone']); + Milestone::create(['tracker_id' => $tracker->id, 'target' => 3000, 'description' => 'Second milestone']); $response = $this->get('/milestones'); @@ -44,10 +60,10 @@ public function test_can_fetch_milestones_via_api(): void public function test_milestones_ordered_by_target(): void { - // Create milestones in reverse order - Milestone::create(['target' => 3000, 'description' => 'Third']); - Milestone::create(['target' => 1000, 'description' => 'First']); - Milestone::create(['target' => 2000, 'description' => 'Second']); + $tracker = $this->tracker(); + Milestone::create(['tracker_id' => $tracker->id, 'target' => 3000, 'description' => 'Third']); + Milestone::create(['tracker_id' => $tracker->id, 'target' => 1000, 'description' => 'First']); + Milestone::create(['tracker_id' => $tracker->id, 'target' => 2000, 'description' => 'Second']); $response = $this->get('/milestones');