Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.92% |
47 / 48 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
| RoleplayLevelsReportRepository | |
97.92% |
47 / 48 |
|
50.00% |
1 / 2 |
12 | |
0.00% |
0 / 1 |
| getRoleplayUserIds | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
3 | |||
| getAverageScoreByUser | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
9 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Repositories\Report; |
| 4 | |
| 5 | use App\Http\Models\RolePlayConversations; |
| 6 | use App\Http\Models\UserAddOns; |
| 7 | use Carbon\Carbon; |
| 8 | use Illuminate\Support\Collection; |
| 9 | use MongoDB\BSON\UTCDateTime; |
| 10 | |
| 11 | /** |
| 12 | * Data-access layer for the "roleplay levels" admin distribution report. |
| 13 | * |
| 14 | * This repository owns all direct database access for the report and keeps |
| 15 | * the service layer free of query construction details. Queries are kept |
| 16 | * database-agnostic where practical; MongoDB-specific aggregation is used |
| 17 | * only for the average-score pipeline which has no Eloquent equivalent. |
| 18 | */ |
| 19 | class RoleplayLevelsReportRepository |
| 20 | { |
| 21 | /** |
| 22 | * Return the list of user IDs that currently hold an active RolePlay |
| 23 | * UserAddOn, optionally scoped to a specific company. |
| 24 | * |
| 25 | * An addon is considered "active" when its `status` field equals |
| 26 | * "active". Both individual (Stripe-billed) and company (corporate) |
| 27 | * sources are included per product requirements. |
| 28 | * |
| 29 | * @param array<int, string>|null $companyIds Optional list of company IDs to scope to |
| 30 | * @return Collection<int, string> Collection of user ID strings |
| 31 | */ |
| 32 | public function getRoleplayUserIds(?array $companyIds = null): Collection |
| 33 | { |
| 34 | $addonUserIds = UserAddOns::query() |
| 35 | ->where('product', 'RolePlay') |
| 36 | ->where('status', 'active') |
| 37 | ->pluck('user_id') |
| 38 | ->map(fn ($id) => (string) $id) |
| 39 | ->unique() |
| 40 | ->values(); |
| 41 | |
| 42 | if ($companyIds === null || empty($companyIds)) { |
| 43 | return $addonUserIds; |
| 44 | } |
| 45 | |
| 46 | // Scope via the users' company_id. Done as a secondary query rather |
| 47 | // than a join so the repository stays portable across database backends. |
| 48 | $scopedIds = \App\Http\Models\Auth\User::query() |
| 49 | ->whereIn('company_id', $companyIds) |
| 50 | ->get(['_id']) |
| 51 | ->map(fn ($u) => (string) $u->_id) |
| 52 | ->values() |
| 53 | ->all(); |
| 54 | |
| 55 | $scopedSet = array_flip($scopedIds); |
| 56 | |
| 57 | return $addonUserIds |
| 58 | ->filter(fn (string $id) => isset($scopedSet[$id])) |
| 59 | ->values(); |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Compute the average non-failed roleplay score for each of the given |
| 64 | * users within the optional date window. |
| 65 | * |
| 66 | * Users with no qualifying sessions are NOT returned in the map; the |
| 67 | * service layer is responsible for defaulting missing users to score 0. |
| 68 | * |
| 69 | * @param array<int, string> $userIds List of user IDs to aggregate |
| 70 | * @param Carbon|null $from Inclusive lower bound on created_at |
| 71 | * @param Carbon|null $to Inclusive upper bound on created_at |
| 72 | * @return array<string, float> Map of user_id → average score |
| 73 | */ |
| 74 | public function getAverageScoreByUser(array $userIds, ?Carbon $from, ?Carbon $to): array |
| 75 | { |
| 76 | if (empty($userIds)) { |
| 77 | return []; |
| 78 | } |
| 79 | |
| 80 | $match = [ |
| 81 | 'user_id' => ['$in' => array_values(array_map('strval', $userIds))], |
| 82 | 'status' => ['$ne' => 'failed'], |
| 83 | ]; |
| 84 | |
| 85 | if ($from || $to) { |
| 86 | $range = []; |
| 87 | if ($from) { |
| 88 | $range['$gte'] = new UTCDateTime($from->copy()->startOfDay()); |
| 89 | } |
| 90 | if ($to) { |
| 91 | $range['$lte'] = new UTCDateTime($to->copy()->endOfDay()); |
| 92 | } |
| 93 | $match['created_at'] = $range; |
| 94 | } |
| 95 | |
| 96 | $pipeline = [ |
| 97 | ['$match' => $match], |
| 98 | ['$group' => [ |
| 99 | '_id' => '$user_id', |
| 100 | 'avg_score' => ['$avg' => '$score'], |
| 101 | ]], |
| 102 | ]; |
| 103 | |
| 104 | $results = collect(RolePlayConversations::raw(fn ($c) => $c->aggregate($pipeline))); |
| 105 | |
| 106 | $map = []; |
| 107 | foreach ($results as $row) { |
| 108 | $userId = (string) ($row['_id'] ?? ''); |
| 109 | if ($userId === '') { |
| 110 | continue; |
| 111 | } |
| 112 | $avg = $row['avg_score'] ?? null; |
| 113 | $map[$userId] = $avg === null ? 0.0 : (float) $avg; |
| 114 | } |
| 115 | |
| 116 | return $map; |
| 117 | } |
| 118 | } |