31 - Schema: create trackers, rename purchases to entries, add tracker_id to milestones

This commit is contained in:
myrmidex 2026-05-02 16:54:50 +02:00
parent 2f7a248e9f
commit b66e018b3a
6 changed files with 193 additions and 7 deletions

View file

@ -11,6 +11,7 @@
class Milestone extends Model class Milestone extends Model
{ {
protected $fillable = [ protected $fillable = [
'tracker_id',
'target', 'target',
'description', 'description',
]; ];

25
app/Models/Tracker.php Normal file
View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tracker extends Model
{
protected $fillable = [
'user_id',
'asset_id',
'label',
'unit',
'price_tracking_enabled',
];
protected function casts(): array
{
return [
'price_tracking_enabled' => 'boolean',
];
}
}

View file

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('trackers', function (Blueprint $table) {
$table->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');
}
};

View file

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Rename table
Schema::rename('purchases', 'entries');
Schema::table('entries', function (Blueprint $table) {
// Rename columns
$table->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');
}
};

View file

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('milestones', function (Blueprint $table) {
$table->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');
});
}
};

View file

@ -3,6 +3,8 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Models\Milestone; use App\Models\Milestone;
use App\Models\Tracker;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -10,9 +12,23 @@ class MilestoneTest extends TestCase
{ {
use RefreshDatabase; 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 public function test_can_create_milestone(): void
{ {
$tracker = $this->tracker();
$milestone = Milestone::create([ $milestone = Milestone::create([
'tracker_id' => $tracker->id,
'target' => 1500, 'target' => 1500,
'description' => 'First milestone', 'description' => 'First milestone',
]); ]);
@ -28,9 +44,9 @@ public function test_can_create_milestone(): void
public function test_can_fetch_milestones_via_api(): void public function test_can_fetch_milestones_via_api(): void
{ {
// Create test milestones $tracker = $this->tracker();
Milestone::create(['target' => 1500, 'description' => 'First milestone']); Milestone::create(['tracker_id' => $tracker->id, 'target' => 1500, 'description' => 'First milestone']);
Milestone::create(['target' => 3000, 'description' => 'Second milestone']); Milestone::create(['tracker_id' => $tracker->id, 'target' => 3000, 'description' => 'Second milestone']);
$response = $this->get('/milestones'); $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 public function test_milestones_ordered_by_target(): void
{ {
// Create milestones in reverse order $tracker = $this->tracker();
Milestone::create(['target' => 3000, 'description' => 'Third']); Milestone::create(['tracker_id' => $tracker->id, 'target' => 3000, 'description' => 'Third']);
Milestone::create(['target' => 1000, 'description' => 'First']); Milestone::create(['tracker_id' => $tracker->id, 'target' => 1000, 'description' => 'First']);
Milestone::create(['target' => 2000, 'description' => 'Second']); Milestone::create(['tracker_id' => $tracker->id, 'target' => 2000, 'description' => 'Second']);
$response = $this->get('/milestones'); $response = $this->get('/milestones');