Passed
Push — master ( 4f5b3b...1c5d75 )
by Darko
09:48
created

UserStatsService   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 361
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 33
eloc 213
c 4
b 0
f 0
dl 0
loc 361
rs 9.76

9 Methods

Rating   Name   Duplication   Size   Complexity  
B getDownloadsPerHour() 0 41 6
A getUsersByRole() 0 15 1
A getApiHitsPerMinute() 0 28 3
A getDownloadsPerMinute() 0 27 3
B getApiHitsPerHour() 0 41 6
B getDownloadsPerDay() 0 56 6
A getSummaryStats() 0 31 1
B getApiHitsPerDay() 0 56 6
A getTopDownloaders() 0 13 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,
0 ignored issues
show
Bug introduced by
The property downloads_count does not seem to exist on App\Models\UserActivityStat. 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...
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 live data from user_downloads table
100
     */
101
    public function getDownloadsPerHour(int $hours = 168): array
102
    {
103
        $startTime = Carbon::now()->subHours($hours - 1)->startOfHour();
104
105
        $downloads = UserDownload::query()
106
            ->select(
107
                DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:00:00") as hour'),
108
                DB::raw('COUNT(*) as count')
109
            )
110
            ->where('timestamp', '>=', $startTime)
111
            ->groupBy(DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:00:00")'))
112
            ->orderBy('hour', 'asc')
0 ignored issues
show
Bug introduced by
'hour' 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

112
            ->orderBy(/** @scrutinizer ignore-type */ 'hour', 'asc')
Loading history...
113
            ->get()
114
            ->keyBy('hour');
115
116
        // Fill in missing hours with zero counts
117
        $result = [];
118
        for ($i = $hours - 1; $i >= 0; $i--) {
119
            $time = Carbon::now()->subHours($i)->startOfHour();
120
            $hourKey = $time->format('Y-m-d H:00:00');
121
            $found = $downloads->get($hourKey);
122
123
            // Format label based on how recent the hour is
124
            $now = Carbon::now();
125
            if ($time->isToday()) {
126
                $label = $time->format('H:i');
127
            } elseif ($time->isYesterday()) {
128
                $label = 'Yesterday '.$time->format('H:i');
129
            } elseif ($time->diffInDays($now) < 7) {
130
                $label = $time->format('D H:i');
131
            } else {
132
                $label = $time->format('M d H:i');
133
            }
134
135
            $result[] = [
136
                'time' => $label,
137
                '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...
138
            ];
139
        }
140
141
        return $result;
142
    }
143
144
    /**
145
     * Get downloads per minute for the last N minutes
146
     */
147
    public function getDownloadsPerMinute(int $minutes = 60): array
148
    {
149
        $startTime = Carbon::now()->subMinutes($minutes);
150
151
        $downloads = UserDownload::query()
152
            ->select(
153
                DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:%i:00") as minute'),
154
                DB::raw('COUNT(*) as count')
155
            )
156
            ->where('timestamp', '>=', $startTime)
157
            ->groupBy(DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:%i:00")'))
158
            ->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

158
            ->orderBy(/** @scrutinizer ignore-type */ 'minute', 'asc')
Loading history...
159
            ->get();
160
161
        // Fill in missing minutes with zero counts
162
        $result = [];
163
        for ($i = $minutes - 1; $i >= 0; $i--) {
164
            $time = Carbon::now()->subMinutes($i);
165
            $minuteKey = $time->format('Y-m-d H:i:00');
166
            $found = $downloads->firstWhere('minute', $minuteKey);
167
            $result[] = [
168
                'time' => $time->format('H:i'),
169
                'count' => $found ? $found->count : 0,
170
            ];
171
        }
172
173
        return $result;
174
    }
175
176
    /**
177
     * Get API hits per day for the last N days
178
     * Uses aggregated stats from user_activity_stats table for dates older than 2 days
179
     * Uses live data from user_requests table for recent days
180
     */
181
    public function getApiHitsPerDay(int $days = 7): array
182
    {
183
        $startDate = Carbon::now()->subDays($days - 1)->startOfDay();
184
        $twoDaysAgo = Carbon::now()->subDays(2)->startOfDay();
185
186
        $result = [];
187
188
        // For historical data (older than 2 days), use aggregated stats
189
        if ($days > 2) {
190
            $historicalStartDate = $startDate->format('Y-m-d');
191
            $historicalEndDate = $twoDaysAgo->copy()->subDay()->format('Y-m-d');
192
193
            $historicalStats = UserActivityStat::query()
194
                ->select('stat_date', 'api_hits_count')
195
                ->where('stat_date', '>=', $historicalStartDate)
196
                ->where('stat_date', '<=', $historicalEndDate)
197
                ->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

197
                ->orderBy(/** @scrutinizer ignore-type */ 'stat_date', 'asc')
Loading history...
198
                ->get()
199
                ->keyBy('stat_date');
200
201
            // Add historical data
202
            $currentDate = $startDate->copy();
203
            while ($currentDate->lt($twoDaysAgo)) {
204
                $dateStr = $currentDate->format('Y-m-d');
205
                $stat = $historicalStats->get($dateStr);
206
                $result[] = [
207
                    'date' => $currentDate->format('M d'),
208
                    'count' => $stat ? $stat->api_hits_count : 0,
0 ignored issues
show
Bug introduced by
The property api_hits_count does not seem to exist on App\Models\UserActivityStat. 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...
209
                ];
210
                $currentDate->addDay();
211
            }
212
        }
213
214
        // For recent data (last 2 days), use live data from user_requests
215
        $apiHits = UserRequest::query()
216
            ->select(DB::raw('DATE(timestamp) as date'), DB::raw('COUNT(*) as count'))
217
            ->where('timestamp', '>=', $twoDaysAgo)
218
            ->groupBy(DB::raw('DATE(timestamp)'))
219
            ->orderBy('date', 'asc')
220
            ->get()
221
            ->keyBy('date');
222
223
        // Add recent data
224
        $currentDate = $twoDaysAgo->copy();
225
        $now = Carbon::now();
226
        while ($currentDate->lte($now)) {
227
            $dateStr = $currentDate->format('Y-m-d');
228
            $found = $apiHits->get($dateStr);
229
            $result[] = [
230
                'date' => $currentDate->format('M d'),
231
                '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...
232
            ];
233
            $currentDate->addDay();
234
        }
235
236
        return $result;
237
    }
238
239
    /**
240
     * Get API hits per hour for the last N hours
241
     * Uses live data from user_requests table
242
     */
243
    public function getApiHitsPerHour(int $hours = 168): array
244
    {
245
        $startTime = Carbon::now()->subHours($hours - 1)->startOfHour();
246
247
        $apiHits = UserRequest::query()
248
            ->select(
249
                DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:00:00") as hour'),
250
                DB::raw('COUNT(*) as count')
251
            )
252
            ->where('timestamp', '>=', $startTime)
253
            ->groupBy(DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:00:00")'))
254
            ->orderBy('hour', 'asc')
0 ignored issues
show
Bug introduced by
'hour' 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

254
            ->orderBy(/** @scrutinizer ignore-type */ 'hour', 'asc')
Loading history...
255
            ->get()
256
            ->keyBy('hour');
257
258
        // Fill in missing hours with zero counts
259
        $result = [];
260
        for ($i = $hours - 1; $i >= 0; $i--) {
261
            $time = Carbon::now()->subHours($i)->startOfHour();
262
            $hourKey = $time->format('Y-m-d H:00:00');
263
            $found = $apiHits->get($hourKey);
264
265
            // Format label based on how recent the hour is
266
            $now = Carbon::now();
267
            if ($time->isToday()) {
268
                $label = $time->format('H:i');
269
            } elseif ($time->isYesterday()) {
270
                $label = 'Yesterday '.$time->format('H:i');
271
            } elseif ($time->diffInDays($now) < 7) {
272
                $label = $time->format('D H:i');
273
            } else {
274
                $label = $time->format('M d H:i');
275
            }
276
277
            $result[] = [
278
                'time' => $label,
279
                '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...
280
            ];
281
        }
282
283
        return $result;
284
    }
285
286
    /**
287
     * Get API hits per minute for the last N minutes
288
     */
289
    public function getApiHitsPerMinute(int $minutes = 60): array
290
    {
291
        $startTime = Carbon::now()->subMinutes($minutes);
292
293
        // Track actual API requests from user_requests table
294
        $apiHits = UserRequest::query()
295
            ->select(
296
                DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:%i:00") as minute'),
297
                DB::raw('COUNT(*) as count')
298
            )
299
            ->where('timestamp', '>=', $startTime)
300
            ->groupBy(DB::raw('DATE_FORMAT(timestamp, "%Y-%m-%d %H:%i:00")'))
301
            ->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

301
            ->orderBy(/** @scrutinizer ignore-type */ 'minute', 'asc')
Loading history...
302
            ->get();
303
304
        // Fill in missing minutes with zero counts
305
        $result = [];
306
        for ($i = $minutes - 1; $i >= 0; $i--) {
307
            $time = Carbon::now()->subMinutes($i);
308
            $minuteKey = $time->format('Y-m-d H:i:00');
309
            $found = $apiHits->firstWhere('minute', $minuteKey);
310
            $result[] = [
311
                'time' => $time->format('H:i'),
312
                'count' => $found ? $found->count : 0,
313
            ];
314
        }
315
316
        return $result;
317
    }
318
319
    /**
320
     * Get summary statistics
321
     * Uses aggregated stats for weekly totals where possible
322
     */
323
    public function getSummaryStats(): array
324
    {
325
        $today = Carbon::now()->startOfDay();
326
        $twoDaysAgo = Carbon::now()->subDays(2)->startOfDay();
327
        $sevenDaysAgo = Carbon::now()->subDays(7)->startOfDay();
328
329
        // For weekly stats, combine aggregated historical data + live recent data
330
        $historicalDownloads = UserActivityStat::query()
331
            ->where('stat_date', '>=', $sevenDaysAgo->format('Y-m-d'))
332
            ->where('stat_date', '<', $twoDaysAgo->format('Y-m-d'))
333
            ->sum('downloads_count');
334
335
        $recentDownloads = UserDownload::query()
336
            ->where('timestamp', '>=', $twoDaysAgo)
337
            ->count();
338
339
        $historicalApiHits = UserActivityStat::query()
340
            ->where('stat_date', '>=', $sevenDaysAgo->format('Y-m-d'))
341
            ->where('stat_date', '<', $twoDaysAgo->format('Y-m-d'))
342
            ->sum('api_hits_count');
343
344
        $recentApiHits = UserRequest::query()
345
            ->where('timestamp', '>=', $twoDaysAgo)
346
            ->count();
347
348
        return [
349
            'total_users' => User::whereNull('deleted_at')->count(),
350
            'downloads_today' => UserDownload::where('timestamp', '>=', $today)->count(),
351
            'downloads_week' => $historicalDownloads + $recentDownloads,
352
            'api_hits_today' => UserRequest::query()->where('timestamp', '>=', $today)->count(),
353
            'api_hits_week' => $historicalApiHits + $recentApiHits,
354
        ];
355
    }
356
357
    /**
358
     * Get top downloaders
359
     */
360
    public function getTopDownloaders(int $limit = 5): array
361
    {
362
        $weekAgo = Carbon::now()->subDays(7);
363
364
        return UserDownload::query()
365
            ->join('users', 'user_downloads.users_id', '=', 'users.id')
366
            ->select('users.username', DB::raw('COUNT(*) as download_count'))
367
            ->where('user_downloads.timestamp', '>=', $weekAgo)
368
            ->groupBy('users.id', 'users.username')
369
            ->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

369
            ->orderByDesc(/** @scrutinizer ignore-type */ 'download_count')
Loading history...
370
            ->limit($limit)
371
            ->get()
372
            ->toArray();
373
    }
374
}
375