Passed
Push — master ( ee4c3d...022e93 )
by Darko
09:16
created

UserStatsService::getApiHitsPerHour()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
namespace App\Services;
4
5
use App\Models\User;
6
use App\Models\UserActivityStat;
7
use App\Models\UserDownload;
8
use App\Models\UserRequest;
9
use Carbon\Carbon;
10
use Illuminate\Support\Facades\DB;
11
12
class UserStatsService
13
{
14
    /**
15
     * Get user statistics by role
16
     */
17
    public function getUsersByRole(): array
18
    {
19
        $usersByRole = User::query()
20
            ->join('roles', 'users.roles_id', '=', 'roles.id')
21
            ->select('roles.name as role_name', DB::raw('COUNT(users.id) as count'))
22
            ->whereNull('users.deleted_at')
23
            ->groupBy('roles.id', 'roles.name')
24
            ->get();
25
26
        return $usersByRole->map(function ($item) {
27
            return [
28
                'role' => $item->role_name,
29
                'count' => $item->count,
30
            ];
31
        })->toArray();
32
    }
33
34
    /**
35
     * Get downloads per day for the last N days
36
     * Uses aggregated stats from user_activity_stats table for dates older than 2 days
37
     * Uses live data from user_downloads table for recent days
38
     */
39
    public function getDownloadsPerDay(int $days = 7): array
40
    {
41
        $startDate = Carbon::now()->subDays($days - 1)->startOfDay();
42
        $twoDaysAgo = Carbon::now()->subDays(2)->startOfDay();
43
44
        $result = [];
45
46
        // For historical data (older than 2 days), use aggregated stats
47
        if ($days > 2) {
48
            $historicalStartDate = $startDate->format('Y-m-d');
49
            $historicalEndDate = $twoDaysAgo->copy()->subDay()->format('Y-m-d');
50
51
            $historicalStats = UserActivityStat::query()
52
                ->select('stat_date', 'downloads_count')
53
                ->where('stat_date', '>=', $historicalStartDate)
54
                ->where('stat_date', '<=', $historicalEndDate)
55
                ->orderBy('stat_date', 'asc')
0 ignored issues
show
Bug introduced by
'stat_date' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

55
                ->orderBy(/** @scrutinizer ignore-type */ 'stat_date', 'asc')
Loading history...
56
                ->get()
57
                ->keyBy('stat_date');
58
59
            // Add historical data
60
            $currentDate = $startDate->copy();
61
            while ($currentDate->lt($twoDaysAgo)) {
62
                $dateStr = $currentDate->format('Y-m-d');
63
                $stat = $historicalStats->get($dateStr);
64
                $result[] = [
65
                    'date' => $currentDate->format('M d'),
66
                    'count' => $stat ? $stat->downloads_count : 0,
67
                ];
68
                $currentDate->addDay();
69
            }
70
        }
71
72
        // For recent data (last 2 days), use live data from user_downloads
73
        $downloads = UserDownload::query()
74
            ->select(DB::raw('DATE(timestamp) as date'), DB::raw('COUNT(*) as count'))
75
            ->where('timestamp', '>=', $twoDaysAgo)
76
            ->groupBy(DB::raw('DATE(timestamp)'))
77
            ->orderBy('date', 'asc')
78
            ->get()
79
            ->keyBy('date');
80
81
        // Add recent data
82
        $currentDate = $twoDaysAgo->copy();
83
        $now = Carbon::now();
84
        while ($currentDate->lte($now)) {
85
            $dateStr = $currentDate->format('Y-m-d');
86
            $found = $downloads->get($dateStr);
87
            $result[] = [
88
                'date' => $currentDate->format('M d'),
89
                'count' => $found ? $found->count : 0,
0 ignored issues
show
Bug introduced by
The property count does not seem to exist on App\Models\UserDownload. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
90
            ];
91
            $currentDate->addDay();
92
        }
93
94
        return $result;
95
    }
96
97
    /**
98
     * Get downloads per hour for the last N hours
99
     * Uses aggregated hourly stats from user_activity_stats_hourly table
100
     */
101
    public function getDownloadsPerHour(int $hours = 168): array
102
    {
103
        // Use the aggregated hourly stats from UserActivityStat model
104
        return UserActivityStat::getDownloadsPerHour($hours);
105
    }
106
107
    /**
108
     * Get downloads per minute for the last N minutes
109
     */
110
    public function getDownloadsPerMinute(int $minutes = 60): array
111
    {
112
        $startTime = Carbon::now()->subMinutes($minutes);
113
114
        $downloads = UserDownload::query()
115
            ->select(
116
                DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:%i:00") as minute'),
117
                DB::raw('COUNT(*) as count')
118
            )
119
            ->where('timestamp', '>=', $startTime)
120
            ->groupBy(DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:%i:00")'))
121
            ->orderBy('minute', 'asc')
0 ignored issues
show
Bug introduced by
'minute' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

121
            ->orderBy(/** @scrutinizer ignore-type */ 'minute', 'asc')
Loading history...
122
            ->get();
123
124
        // Fill in missing minutes with zero counts
125
        $result = [];
126
        for ($i = $minutes - 1; $i >= 0; $i--) {
127
            $time = Carbon::now()->subMinutes($i);
128
            $minuteKey = $time->format('Y-m-d H:i:00');
129
            $found = $downloads->firstWhere('minute', $minuteKey);
130
            $result[] = [
131
                'time' => $time->format('H:i'),
132
                'count' => $found ? $found->count : 0,
133
            ];
134
        }
135
136
        return $result;
137
    }
138
139
    /**
140
     * Get API hits per day for the last N days
141
     * Uses aggregated stats from user_activity_stats table for dates older than 2 days
142
     * Uses live data from user_requests table for recent days
143
     */
144
    public function getApiHitsPerDay(int $days = 7): array
145
    {
146
        $startDate = Carbon::now()->subDays($days - 1)->startOfDay();
147
        $twoDaysAgo = Carbon::now()->subDays(2)->startOfDay();
148
149
        $result = [];
150
151
        // For historical data (older than 2 days), use aggregated stats
152
        if ($days > 2) {
153
            $historicalStartDate = $startDate->format('Y-m-d');
154
            $historicalEndDate = $twoDaysAgo->copy()->subDay()->format('Y-m-d');
155
156
            $historicalStats = UserActivityStat::query()
157
                ->select('stat_date', 'api_hits_count')
158
                ->where('stat_date', '>=', $historicalStartDate)
159
                ->where('stat_date', '<=', $historicalEndDate)
160
                ->orderBy('stat_date', 'asc')
0 ignored issues
show
Bug introduced by
'stat_date' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

160
                ->orderBy(/** @scrutinizer ignore-type */ 'stat_date', 'asc')
Loading history...
161
                ->get()
162
                ->keyBy('stat_date');
163
164
            // Add historical data
165
            $currentDate = $startDate->copy();
166
            while ($currentDate->lt($twoDaysAgo)) {
167
                $dateStr = $currentDate->format('Y-m-d');
168
                $stat = $historicalStats->get($dateStr);
169
                $result[] = [
170
                    'date' => $currentDate->format('M d'),
171
                    'count' => $stat ? $stat->api_hits_count : 0,
172
                ];
173
                $currentDate->addDay();
174
            }
175
        }
176
177
        // For recent data (last 2 days), use live data from user_requests
178
        $apiHits = UserRequest::query()
179
            ->select(DB::raw('DATE(timestamp) as date'), DB::raw('COUNT(*) as count'))
180
            ->where('timestamp', '>=', $twoDaysAgo)
181
            ->groupBy(DB::raw('DATE(timestamp)'))
182
            ->orderBy('date', 'asc')
183
            ->get()
184
            ->keyBy('date');
185
186
        // Add recent data
187
        $currentDate = $twoDaysAgo->copy();
188
        $now = Carbon::now();
189
        while ($currentDate->lte($now)) {
190
            $dateStr = $currentDate->format('Y-m-d');
191
            $found = $apiHits->get($dateStr);
192
            $result[] = [
193
                'date' => $currentDate->format('M d'),
194
                'count' => $found ? $found->count : 0,
0 ignored issues
show
Bug introduced by
The property count does not seem to exist on App\Models\UserRequest. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
195
            ];
196
            $currentDate->addDay();
197
        }
198
199
        return $result;
200
    }
201
202
    /**
203
     * Get API hits per hour for the last N hours
204
     * Uses aggregated hourly stats from user_activity_stats_hourly table
205
     */
206
    public function getApiHitsPerHour(int $hours = 168): array
207
    {
208
        // Use the aggregated hourly stats from UserActivityStat model
209
        return UserActivityStat::getApiHitsPerHour($hours);
210
    }
211
212
    /**
213
     * Get API hits per minute for the last N minutes
214
     */
215
    public function getApiHitsPerMinute(int $minutes = 60): array
216
    {
217
        $startTime = Carbon::now()->subMinutes($minutes);
218
219
        // Track actual API requests from user_requests table
220
        $apiHits = UserRequest::query()
221
            ->select(
222
                DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:%i:00") as minute'),
223
                DB::raw('COUNT(*) as count')
224
            )
225
            ->where('timestamp', '>=', $startTime)
226
            ->groupBy(DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:%i:00")'))
227
            ->orderBy('minute', 'asc')
0 ignored issues
show
Bug introduced by
'minute' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

227
            ->orderBy(/** @scrutinizer ignore-type */ 'minute', 'asc')
Loading history...
228
            ->get();
229
230
        // Fill in missing minutes with zero counts
231
        $result = [];
232
        for ($i = $minutes - 1; $i >= 0; $i--) {
233
            $time = Carbon::now()->subMinutes($i);
234
            $minuteKey = $time->format('Y-m-d H:i:00');
235
            $found = $apiHits->firstWhere('minute', $minuteKey);
236
            $result[] = [
237
                'time' => $time->format('H:i'),
238
                'count' => $found ? $found->count : 0,
239
            ];
240
        }
241
242
        return $result;
243
    }
244
245
    /**
246
     * Get summary statistics
247
     * Uses aggregated stats for weekly totals where possible
248
     */
249
    public function getSummaryStats(): array
250
    {
251
        $today = Carbon::now()->startOfDay();
252
        $twoDaysAgo = Carbon::now()->subDays(2)->startOfDay();
253
        $sevenDaysAgo = Carbon::now()->subDays(7)->startOfDay();
254
255
        // For weekly stats, combine aggregated historical data + live recent data
256
        $historicalDownloads = UserActivityStat::query()
257
            ->where('stat_date', '>=', $sevenDaysAgo->format('Y-m-d'))
258
            ->where('stat_date', '<', $twoDaysAgo->format('Y-m-d'))
259
            ->sum('downloads_count');
260
261
        $recentDownloads = UserDownload::query()
262
            ->where('timestamp', '>=', $twoDaysAgo)
263
            ->count();
264
265
        $historicalApiHits = UserActivityStat::query()
266
            ->where('stat_date', '>=', $sevenDaysAgo->format('Y-m-d'))
267
            ->where('stat_date', '<', $twoDaysAgo->format('Y-m-d'))
268
            ->sum('api_hits_count');
269
270
        $recentApiHits = UserRequest::query()
271
            ->where('timestamp', '>=', $twoDaysAgo)
272
            ->count();
273
274
        return [
275
            'total_users' => User::whereNull('deleted_at')->count(),
276
            'downloads_today' => UserDownload::where('timestamp', '>=', $today)->count(),
277
            'downloads_week' => $historicalDownloads + $recentDownloads,
278
            'api_hits_today' => UserRequest::query()->where('timestamp', '>=', $today)->count(),
279
            'api_hits_week' => $historicalApiHits + $recentApiHits,
280
        ];
281
    }
282
283
    /**
284
     * Get top downloaders
285
     */
286
    public function getTopDownloaders(int $limit = 5): array
287
    {
288
        $weekAgo = Carbon::now()->subDays(7);
289
290
        return UserDownload::query()
291
            ->join('users', 'user_downloads.users_id', '=', 'users.id')
292
            ->select('users.username', DB::raw('COUNT(*) as download_count'))
293
            ->where('user_downloads.timestamp', '>=', $weekAgo)
294
            ->groupBy('users.id', 'users.username')
295
            ->orderByDesc('download_count')
0 ignored issues
show
Bug introduced by
'download_count' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderByDesc(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

295
            ->orderByDesc(/** @scrutinizer ignore-type */ 'download_count')
Loading history...
296
            ->limit($limit)
297
            ->get()
298
            ->toArray();
299
    }
300
}
301