Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
29 / 29 |
|
100.00% |
2 / 2 |
CRAP | |
100.00% |
1 / 1 |
| RoleplayLevelsReportService | |
100.00% |
29 / 29 |
|
100.00% |
2 / 2 |
7 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getDistribution | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
6 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Services\Report; |
| 4 | |
| 5 | use App\DTO\Report\RoleplayLevelBucket; |
| 6 | use App\Http\Repositories\Report\RoleplayLevelsReportRepository; |
| 7 | use Carbon\Carbon; |
| 8 | |
| 9 | /** |
| 10 | * Orchestration layer for the "roleplay levels" admin distribution report. |
| 11 | * |
| 12 | * Responsibilities: |
| 13 | * 1. Resolve the cohort of users that currently hold an active RolePlay |
| 14 | * addon (optionally scoped to a company). |
| 15 | * 2. Compute each user's average non-failed session score within the |
| 16 | * requested date window, defaulting users with no sessions to 0. |
| 17 | * 3. Classify each user into one of the five coach-level buckets via |
| 18 | * {@see RoleplayLevelClassifier} and aggregate counts/percentages. |
| 19 | */ |
| 20 | class RoleplayLevelsReportService |
| 21 | { |
| 22 | public function __construct( |
| 23 | private readonly RoleplayLevelsReportRepository $repository, |
| 24 | ) {} |
| 25 | |
| 26 | /** |
| 27 | * Generate the level distribution report. |
| 28 | * |
| 29 | * @param array<int, string>|null $companyIds Optional company scope. `null` or empty |
| 30 | * means global (CMC) scope. |
| 31 | * @param Carbon|null $from Inclusive lower bound on session created_at |
| 32 | * @param Carbon|null $to Inclusive upper bound on session created_at |
| 33 | * @return array{total_users: int, levels: array<int, array{key: string, label: string, count: int, percentage: float}>} |
| 34 | */ |
| 35 | public function getDistribution(?array $companyIds, ?Carbon $from, ?Carbon $to): array |
| 36 | { |
| 37 | $userIds = $this->repository->getRoleplayUserIds($companyIds)->all(); |
| 38 | $totalUsers = count($userIds); |
| 39 | |
| 40 | $averageByUser = $totalUsers > 0 |
| 41 | ? $this->repository->getAverageScoreByUser($userIds, $from, $to) |
| 42 | : []; |
| 43 | |
| 44 | // Initialize all buckets at zero so every level is always present. |
| 45 | $counts = []; |
| 46 | foreach (RoleplayLevelClassifier::LEVELS as $level) { |
| 47 | $counts[$level['key']] = 0; |
| 48 | } |
| 49 | |
| 50 | foreach ($userIds as $userId) { |
| 51 | $score = (float) ($averageByUser[$userId] ?? 0.0); |
| 52 | $bucket = RoleplayLevelClassifier::classify($score); |
| 53 | $counts[$bucket]++; |
| 54 | } |
| 55 | |
| 56 | $buckets = []; |
| 57 | foreach (RoleplayLevelClassifier::LEVELS as $level) { |
| 58 | $count = $counts[$level['key']]; |
| 59 | $percentage = $totalUsers > 0 |
| 60 | ? round(($count / $totalUsers) * 100, 2) |
| 61 | : 0.0; |
| 62 | |
| 63 | $buckets[] = new RoleplayLevelBucket( |
| 64 | key: $level['key'], |
| 65 | label: $level['label'], |
| 66 | count: $count, |
| 67 | percentage: $percentage, |
| 68 | ); |
| 69 | } |
| 70 | |
| 71 | return [ |
| 72 | 'total_users' => $totalUsers, |
| 73 | 'levels' => array_map(fn (RoleplayLevelBucket $b) => $b->toArray(), $buckets), |
| 74 | ]; |
| 75 | } |
| 76 | } |