release/0.4.0 #60
12 changed files with 154 additions and 470 deletions
|
|
@ -1,63 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AssetController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return response()->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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Pricing;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Pricing\AssetPrice;
|
||||
use App\Models\Tracker;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PricingController extends Controller
|
||||
{
|
||||
private ?Tracker $tracker;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @method static create(array $array)
|
||||
* @method static where(string $string, string $value)
|
||||
* @method static find(int $id)
|
||||
* @method static orderBy(string $string)
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $symbol
|
||||
* @property string|null $full_name
|
||||
*/
|
||||
class Asset extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'symbol',
|
||||
'full_name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'symbol' => '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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models\Pricing;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @method static latest(string $string)
|
||||
* @method static where(string $string, string $string1, string $date)
|
||||
* @method static updateOrCreate(string[] $array, float[] $array1)
|
||||
* @method static orderBy(string $string, string $string1)
|
||||
*
|
||||
* @property Carbon $date
|
||||
* @property float $price
|
||||
*/
|
||||
class AssetPrice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'asset_id',
|
||||
'date',
|
||||
'price',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date' => '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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('trackers', function (Blueprint $table): void {
|
||||
$table->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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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<Record<string, string>>({});
|
||||
|
||||
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 (
|
||||
<div className="w-full">
|
||||
<div className="space-y-4">
|
||||
<ComponentTitle>SET UP YOUR TRACKER</ComponentTitle>
|
||||
<p className="text-sm text-red-400/60 font-mono">
|
||||
[SYSTEM] What are you tracking?
|
||||
</p>
|
||||
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="label" className="text-red-400 font-mono text-xs uppercase tracking-wider">
|
||||
> Tracker Name
|
||||
</Label>
|
||||
<Input
|
||||
id="label"
|
||||
type="text"
|
||||
placeholder="My Portfolio"
|
||||
value={label}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="text-xs text-red-400/60 mt-1 font-mono">
|
||||
[REQUIRED] e.g. "My Portfolio", "Books Read", "KM Run"
|
||||
</p>
|
||||
<InputError message={errors.label} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="unit" className="text-red-400 font-mono text-xs uppercase tracking-wider">
|
||||
> Unit
|
||||
</Label>
|
||||
<Input
|
||||
id="unit"
|
||||
type="text"
|
||||
placeholder="shares"
|
||||
value={unit}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="text-xs text-red-400/60 mt-1 font-mono">
|
||||
[REQUIRED] e.g. "shares", "books", "km"
|
||||
</p>
|
||||
<InputError message={errors.unit} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={processing || !label || !unit}
|
||||
className="w-full bg-red-500 hover:bg-red-500 text-black font-mono text-sm font-bold border-red-500 rounded-none border-2 uppercase tracking-wider transition-all glow-red"
|
||||
>
|
||||
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||
[INITIALIZE]
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\AssetController;
|
||||
use App\Http\Controllers\CounterController;
|
||||
use App\Http\Controllers\Pricing\PricingController;
|
||||
use App\Http\Controllers\TrackerController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
|
|
@ -20,24 +18,8 @@
|
|||
Route::post('/tracker', [TrackerController::class, 'store'])->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';
|
||||
|
|
|
|||
76
tests/Feature/DropAssetsMigrationTest.php
Normal file
76
tests/Feature/DropAssetsMigrationTest.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DropAssetsMigrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
// Each require returns a fresh anonymous-class instance; there is no name to collide.
|
||||
private function migration(): object
|
||||
{
|
||||
return require __DIR__.'/../../database/migrations/2026_08_15_000003_drop_assets_and_pricing.php';
|
||||
}
|
||||
|
||||
public function test_the_migrated_schema_has_no_asset_or_price_columns(): void
|
||||
{
|
||||
$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_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'));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue