Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
ScorecardResolverService
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
4 / 4
16
100.00% covered (success)
100.00%
1 / 1
 resolve
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
10
 resolveAll
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getSystemDefault
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getKnownCallTypes
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Http\Services;
4
5use App\Http\Models\Auth\User;
6use App\Http\Models\CompanyRolePlayScorecard;
7use App\Http\Models\RolePlayConfig;
8use App\Http\Models\RolePlaySkillProgressions;
9use App\Http\Models\UserRolePlaySettings;
10use Illuminate\Support\Facades\Log;
11
12/**
13 * ScorecardResolverService
14 *
15 * Resolves which scorecard configuration to use for a given user and call type.
16 * Implements a prioritized resolution chain:
17 *
18 * 1. Company forced scorecard (is_forced === true) → locked, cannot be edited by user
19 * 2. User-level scorecard override (scorecard_config on UserRolePlaySettings)
20 * 3. Company default scorecard (is_forced === false) → editable by user
21 * 4. System default from RolePlayConfig or hardcoded static scorecards
22 *
23 * The returned array always contains:
24 * - `scorecard` (array): The resolved scorecard configuration
25 * - `source` (string): One of 'company', 'user', or 'system'
26 * - `locked` (bool): Whether the user is prevented from editing this scorecard
27 */
28class ScorecardResolverService
29{
30    /**
31     * Resolve the scorecard configuration for a specific user and call type.
32     *
33     * Resolution order:
34     * 1. Company forced scorecard (is_forced = true) → source: 'company', locked: true
35     * 2. User-level scorecard_config → source: 'user', locked: false
36     * 3. Company default scorecard (is_forced = false) → source: 'company', locked: false
37     * 4. System default → source: 'system', locked: false
38     *
39     * @param string $userId The user ID to resolve the scorecard for
40     * @param string $callType The call type identifier (e.g., 'cold-call', 'discovery-call')
41     * @return array{scorecard: array, source: string, locked: bool}
42     */
43    public function resolve(string $userId, string $callType): array
44    {
45        $user = User::find($userId);
46        $companyId = $user?->company_id;
47
48        // Step 1: Check for a forced company scorecard
49        if ($companyId) {
50            $companyScorecard = CompanyRolePlayScorecard::forCompany($companyId)
51                ->forCallType($callType)
52                ->first();
53
54            if ($companyScorecard && $companyScorecard->is_forced === true && ! empty($companyScorecard->scorecard)) {
55                return [
56                    'scorecard' => $companyScorecard->scorecard,
57                    'source'    => 'company',
58                    'locked'    => true,
59                ];
60            }
61        }
62
63        // Step 2: Check user-level scorecard_config
64        $userSettings = UserRolePlaySettings::where('user_id', $userId)->first();
65
66        if ($userSettings) {
67            $userScorecardConfig = $userSettings->scorecard_config ?? [];
68
69            if (! empty($userScorecardConfig[$callType])) {
70                return [
71                    'scorecard' => $userScorecardConfig[$callType],
72                    'source'    => 'user',
73                    'locked'    => false,
74                ];
75            }
76        }
77
78        // Step 3: Check for a non-forced company scorecard as default
79        if ($companyId) {
80            // Re-use $companyScorecard from step 1 if available, otherwise query
81            $companyScorecard = $companyScorecard
82                ?? CompanyRolePlayScorecard::forCompany($companyId)
83                    ->forCallType($callType)
84                    ->first();
85
86            if ($companyScorecard && ! empty($companyScorecard->scorecard)) {
87                return [
88                    'scorecard' => $companyScorecard->scorecard,
89                    'source'    => 'company',
90                    'locked'    => false,
91                ];
92            }
93        }
94
95        // Step 4: Fall back to system default
96        return [
97            'scorecard' => $this->getSystemDefault($callType),
98            'source'    => 'system',
99            'locked'    => false,
100        ];
101    }
102
103    /**
104     * Resolve scorecard configurations for ALL known call types.
105     *
106     * Gets the list of call types from RolePlayConfig's default_scorecards keys,
107     * falling back to ['cold-call', 'discovery-call'] if none are configured.
108     *
109     * @param string $userId The user ID to resolve scorecards for
110     * @return array<string, array{scorecard: array, source: string, locked: bool}> Keyed by call type
111     */
112    public function resolveAll(string $userId): array
113    {
114        $callTypes = $this->getKnownCallTypes();
115        $results = [];
116
117        foreach ($callTypes as $callType) {
118            $results[$callType] = $this->resolve($userId, $callType);
119        }
120
121        return $results;
122    }
123
124    /**
125     * Get the system default scorecard for a given call type.
126     *
127     * First checks RolePlayConfig for DB-seeded defaults, then falls back
128     * to the hardcoded static properties on RolePlaySkillProgressions.
129     *
130     * @param string $callType The call type identifier
131     * @return array The default scorecard configuration
132     */
133    private function getSystemDefault(string $callType): array
134    {
135        $config = RolePlayConfig::first();
136        $defaultScorecards = $config?->default_scorecards ?? [];
137
138        if (! empty($defaultScorecards[$callType])) {
139            return $defaultScorecards[$callType];
140        }
141
142        // Ultimate fallback: hardcoded static scorecards
143        return RolePlaySkillProgressions::getScorecard($callType);
144    }
145
146    /**
147     * Get all known call types from system configuration.
148     *
149     * Reads from RolePlayConfig's default_scorecards keys, falling back
150     * to the two standard call types if no configuration exists.
151     *
152     * @return array<string> Array of call type identifiers
153     */
154    private function getKnownCallTypes(): array
155    {
156        $config = RolePlayConfig::first();
157        $defaultScorecards = $config?->default_scorecards ?? [];
158
159        if (! empty($defaultScorecards)) {
160            return array_keys($defaultScorecards);
161        }
162
163        return ['cold-call', 'discovery-call'];
164    }
165}