From 671cdb66ba06a093db18a9bf59ec0bd078b1d257 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 21:34:19 +0200 Subject: [PATCH] 48 - Remove asset and price-tracking subsystem --- app/Http/Controllers/AssetController.php | 63 ---------- .../Controllers/Pricing/PricingController.php | 66 ---------- app/Http/Controllers/TrackerController.php | 51 +------- app/Models/Asset.php | 63 ---------- app/Models/Pricing/AssetPrice.php | 86 ------------- app/Models/Tracker.php | 8 -- ...6_08_15_000003_drop_assets_and_pricing.php | 51 ++++++++ .../Onboarding/CreateTrackerStep.tsx | 113 ------------------ resources/js/types/domain.ts | 9 +- routes/web.php | 18 --- tests/Feature/DropAssetsMigrationTest.php | 76 ++++++++++++ tests/Feature/TrackerTest.php | 20 ++++ 12 files changed, 154 insertions(+), 470 deletions(-) delete mode 100644 app/Http/Controllers/AssetController.php delete mode 100644 app/Http/Controllers/Pricing/PricingController.php delete mode 100644 app/Models/Asset.php delete mode 100644 app/Models/Pricing/AssetPrice.php create mode 100644 database/migrations/2026_08_15_000003_drop_assets_and_pricing.php delete mode 100644 resources/js/components/Onboarding/CreateTrackerStep.tsx create mode 100644 tests/Feature/DropAssetsMigrationTest.php diff --git a/app/Http/Controllers/AssetController.php b/app/Http/Controllers/AssetController.php deleted file mode 100644 index b8c87cf..0000000 --- a/app/Http/Controllers/AssetController.php +++ /dev/null @@ -1,63 +0,0 @@ -json(Asset::orderBy('symbol')->get()); - } - - public function store(Request $request): JsonResponse - { - $validated = $request->validate([ - 'symbol' => 'required|string|max:10|unique:assets,symbol', - 'full_name' => 'nullable|string|max:255', - ]); - - $asset = Asset::create([ - 'symbol' => strtoupper($validated['symbol']), - 'full_name' => $validated['full_name'], - ]); - - return response()->json([ - 'success' => true, - 'message' => 'Asset created successfully!', - 'asset' => $asset, - ], 201); - } - - public function show(Asset $asset): JsonResponse - { - $asset->load('assetPrices'); - - return response()->json([ - 'asset' => $asset, - 'current_price' => $asset->currentPrice(), - ]); - } - - public function search(Request $request): JsonResponse - { - $query = $request->get('q'); - - if (! $query) { - return response()->json([]); - } - - $assets = Asset::where('symbol', 'like', "%{$query}%") - ->orWhere('full_name', 'like', "%{$query}%") - ->orderBy('symbol') - ->limit(10) - ->get(); - - return response()->json($assets); - } -} diff --git a/app/Http/Controllers/Pricing/PricingController.php b/app/Http/Controllers/Pricing/PricingController.php deleted file mode 100644 index e7a2ce3..0000000 --- a/app/Http/Controllers/Pricing/PricingController.php +++ /dev/null @@ -1,66 +0,0 @@ -tracker = User::default()->tracker; - } - - public function current(): JsonResponse - { - return response()->json([ - 'current_price' => AssetPrice::current($this->tracker?->asset_id), - ]); - } - - public function update(Request $request) - { - $validated = $request->validate([ - 'date' => 'required|date|before_or_equal:today', - 'price' => 'required|numeric|min:0.0001', - ]); - - if (! $this->tracker?->asset_id) { - return back()->withErrors(['asset' => 'Please set an asset first.']); - } - - AssetPrice::updatePrice($this->tracker->asset_id, $validated['date'], $validated['price']); - - if (! $this->tracker->price_tracking_enabled) { - $this->tracker->update(['price_tracking_enabled' => true]); - } - - return back()->with('success', 'Asset price updated successfully!'); - } - - public function history(Request $request): JsonResponse - { - $limit = min(max(1, $request->integer('limit', 30)), 365); - - return response()->json(AssetPrice::history($this->tracker?->asset_id, $limit)); - } - - public function forDate(Request $request, string $date): JsonResponse - { - validator(['date' => $date], ['date' => 'required|date_format:Y-m-d'])->validate(); - - return response()->json([ - 'date' => $date, - 'price' => AssetPrice::forDate($date, $this->tracker?->asset_id), - ]); - } -} diff --git a/app/Http/Controllers/TrackerController.php b/app/Http/Controllers/TrackerController.php index 4631686..09fa31e 100644 --- a/app/Http/Controllers/TrackerController.php +++ b/app/Http/Controllers/TrackerController.php @@ -4,10 +4,8 @@ namespace App\Http\Controllers; -use App\Models\Asset; use App\Models\User; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class TrackerController extends Controller @@ -20,7 +18,7 @@ public function show(): JsonResponse return response()->json(['exists' => false]); } - return response()->json(['exists' => true, 'tracker' => $tracker->load('asset')]); + return response()->json(['exists' => true, 'tracker' => $tracker]); } public function store(Request $request): JsonResponse @@ -28,9 +26,6 @@ public function store(Request $request): JsonResponse $validated = $request->validate([ 'label' => 'sometimes|string|max:255', 'unit' => 'sometimes|string|max:50', - 'price_tracking_enabled' => 'boolean', - 'symbol' => 'nullable|string|max:10', - 'full_name' => 'nullable|string|max:255', ]); $user = User::default(); @@ -39,63 +34,29 @@ public function store(Request $request): JsonResponse return response()->json(['error' => 'Tracker already exists.'], 409); } - $assetId = null; - if (! empty($validated['symbol'])) { - $asset = Asset::findOrCreateBySymbol($validated['symbol'], $validated['full_name'] ?? null); - $assetId = $asset->id; - } - $tracker = $user->tracker()->create([ 'label' => $validated['label'] ?? 'Counter', 'unit' => $validated['unit'] ?? 'units', - 'price_tracking_enabled' => $validated['price_tracking_enabled'] ?? false, - 'asset_id' => $assetId, ]); - return response()->json($tracker->load('asset'), 201); + return response()->json($tracker, 201); } - public function update(Request $request): RedirectResponse|JsonResponse + public function update(Request $request): JsonResponse { $validated = $request->validate([ 'label' => 'sometimes|string|max:255', 'unit' => 'sometimes|string|max:50', - 'price_tracking_enabled' => 'sometimes|boolean', - 'symbol' => 'nullable|string|max:10', - 'full_name' => 'nullable|string|max:255', ]); $tracker = User::default()->tracker; if (! $tracker) { - return back()->withErrors(['tracker' => 'No tracker found.']); + return response()->json(['error' => 'No counter found.'], 404); } - if (array_key_exists('symbol', $validated)) { - if ($validated['symbol']) { - $asset = Asset::findOrCreateBySymbol($validated['symbol'], $validated['full_name'] ?? null); - $tracker->asset_id = $asset->id; - } else { - $tracker->asset_id = null; - } - } + $tracker->update($validated); - $update = []; - if (isset($validated['label'])) { - $update['label'] = $validated['label']; - } - if (isset($validated['unit'])) { - $update['unit'] = $validated['unit']; - } - if (array_key_exists('price_tracking_enabled', $validated)) { - $update['price_tracking_enabled'] = $validated['price_tracking_enabled']; - } - if (array_key_exists('symbol', $validated)) { - $update['asset_id'] = $tracker->asset_id; - } - - $tracker->update($update); - - return back(); + return response()->json($tracker); } } diff --git a/app/Models/Asset.php b/app/Models/Asset.php deleted file mode 100644 index 719a8b5..0000000 --- a/app/Models/Asset.php +++ /dev/null @@ -1,63 +0,0 @@ - 'string', - 'full_name' => 'string', - ]; - - public function assetPrices(): HasMany - { - return $this->hasMany(Pricing\AssetPrice::class); - } - - public function currentPrice(): ?float - { - $latestPrice = $this->assetPrices()->latest('date')->first(); - - return $latestPrice ? $latestPrice->price : null; - } - - public static function findBySymbol(string $symbol): ?self - { - return static::where('symbol', strtoupper($symbol))->first(); - } - - public static function findOrCreateBySymbol(string $symbol, ?string $fullName = null): self - { - $asset = static::findBySymbol($symbol); - - if (! $asset) { - $asset = static::create([ - 'symbol' => strtoupper($symbol), - 'full_name' => $fullName, - ]); - } - - return $asset; - } -} diff --git a/app/Models/Pricing/AssetPrice.php b/app/Models/Pricing/AssetPrice.php deleted file mode 100644 index 9cbcebd..0000000 --- a/app/Models/Pricing/AssetPrice.php +++ /dev/null @@ -1,86 +0,0 @@ - 'date', - 'price' => 'decimal:4', - ]; - - public function asset(): BelongsTo - { - return $this->belongsTo(Asset::class); - } - - public static function current(?int $assetId = null): ?float - { - $query = static::latest('date'); - - if ($assetId) { - $query->where('asset_id', $assetId); - } - - $latestPrice = $query->first(); - - return $latestPrice ? $latestPrice->price : null; - } - - public static function forDate(string $date, ?int $assetId = null): ?float - { - $query = static::where('date', '<=', $date) - ->orderBy('date', 'desc'); - - if ($assetId) { - $query->where('asset_id', $assetId); - } - - $price = $query->first(); - - return $price ? $price->price : null; - } - - public static function updatePrice(int $assetId, string $date, float $price): self - { - return static::updateOrCreate( - ['asset_id' => $assetId, 'date' => $date], - ['price' => $price] - ); - } - - public static function history(?int $assetId = null, int $limit = 30): Collection - { - $query = static::orderBy('date', 'desc')->limit($limit); - - if ($assetId) { - $query->where('asset_id', $assetId); - } - - return $query->get(); - } -} diff --git a/app/Models/Tracker.php b/app/Models/Tracker.php index 3fbdb2d..010eee9 100644 --- a/app/Models/Tracker.php +++ b/app/Models/Tracker.php @@ -16,18 +16,15 @@ class Tracker extends Model protected $fillable = [ 'user_id', - 'asset_id', 'label', 'unit', 'count', - 'price_tracking_enabled', ]; protected function casts(): array { return [ 'count' => 'integer', - 'price_tracking_enabled' => 'boolean', ]; } @@ -35,9 +32,4 @@ public function user(): BelongsTo { return $this->belongsTo(User::class); } - - public function asset(): BelongsTo - { - return $this->belongsTo(Asset::class); - } } diff --git a/database/migrations/2026_08_15_000003_drop_assets_and_pricing.php b/database/migrations/2026_08_15_000003_drop_assets_and_pricing.php new file mode 100644 index 0000000..ae3cd5f --- /dev/null +++ b/database/migrations/2026_08_15_000003_drop_assets_and_pricing.php @@ -0,0 +1,51 @@ +dropForeign(['asset_id']); + $table->dropColumn(['asset_id', 'price_tracking_enabled']); + }); + + Schema::dropIfExists('asset_prices'); + Schema::dropIfExists('assets'); + } + + // Lossy by design: recorded symbols and prices cannot be recovered. + public function down(): void + { + Schema::create('assets', function (Blueprint $table): void { + $table->id(); + $table->string('symbol')->unique(); + $table->string('full_name')->nullable(); + $table->timestamps(); + + $table->index('symbol'); + }); + + Schema::create('asset_prices', function (Blueprint $table): void { + $table->id(); + $table->foreignId('asset_id')->constrained()->onDelete('cascade'); + $table->date('date'); + $table->decimal('price', 10, 4); + $table->timestamps(); + + $table->unique(['asset_id', 'date']); + $table->index('asset_id'); + $table->index('date'); + }); + + Schema::table('trackers', function (Blueprint $table): void { + $table->foreignId('asset_id')->nullable()->after('user_id')->constrained()->nullOnDelete(); + $table->boolean('price_tracking_enabled')->default(false); + }); + } +}; diff --git a/resources/js/components/Onboarding/CreateTrackerStep.tsx b/resources/js/components/Onboarding/CreateTrackerStep.tsx deleted file mode 100644 index 90491e8..0000000 --- a/resources/js/components/Onboarding/CreateTrackerStep.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import InputError from '@/components/InputError'; -import { LoaderCircle } from 'lucide-react'; -import { FormEventHandler, useState } from 'react'; -import ComponentTitle from '@/components/ui/ComponentTitle'; - -interface CreateTrackerStepProps { - onSuccess: () => void; -} - -export default function CreateTrackerStep({ onSuccess }: CreateTrackerStepProps) { - const [label, setLabel] = useState(''); - const [unit, setUnit] = useState(''); - const [processing, setProcessing] = useState(false); - const [errors, setErrors] = useState>({}); - - const submit: FormEventHandler = async (e) => { - e.preventDefault(); - setProcessing(true); - setErrors({}); - - try { - const response = await fetch(route('tracker.store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '', - 'Accept': 'application/json', - }, - body: JSON.stringify({ - label, - unit, - price_tracking_enabled: 0, - }), - }); - - if (response.ok || response.status === 201 || response.status === 409) { - onSuccess(); - } else { - const data = await response.json(); - if (data.errors) { - setErrors(data.errors); - } else if (data.message) { - setErrors({ label: data.message }); - } - } - } catch { - setErrors({ label: 'Something went wrong. Please try again.' }); - } finally { - setProcessing(false); - } - }; - - return ( -
-
- SET UP YOUR TRACKER -

- [SYSTEM] What are you tracking? -

- -
-
- - setLabel(e.target.value)} - className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none focus:shadow-[0_0_10px_rgba(239,68,68,0.5)] placeholder:text-red-400/40 transition-all" - /> -

- [REQUIRED] e.g. "My Portfolio", "Books Read", "KM Run" -

- -
- -
- - setUnit(e.target.value)} - className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none focus:shadow-[0_0_10px_rgba(239,68,68,0.5)] placeholder:text-red-400/40 transition-all" - /> -

- [REQUIRED] e.g. "shares", "books", "km" -

- -
- - -
-
-
- ); -} diff --git a/resources/js/types/domain.ts b/resources/js/types/domain.ts index 6acb030..f77eb91 100644 --- a/resources/js/types/domain.ts +++ b/resources/js/types/domain.ts @@ -1,13 +1,6 @@ -export interface TrackerAsset { - id: number; - symbol: string; - full_name: string | null; -} - export interface Tracker { id: number; label: string; unit: string; - price_tracking_enabled: boolean; - asset: TrackerAsset | null; + count: number; } diff --git a/routes/web.php b/routes/web.php index 7ff9575..5a282c1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,8 +1,6 @@ name('tracker.store'); Route::patch('/tracker', [TrackerController::class, 'update'])->name('tracker.update'); -// Asset routes -Route::prefix('assets')->name('assets.')->group(function () { - Route::get('/', [AssetController::class, 'index'])->name('index'); - Route::post('/', [AssetController::class, 'store'])->name('store'); - Route::get('/search', [AssetController::class, 'search'])->name('search'); - Route::get('/{asset}', [AssetController::class, 'show'])->name('show'); -}); - // Counter routes Route::post('/increment', [CounterController::class, 'increment'])->name('counter.increment'); Route::patch('/count', [CounterController::class, 'update'])->name('counter.update'); -// Pricing routes -Route::prefix('pricing')->name('pricing.')->group(function () { - Route::get('/current', [PricingController::class, 'current'])->name('current'); - Route::post('/update', [PricingController::class, 'update'])->name('update'); - Route::get('/history', [PricingController::class, 'history'])->name('history'); - Route::get('/date/{date}', [PricingController::class, 'forDate'])->name('for-date'); -}); - require __DIR__.'/auth.php'; diff --git a/tests/Feature/DropAssetsMigrationTest.php b/tests/Feature/DropAssetsMigrationTest.php new file mode 100644 index 0000000..8b4fdfe --- /dev/null +++ b/tests/Feature/DropAssetsMigrationTest.php @@ -0,0 +1,76 @@ +assertFalse(Schema::hasTable('assets')); + $this->assertFalse(Schema::hasTable('asset_prices')); + $this->assertFalse(Schema::hasColumn('trackers', 'asset_id')); + $this->assertFalse(Schema::hasColumn('trackers', 'price_tracking_enabled')); + } + + public function test_down_restores_the_tables_and_columns(): void + { + $this->migration()->down(); + + $this->assertTrue(Schema::hasTable('assets')); + $this->assertTrue(Schema::hasTable('asset_prices')); + $this->assertTrue(Schema::hasColumn('trackers', 'asset_id')); + $this->assertTrue(Schema::hasColumn('trackers', 'price_tracking_enabled')); + } + + public function test_up_drops_them_again_respecting_foreign_keys(): void + { + $this->migration()->down(); + $this->migration()->up(); + + $this->assertFalse(Schema::hasTable('assets')); + $this->assertFalse(Schema::hasTable('asset_prices')); + $this->assertFalse(Schema::hasColumn('trackers', 'asset_id')); + $this->assertFalse(Schema::hasColumn('trackers', 'price_tracking_enabled')); + } + + public function test_dropping_assets_does_not_disturb_the_counter(): void + { + $this->migration()->down(); + + $userId = DB::table('users')->insertGetId([ + 'name' => 'Test', + 'email' => 'assets@example.test', + 'password' => 'x', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $trackerId = DB::table('trackers')->insertGetId([ + 'user_id' => $userId, + 'label' => 'Counter', + 'unit' => 'units', + 'count' => 31, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->migration()->up(); + + $this->assertSame(31, (int) DB::table('trackers')->where('id', $trackerId)->value('count')); + } +} diff --git a/tests/Feature/TrackerTest.php b/tests/Feature/TrackerTest.php index 5a0673c..b35229c 100644 --- a/tests/Feature/TrackerTest.php +++ b/tests/Feature/TrackerTest.php @@ -58,4 +58,24 @@ public function test_creating_a_second_tracker_conflicts(): void $this->assertSame(1, Tracker::count()); } + + public function test_label_and_unit_can_be_updated(): void + { + $tracker = Tracker::factory()->create(['count' => 4]); + + $this->patchJson('/tracker', ['label' => 'Books', 'unit' => 'books']) + ->assertOk() + ->assertJson(['label' => 'Books', 'unit' => 'books']); + + $tracker->refresh(); + + $this->assertSame('Books', $tracker->label); + $this->assertSame('books', $tracker->unit); + $this->assertSame(4, $tracker->count, 'updating the label must not disturb the count'); + } + + public function test_update_returns_404_without_a_tracker(): void + { + $this->patchJson('/tracker', ['label' => 'Books'])->assertNotFound(); + } }