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
|
||||
|
||||
use App\Http\Controllers\Transactions\PurchaseController;
|
||||
use App\Http\Controllers\Pricing\PricingController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
|
||||
|
|
@ -20,5 +21,13 @@
|
|||
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__.'/auth.php';
|
||||
|
|
|
|||
Loading…
Reference in a new issue