Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
RoleplayLevelsReportService
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
2 / 2
7
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDistribution
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3namespace App\Http\Services\Report;
4
5use App\DTO\Report\RoleplayLevelBucket;
6use App\Http\Repositories\Report\RoleplayLevelsReportRepository;
7use 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 */
20class 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}