Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.89% covered (warning)
88.89%
8 / 9
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
RoleplayLevelClassifier
88.89% covered (warning)
88.89%
8 / 9
50.00% covered (danger)
50.00%
1 / 2
4.02
0.00% covered (danger)
0.00%
0 / 1
 classify
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 labelFor
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
1<?php
2
3namespace App\Http\Services\Report;
4
5/**
6 * Authoritative mapping from a 0-100 roleplay score to its coach level bucket.
7 *
8 * The mapping is intentionally centralised in a single static helper so that
9 * controllers, services, exports and client tools never diverge on boundary
10 * semantics. Boundaries are inclusive on the lower bound:
11 *
12 *   score >= 85 → expert
13 *   score >= 70 → advanced
14 *   score >= 50 → competent
15 *   score >= 30 → developing
16 *   score  < 30 → needs_work
17 *
18 * A user with no sessions at all is treated as score 0 by callers and falls
19 * into the "needs_work" bucket.
20 */
21final class RoleplayLevelClassifier
22{
23    /** Bucket key for scores below 30 (or users with no sessions). */
24    public const LEVEL_NEEDS_WORK = 'needs_work';
25
26    /** Bucket key for scores in [30, 50). */
27    public const LEVEL_DEVELOPING = 'developing';
28
29    /** Bucket key for scores in [50, 70). */
30    public const LEVEL_COMPETENT = 'competent';
31
32    /** Bucket key for scores in [70, 85). */
33    public const LEVEL_ADVANCED = 'advanced';
34
35    /** Bucket key for scores >= 85. */
36    public const LEVEL_EXPERT = 'expert';
37
38    /**
39     * Ordered list of levels from lowest to highest. The response always
40     * renders buckets in this order.
41     *
42     * @var array<int, array{key: string, label: string}>
43     */
44    public const LEVELS = [
45        ['key' => self::LEVEL_NEEDS_WORK, 'label' => 'Needs Work'],
46        ['key' => self::LEVEL_DEVELOPING, 'label' => 'Developing'],
47        ['key' => self::LEVEL_COMPETENT, 'label' => 'Competent'],
48        ['key' => self::LEVEL_ADVANCED, 'label' => 'Advanced'],
49        ['key' => self::LEVEL_EXPERT, 'label' => 'Expert'],
50    ];
51
52    /**
53     * Classify a numeric score into its level key.
54     *
55     * @param  float  $score  The average roleplay score (0-100)
56     * @return string One of the LEVEL_* constants
57     */
58    public static function classify(float $score): string
59    {
60        return match (true) {
61            $score >= 85 => self::LEVEL_EXPERT,
62            $score >= 70 => self::LEVEL_ADVANCED,
63            $score >= 50 => self::LEVEL_COMPETENT,
64            $score >= 30 => self::LEVEL_DEVELOPING,
65            default => self::LEVEL_NEEDS_WORK,
66        };
67    }
68
69    /**
70     * Return the human-readable label for a level key.
71     *
72     * @param  string  $key  One of the LEVEL_* constants
73     * @return string Friendly label (e.g. "Needs Work")
74     */
75    public static function labelFor(string $key): string
76    {
77        foreach (self::LEVELS as $level) {
78            if ($level['key'] === $key) {
79                return $level['label'];
80            }
81        }
82
83        return $key;
84    }
85}