🏆 Add highscore feature with API endpoints, validations, and tests

- **Added:** Endpoints for submitting highscores (`highscores.store`) and retrieving the leaderboard (`highscores.index`).
- **Implemented:** Validation rules via `StoreHighscoreRequest` to ensure highscore integrity.
- **Included:** `Highscore` model, migration, and factory for data handling and seeding.
- **Enhanced:** Comprehensive feature tests covering submission, updating, retrieval, and payload validation.
This commit is contained in:
HolgerHatGarKeineNode
2026-02-02 12:27:01 +01:00
parent 5f5a369ff9
commit 6dd04dee30
7 changed files with 422 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreHighscoreRequest;
use App\Models\Highscore;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class HighscoreController extends Controller
{
public function index(): JsonResponse
{
// npub1pt0kw36ue3w2g4haxq3wgm6a2fhtptmzsjlc2j2vphtcgle72qesgpjyc6
$highscores = Highscore::query()
->orderByDesc('satoshis')
->orderBy('achieved_at')
->get()
->map(fn (Highscore $highscore) => [
'npub' => $highscore->npub,
'name' => $highscore->name,
'satoshis' => $highscore->satoshis,
'blocks' => $highscore->blocks,
'datetime' => $highscore->achieved_at->toIso8601String(),
]);
return response()->json([
'data' => $highscores,
]);
}
public function store(StoreHighscoreRequest $request): JsonResponse
{
$validated = $request->validated();
$achievedAt = CarbonImmutable::parse($validated['datetime']);
$highscore = Highscore::query()->firstOrNew([
'npub' => $validated['npub'],
'achieved_at' => $achievedAt,
]);
$highscore->satoshis = (int) $validated['satoshis'];
$highscore->blocks = (int) $validated['blocks'];
if (array_key_exists('name', $validated)) {
$highscore->name = $validated['name'];
}
$highscore->save();
Log::info('Highscore submission received', [
'npub' => $highscore->npub,
'name' => $highscore->name,
'satoshis' => $highscore->satoshis,
'blocks' => $highscore->blocks,
'datetime' => $highscore->achieved_at->toIso8601String(),
]);
return response()->json([
'message' => 'Highscore received',
'data' => [
'npub' => $highscore->npub,
'name' => $highscore->name,
'satoshis' => $highscore->satoshis,
'blocks' => $highscore->blocks,
'datetime' => $highscore->achieved_at->toIso8601String(),
],
], 202);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreHighscoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
public function expectsJson(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'npub' => ['required', 'string', 'starts_with:npub1', 'max:100'],
'name' => ['nullable', 'string', 'max:255'],
'satoshis' => ['required', 'integer', 'min:0'],
'blocks' => ['required', 'integer', 'min:0'],
'datetime' => ['required', 'date'],
];
}
public function messages(): array
{
return [
'npub.required' => 'An npub is required to record the highscore.',
'npub.starts_with' => 'The npub must start with npub1.',
'name.string' => 'The name must be a valid string.',
'satoshis.required' => 'Please provide the earned satoshis amount.',
'satoshis.integer' => 'Satoshis must be a whole number.',
'blocks.required' => 'Please provide the number of blocks.',
'blocks.integer' => 'Blocks must be a whole number.',
'datetime.required' => 'Please provide when the score was achieved.',
'datetime.date' => 'The datetime value must be a valid date.',
];
}
}