31 - Schema: create trackers, rename purchases to entries, add tracker_id to milestones
This commit is contained in:
parent
2f7a248e9f
commit
b66e018b3a
6 changed files with 193 additions and 7 deletions
|
|
@ -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
25
app/Models/Tracker.php
Normal 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue