Pricing
This commit is contained in:
parent
52f8ae2fd1
commit
12c377c92c
5 changed files with 240 additions and 0 deletions
54
app/Http/Controllers/Pricing/PricingController.php
Normal file
54
app/Http/Controllers/Pricing/PricingController.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Pricing;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Pricing\AssetPrice;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PricingController extends Controller
|
||||||
|
{
|
||||||
|
public function current(): JsonResponse
|
||||||
|
{
|
||||||
|
$price = AssetPrice::current();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'current_price' => $price,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'date' => 'required|date|before_or_equal:today',
|
||||||
|
'price' => 'required|numeric|min:0.0001',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$assetPrice = AssetPrice::updatePrice($validated['date'], $validated['price']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Asset price updated successfully!',
|
||||||
|
'data' => $assetPrice,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function history(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$limit = $request->get('limit', 30);
|
||||||
|
$history = AssetPrice::history($limit);
|
||||||
|
|
||||||
|
return response()->json($history);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forDate(Request $request, string $date): JsonResponse
|
||||||
|
{
|
||||||
|
$price = AssetPrice::forDate($date);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'date' => $date,
|
||||||
|
'price' => $price,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
app/Models/Pricing/AssetPrice.php
Normal file
60
app/Models/Pricing/AssetPrice.php
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Pricing;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
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 = [
|
||||||
|
'date',
|
||||||
|
'price',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'date' => 'date',
|
||||||
|
'price' => 'decimal:4',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function current(): ?float
|
||||||
|
{
|
||||||
|
$latestPrice = static::latest('date')->first();
|
||||||
|
|
||||||
|
return $latestPrice ? $latestPrice->price : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function forDate(string $date): ?float
|
||||||
|
{
|
||||||
|
$price = static::where('date', '<=', $date)
|
||||||
|
->orderBy('date', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $price ? $price->price : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function updatePrice(string $date, float $price): self
|
||||||
|
{
|
||||||
|
return static::updateOrCreate(
|
||||||
|
['date' => $date],
|
||||||
|
['price' => $price]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function history(int $limit = 30): Collection
|
||||||
|
{
|
||||||
|
return static::orderBy('date', 'desc')->limit($limit)->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?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('asset_prices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->date('date');
|
||||||
|
$table->decimal('price', 10, 4);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique('date');
|
||||||
|
$table->index('date');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('asset_prices');
|
||||||
|
}
|
||||||
|
};
|
||||||
91
resources/js/components/Pricing/UpdatePriceForm.tsx
Normal file
91
resources/js/components/Pricing/UpdatePriceForm.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import InputError from '@/components/InputError';
|
||||||
|
import { useForm } from '@inertiajs/react';
|
||||||
|
import { LoaderCircle } from 'lucide-react';
|
||||||
|
import { FormEventHandler } from 'react';
|
||||||
|
|
||||||
|
interface PriceUpdateFormData {
|
||||||
|
date: string;
|
||||||
|
price: string;
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdatePriceFormProps {
|
||||||
|
currentPrice?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UpdatePriceForm({ currentPrice, className }: UpdatePriceFormProps) {
|
||||||
|
const { data, setData, post, processing, errors } = useForm<PriceUpdateFormData>({
|
||||||
|
date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format
|
||||||
|
price: currentPrice?.toString() || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit: FormEventHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
post(route('pricing.update'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
// Keep the date, reset only price if needed
|
||||||
|
// User might want to update same day multiple times
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Update Asset Price</CardTitle>
|
||||||
|
{currentPrice && (
|
||||||
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
Current price: €{currentPrice.toFixed(4)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={submit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="date">Price Date</Label>
|
||||||
|
<Input
|
||||||
|
id="date"
|
||||||
|
type="date"
|
||||||
|
value={data.date}
|
||||||
|
onChange={(e) => setData('date', e.target.value)}
|
||||||
|
max={new Date().toISOString().split('T')[0]}
|
||||||
|
/>
|
||||||
|
<InputError message={errors.date} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="price">Asset Price (€)</Label>
|
||||||
|
<Input
|
||||||
|
id="price"
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
min="0"
|
||||||
|
placeholder="123.4567"
|
||||||
|
value={data.price}
|
||||||
|
onChange={(e) => setData('price', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-neutral-500 mt-1">
|
||||||
|
Price per unit/share of the asset
|
||||||
|
</p>
|
||||||
|
<InputError message={errors.price} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={processing}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{processing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
Update Price
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Transactions\PurchaseController;
|
use App\Http\Controllers\Transactions\PurchaseController;
|
||||||
|
use App\Http\Controllers\Pricing\PricingController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
|
@ -20,5 +21,13 @@
|
||||||
Route::delete('/{purchase}', [PurchaseController::class, 'destroy'])->name('destroy');
|
Route::delete('/{purchase}', [PurchaseController::class, 'destroy'])->name('destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue