feat: add GET /api/stats/searches endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
49
app/Http/Controllers/Api/StatsController.php
Normal file
49
app/Http/Controllers/Api/StatsController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Search;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class StatsController extends Controller
|
||||
{
|
||||
public function searches(Request $request): JsonResponse
|
||||
{
|
||||
$period = $request->input('period', 'week');
|
||||
$days = $period === 'month' ? 30 : 7;
|
||||
|
||||
$stats = Search::query()
|
||||
->where('searched_at', '>=', now()->subDays($days))
|
||||
->selectRaw('
|
||||
COUNT(*) as total_searches,
|
||||
COUNT(DISTINCT ip_hash) as unique_searchers,
|
||||
AVG(results_count) as avg_results,
|
||||
AVG(lowest_pence) as avg_lowest_pence,
|
||||
AVG(highest_pence) as avg_highest_pence,
|
||||
AVG(avg_pence) as avg_avg_pence
|
||||
')
|
||||
->first();
|
||||
|
||||
$totalSearches = (int) $stats->total_searches;
|
||||
$uniqueSearchers = (int) $stats->unique_searchers;
|
||||
$avgResults = $stats->avg_results !== null ? round((float) $stats->avg_results, 1) : 0.0;
|
||||
$avgLowestPrice = $stats->avg_lowest_pence !== null ? round((float) $stats->avg_lowest_pence / 100, 1) : 0.0;
|
||||
$avgHighestPrice = $stats->avg_highest_pence !== null ? round((float) $stats->avg_highest_pence / 100, 1) : 0.0;
|
||||
$avgPrice = $stats->avg_avg_pence !== null ? round((float) $stats->avg_avg_pence / 100, 1) : 0.0;
|
||||
|
||||
$periodLabel = $period === 'month' ? 'month' : 'week';
|
||||
|
||||
return response()->json([
|
||||
'total_searches' => $totalSearches,
|
||||
'unique_searchers' => $uniqueSearchers,
|
||||
'avg_results' => $avgResults,
|
||||
'avg_lowest_price' => $avgLowestPrice,
|
||||
'avg_highest_price' => $avgHighestPrice,
|
||||
'avg_price' => $avgPrice,
|
||||
'period' => $periodLabel,
|
||||
'message' => "Helped {$uniqueSearchers} drivers find cheaper fuel this {$periodLabel} so far!",
|
||||
]);
|
||||
}
|
||||
}
|
||||
64
tests/Feature/Api/StatsControllerTest.php
Normal file
64
tests/Feature/Api/StatsControllerTest.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Search;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('returns search stats for current week', function () {
|
||||
// 10 searches within the rolling 7 days (3 unique IPs)
|
||||
Search::factory()->count(5)->create([
|
||||
'searched_at' => now()->subDays(2),
|
||||
'ip_hash' => hash('sha256', '1.2.3.4'),
|
||||
'lowest_pence' => 13800,
|
||||
'highest_pence' => 14500,
|
||||
'avg_pence' => 14150.00,
|
||||
'results_count' => 20,
|
||||
]);
|
||||
Search::factory()->count(3)->create([
|
||||
'searched_at' => now()->subDays(4),
|
||||
'ip_hash' => hash('sha256', '5.6.7.8'),
|
||||
'lowest_pence' => 14200,
|
||||
'highest_pence' => 15000,
|
||||
'avg_pence' => 14600.00,
|
||||
'results_count' => 30,
|
||||
]);
|
||||
Search::factory()->count(2)->create([
|
||||
'searched_at' => now()->subDays(6),
|
||||
'ip_hash' => hash('sha256', '9.10.11.12'),
|
||||
'lowest_pence' => 13500,
|
||||
'highest_pence' => 14000,
|
||||
'avg_pence' => 13750.00,
|
||||
'results_count' => 10,
|
||||
]);
|
||||
// 5 searches outside the 7-day window
|
||||
Search::factory()->count(5)->create(['searched_at' => now()->subDays(10)]);
|
||||
|
||||
$this->getJson('/api/stats/searches?period=week')
|
||||
->assertOk()
|
||||
->assertJsonStructure(['total_searches', 'unique_searchers', 'avg_results', 'avg_lowest_price', 'avg_highest_price', 'avg_price', 'period', 'message'])
|
||||
->assertJsonPath('total_searches', 10)
|
||||
->assertJsonPath('unique_searchers', 3)
|
||||
->assertJsonPath('period', 'week');
|
||||
});
|
||||
|
||||
it('includes a human readable message', function () {
|
||||
Search::factory()->count(3)->create(['searched_at' => now()->subDay()]);
|
||||
|
||||
$response = $this->getJson('/api/stats/searches?period=week')->assertOk();
|
||||
|
||||
expect($response->json('message'))->toContain('drivers');
|
||||
});
|
||||
|
||||
it('returns zero stats when no searches exist', function () {
|
||||
$this->getJson('/api/stats/searches?period=week')
|
||||
->assertOk()
|
||||
->assertJsonPath('total_searches', 0)
|
||||
->assertJsonPath('unique_searchers', 0);
|
||||
});
|
||||
|
||||
it('defaults to week period when period param is omitted', function () {
|
||||
$this->getJson('/api/stats/searches')
|
||||
->assertOk()
|
||||
->assertJsonPath('period', 'week');
|
||||
});
|
||||
Reference in New Issue
Block a user