diff --git a/app/Http/Controllers/Api/StatsController.php b/app/Http/Controllers/Api/StatsController.php new file mode 100644 index 0000000..71d8b3b --- /dev/null +++ b/app/Http/Controllers/Api/StatsController.php @@ -0,0 +1,49 @@ +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!", + ]); + } +} diff --git a/tests/Feature/Api/StatsControllerTest.php b/tests/Feature/Api/StatsControllerTest.php new file mode 100644 index 0000000..a11093c --- /dev/null +++ b/tests/Feature/Api/StatsControllerTest.php @@ -0,0 +1,64 @@ +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'); +});