Onboarding backend
This commit is contained in:
parent
2adb690d28
commit
17b7ea4aea
9 changed files with 287 additions and 20 deletions
97
app/Http/Controllers/AssetController.php
Normal file
97
app/Http/Controllers/AssetController.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AssetController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$assets = Asset::orderBy('symbol')->get();
|
||||
|
||||
return response()->json($assets);
|
||||
}
|
||||
|
||||
public function current(): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$asset = $user->asset;
|
||||
|
||||
return response()->json([
|
||||
'asset' => $asset,
|
||||
]);
|
||||
}
|
||||
|
||||
public function setCurrent(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'symbol' => 'required|string|max:10',
|
||||
'full_name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$asset = Asset::findOrCreateBySymbol(
|
||||
$validated['symbol'],
|
||||
$validated['full_name'] ?? null
|
||||
);
|
||||
|
||||
$user = Auth::user();
|
||||
$user->update(['asset_id' => $asset->id]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Asset set successfully!',
|
||||
'asset' => $asset,
|
||||
]);
|
||||
}
|
||||
|
||||
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');
|
||||
$currentPrice = $asset->currentPrice();
|
||||
|
||||
return response()->json([
|
||||
'asset' => $asset,
|
||||
'current_price' => $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);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,10 @@ class PricingController extends Controller
|
|||
{
|
||||
public function current(): JsonResponse
|
||||
{
|
||||
$price = AssetPrice::current();
|
||||
$user = auth()->user();
|
||||
$assetId = $user->asset_id;
|
||||
|
||||
$price = AssetPrice::current($assetId);
|
||||
|
||||
return response()->json([
|
||||
'current_price' => $price,
|
||||
|
|
@ -25,22 +28,34 @@ public function update(Request $request)
|
|||
'price' => 'required|numeric|min:0.0001',
|
||||
]);
|
||||
|
||||
$assetPrice = AssetPrice::updatePrice($validated['date'], $validated['price']);
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user->asset_id) {
|
||||
return back()->withErrors(['asset' => 'Please set an asset first.']);
|
||||
}
|
||||
|
||||
$assetPrice = AssetPrice::updatePrice($user->asset_id, $validated['date'], $validated['price']);
|
||||
|
||||
return back()->with('success', 'Asset price updated successfully!');
|
||||
}
|
||||
|
||||
public function history(Request $request): JsonResponse
|
||||
{
|
||||
$user = auth()->user();
|
||||
$assetId = $user->asset_id;
|
||||
|
||||
$limit = $request->get('limit', 30);
|
||||
$history = AssetPrice::history($limit);
|
||||
$history = AssetPrice::history($assetId, $limit);
|
||||
|
||||
return response()->json($history);
|
||||
}
|
||||
|
||||
public function forDate(Request $request, string $date): JsonResponse
|
||||
{
|
||||
$price = AssetPrice::forDate($date);
|
||||
$user = auth()->user();
|
||||
$assetId = $user->asset_id;
|
||||
|
||||
$price = AssetPrice::forDate($date, $assetId);
|
||||
|
||||
return response()->json([
|
||||
'date' => $date,
|
||||
|
|
|
|||
67
app/Models/Asset.php
Normal file
67
app/Models/Asset.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?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 users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::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;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
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;
|
||||
|
||||
/**
|
||||
|
|
@ -20,6 +21,7 @@ class AssetPrice extends Model
|
|||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'asset_id',
|
||||
'date',
|
||||
'price',
|
||||
];
|
||||
|
|
@ -29,32 +31,54 @@ class AssetPrice extends Model
|
|||
'price' => 'decimal:4',
|
||||
];
|
||||
|
||||
public static function current(): ?float
|
||||
public function asset(): BelongsTo
|
||||
{
|
||||
$latestPrice = static::latest('date')->first();
|
||||
return $this->belongsTo(\App\Models\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): ?float
|
||||
public static function forDate(string $date, int $assetId = null): ?float
|
||||
{
|
||||
$price = static::where('date', '<=', $date)
|
||||
->orderBy('date', 'desc')
|
||||
->first();
|
||||
$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(string $date, float $price): self
|
||||
public static function updatePrice(int $assetId, string $date, float $price): self
|
||||
{
|
||||
return static::updateOrCreate(
|
||||
['date' => $date],
|
||||
['asset_id' => $assetId, 'date' => $date],
|
||||
['price' => $price]
|
||||
);
|
||||
}
|
||||
|
||||
public static function history(int $limit = 30): Collection
|
||||
public static function history(int $assetId = null, int $limit = 30): Collection
|
||||
{
|
||||
return static::orderBy('date', 'desc')->limit($limit)->get();
|
||||
$query = static::orderBy('date', 'desc')->limit($limit);
|
||||
|
||||
if ($assetId) {
|
||||
$query->where('asset_id', $assetId);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
/**
|
||||
* @property int $asset_id
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
|
|
@ -21,6 +25,7 @@ class User extends Authenticatable
|
|||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'asset_id',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -33,11 +38,6 @@ class User extends Authenticatable
|
|||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -45,4 +45,27 @@ protected function casts(): array
|
|||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function asset(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Asset::class);
|
||||
}
|
||||
|
||||
public function hasCompletedOnboarding(): bool
|
||||
{
|
||||
// Check if user has asset, purchases, and milestones
|
||||
return $this->asset_id !== null
|
||||
&& $this->hasPurchases()
|
||||
&& $this->hasMilestones();
|
||||
}
|
||||
|
||||
public function hasPurchases(): bool
|
||||
{
|
||||
return \App\Models\Transactions\Purchase::totalShares() > 0;
|
||||
}
|
||||
|
||||
public function hasMilestones(): bool
|
||||
{
|
||||
return \App\Models\Milestone::count() > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,11 @@ public function up(): void
|
|||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->foreignId('asset_id')->nullable()->constrained()->onDelete('set null');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('asset_id');
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ public function up(): void
|
|||
{
|
||||
Schema::create('asset_prices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('asset_id')->constrained()->onDelete('cascade');
|
||||
$table->date('date');
|
||||
$table->decimal('price', 10, 4);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('date');
|
||||
$table->unique(['asset_id', 'date']);
|
||||
$table->index('asset_id');
|
||||
$table->index('date');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
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::create('assets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('symbol')->unique();
|
||||
$table->string('full_name')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('symbol');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('assets');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\AssetController;
|
||||
use App\Http\Controllers\Transactions\PurchaseController;
|
||||
use App\Http\Controllers\Pricing\PricingController;
|
||||
use App\Http\Controllers\Milestones\MilestoneController;
|
||||
|
|
@ -14,6 +15,16 @@
|
|||
return Inertia::render('dashboard');
|
||||
})->name('dashboard');
|
||||
|
||||
// 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('/current', [AssetController::class, 'current'])->name('current');
|
||||
Route::post('/set-current', [AssetController::class, 'setCurrent'])->name('set-current');
|
||||
Route::get('/search', [AssetController::class, 'search'])->name('search');
|
||||
Route::get('/{asset}', [AssetController::class, 'show'])->name('show');
|
||||
});
|
||||
|
||||
// Purchase routes
|
||||
Route::prefix('purchases')->name('purchases.')->group(function () {
|
||||
Route::get('/', [PurchaseController::class, 'index'])->name('index');
|
||||
|
|
|
|||
Loading…
Reference in a new issue