diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php new file mode 100644 index 0000000..34259c3 --- /dev/null +++ b/app/Http/Controllers/Api/UserController.php @@ -0,0 +1,62 @@ +json([ + 'preferred_fuel_type' => $request->user()->preferred_fuel_type, + 'postcode' => $request->user()->postcode, + ]); + } + + public function updatePreferences(Request $request): JsonResponse + { + $validated = $request->validate([ + 'preferred_fuel_type' => ['sometimes', Rule::in(['petrol', 'diesel', 'e5', 'b7_premium', 'b10', 'hvo'])], + 'postcode' => ['sometimes', 'string', 'max:8'], + ]); + + $request->user()->update($validated); + + return response()->json([ + 'preferred_fuel_type' => $request->user()->fresh()->preferred_fuel_type, + 'postcode' => $request->user()->fresh()->postcode, + ]); + } + + public function savedStations(Request $request): JsonResponse + { + $stations = $request->user()->savedStations()->get(); + + return response()->json(['data' => $stations]); + } + + public function saveStation(Request $request): JsonResponse + { + $validated = $request->validate([ + 'station_id' => ['required', 'string', 'max:64'], + ]); + + $request->user()->savedStations()->firstOrCreate([ + 'station_id' => $validated['station_id'], + ]); + + return response()->json(null, 201); + } + + public function removeStation(Request $request, string $stationId): Response + { + $request->user()->savedStations()->where('station_id', $stationId)->delete(); + + return response()->noContent(); + } +} diff --git a/app/Models/SavedStation.php b/app/Models/SavedStation.php new file mode 100644 index 0000000..97e180c --- /dev/null +++ b/app/Models/SavedStation.php @@ -0,0 +1,10 @@ +map(fn ($word) => Str::substr($word, 0, 1)) ->implode(''); } + + public function savedStations(): HasMany + { + return $this->hasMany(SavedStation::class); + } } diff --git a/database/migrations/2026_04_10_170504_add_preferred_fuel_type_to_users_table.php b/database/migrations/2026_04_10_170504_add_preferred_fuel_type_to_users_table.php new file mode 100644 index 0000000..add41b5 --- /dev/null +++ b/database/migrations/2026_04_10_170504_add_preferred_fuel_type_to_users_table.php @@ -0,0 +1,29 @@ +string('preferred_fuel_type', 20)->default('petrol')->after('postcode') + ->comment('User\'s default fuel type for homepage search'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropColumn('preferred_fuel_type'); + }); + } +}; diff --git a/database/migrations/2026_04_10_170504_create_saved_stations_table.php b/database/migrations/2026_04_10_170504_create_saved_stations_table.php new file mode 100644 index 0000000..39ccbe0 --- /dev/null +++ b/database/migrations/2026_04_10_170504_create_saved_stations_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('station_id', 64); + $table->timestamps(); + + $table->unique(['user_id', 'station_id']); + $table->index(['user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('saved_stations'); + } +}; diff --git a/routes/api.php b/routes/api.php index 0052f1c..1bbd4b0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Api\AuthController; use App\Http\Controllers\Api\PredictionController; use App\Http\Controllers\Api\StationController; use App\Http\Controllers\Api\StatsController; +use App\Http\Controllers\Api\UserController; use App\Http\Middleware\VerifyApiKey; use Illuminate\Support\Facades\Route; @@ -22,4 +23,11 @@ Route::middleware(['throttle:60,1', VerifyApiKey::class])->group(function (): vo Route::middleware('auth:sanctum')->group(function (): void { Route::get('/auth/me', [AuthController::class, 'me']); Route::post('/auth/logout', [AuthController::class, 'logout']); + + // User dashboard endpoints + Route::get('/user/preferences', [UserController::class, 'preferences']); + Route::put('/user/preferences', [UserController::class, 'updatePreferences']); + Route::get('/user/saved-stations', [UserController::class, 'savedStations']); + Route::post('/user/saved-stations', [UserController::class, 'saveStation']); + Route::delete('/user/saved-stations/{stationId}', [UserController::class, 'removeStation']); }); diff --git a/tests/Feature/Api/UserControllerTest.php b/tests/Feature/Api/UserControllerTest.php new file mode 100644 index 0000000..a8180ce --- /dev/null +++ b/tests/Feature/Api/UserControllerTest.php @@ -0,0 +1,67 @@ +create(['preferred_fuel_type' => 'diesel']); + Sanctum::actingAs($user); + + $this->getJson('/api/user/preferences') + ->assertOk() + ->assertJsonFragment(['preferred_fuel_type' => 'diesel']); +}); + +it('updates user preferences', function (): void { + $user = User::factory()->create(['preferred_fuel_type' => 'petrol']); + Sanctum::actingAs($user); + + $this->putJson('/api/user/preferences', ['preferred_fuel_type' => 'diesel']) + ->assertOk() + ->assertJsonFragment(['preferred_fuel_type' => 'diesel']); + + expect($user->fresh()->preferred_fuel_type)->toBe('diesel'); +}); + +it('rejects invalid fuel type in preferences update', function (): void { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->putJson('/api/user/preferences', ['preferred_fuel_type' => 'aviation_fuel']) + ->assertUnprocessable(); +}); + +it('returns saved stations for authenticated user', function (): void { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->getJson('/api/user/saved-stations') + ->assertOk() + ->assertJsonStructure(['data']); +}); + +it('saves a station', function (): void { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->postJson('/api/user/saved-stations', ['station_id' => 'abc123']) + ->assertCreated(); + + expect($user->savedStations()->where('station_id', 'abc123')->exists())->toBeTrue(); +}); + +it('removes a saved station', function (): void { + $user = User::factory()->create(); + $user->savedStations()->create(['station_id' => 'abc123']); + Sanctum::actingAs($user); + + $this->deleteJson('/api/user/saved-stations/abc123') + ->assertNoContent(); + + expect($user->savedStations()->where('station_id', 'abc123')->exists())->toBeFalse(); +}); + +it('rejects unauthenticated requests to user endpoints', function (): void { + $this->getJson('/api/user/preferences')->assertUnauthorized(); + $this->getJson('/api/user/saved-stations')->assertUnauthorized(); +});