Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.50% |
86 / 103 |
|
66.67% |
4 / 6 |
CRAP | |
0.00% |
0 / 1 |
RolePlayProjects | |
83.50% |
86 / 103 |
|
66.67% |
4 / 6 |
17.15 | |
0.00% |
0 / 1 |
newFactory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
conversations | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
user | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
calculateProgression | |
55.88% |
19 / 34 |
|
0.00% |
0 / 1 |
11.21 | |||
getRules | |
96.83% |
61 / 63 |
|
0.00% |
0 / 1 |
5 | |||
getProjectAverageScore | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace App\Http\Models; |
4 | |
5 | use App\Http\Models\Auth\User; |
6 | use database\factories\http\models\RolePlayProjectsFactory; |
7 | use Illuminate\Database\Eloquent\Factories\HasFactory; |
8 | use Illuminate\Validation\Rule; |
9 | |
10 | class RolePlayProjects extends Moloquent |
11 | { |
12 | use HasFactory; |
13 | protected $table = 'role_play_projects'; |
14 | |
15 | static $LEVEL_LOW = 'Low'; |
16 | static $LEVEL_MODERATE = 'Moderate'; |
17 | static $LEVEL_HIGH = 'High'; |
18 | static $LEVEL_CRITICAL = 'Critical'; |
19 | |
20 | static $COMPANY_SIZE_SMALL = 'Small (10-99 employees)'; |
21 | static $COMPANY_SIZE_MEDIUM = 'Medium (100-999 employees)'; |
22 | static $COMPANY_SIZE_LARGE = 'Large (1000+ employees)'; |
23 | static $COLD_CALL = 'cold-call'; |
24 | static $DISCOVERY_CALL = 'discovery-call'; |
25 | |
26 | protected $fillable = [ |
27 | 'name', |
28 | 'user_id', |
29 | 'type', |
30 | 'description', |
31 | 'difficulty_level', |
32 | 'key_features', |
33 | 'industry', |
34 | 'target_job_titles', |
35 | 'scorecard_config', |
36 | 'objections', |
37 | 'customer_profiles', |
38 | 'training_personalities', |
39 | 'progression', |
40 | 'average_score', |
41 | 'updated_at', |
42 | 'created_at', |
43 | ]; |
44 | |
45 | protected static function newFactory() |
46 | { |
47 | return RolePlayProjectsFactory::new(); |
48 | } |
49 | |
50 | public function conversations() |
51 | { |
52 | return $this->hasMany(RolePlayConversations::class, 'project_id'); |
53 | } |
54 | |
55 | public function user() |
56 | { |
57 | return $this->belongsTo(User::class, 'user_id'); |
58 | } |
59 | |
60 | public function calculateProgression(array $feedback) |
61 | { |
62 | $project = $this; |
63 | $averageScore = $this->getProjectAverageScore($project->id); |
64 | $project->average_score = $averageScore; |
65 | |
66 | $progressions = $project->progression ?? []; |
67 | |
68 | foreach ($feedback['scores'] ?? [] as $score) { |
69 | if (empty($score['criteria']) || !is_array($score['criteria'])) { |
70 | continue; |
71 | } |
72 | |
73 | $currentProgress = collect($progressions)->first(function ($prog) use ($score) { |
74 | return $prog['name'] === $score['name']; |
75 | }); |
76 | |
77 | if (!$currentProgress) { |
78 | $progressions[] = [ |
79 | 'name' => $score['name'], |
80 | 'feedback' => $score['feedback'] ?? '', |
81 | 'score' => max($score['score'] ?? 0, 0), |
82 | 'improve' => $score['improve'] ?? '', |
83 | 'criteria' => $score['criteria'] ?? [], |
84 | ]; |
85 | } else { |
86 | $currentProgress['feedback'] = $score['feedback'] ?? $currentProgress['feedback']; |
87 | $currentProgress['score'] = max(($currentProgress['score'] + ($score['score'] ?? 0)) / 2, 0); |
88 | |
89 | foreach ($score['criteria'] as $criteria) { |
90 | $currentCriteria = collect($currentProgress['criteria'] ?? [])->first(function ($crit) use ($criteria) { |
91 | return $crit['name'] === $criteria['name']; |
92 | }); |
93 | |
94 | if (!$currentCriteria) { |
95 | $currentProgress['criteria'][] = [ |
96 | 'name' => $criteria['name'], |
97 | 'feedback' => $criteria['feedback'] ?? '', |
98 | 'score' => max($criteria['score'] ?? 0, 0), |
99 | ]; |
100 | } else { |
101 | $currentCriteria['feedback'] = $criteria['feedback'] ?? $currentCriteria['feedback']; |
102 | $currentCriteria['score'] = max(($currentCriteria['score'] + ($criteria['score'] ?? 0)) / 2, 0); |
103 | } |
104 | } |
105 | } |
106 | } |
107 | |
108 | $project->progression = $progressions; |
109 | $project->save(); |
110 | } |
111 | |
112 | public static function getRules(): array |
113 | { |
114 | return [ |
115 | 'name' => 'required|string|max:255', |
116 | 'type' => ['required', 'string', Rule::in([RolePlayProjects::$COLD_CALL, RolePlayProjects::$DISCOVERY_CALL])], |
117 | 'description' => 'nullable|string', |
118 | 'difficulty_level' => 'required|integer|min:1|max:5', |
119 | 'key_features' => 'nullable|array', |
120 | 'key_features.*' => 'string|max:255', |
121 | 'industry' => 'required|string', |
122 | 'target_job_titles' => 'required|array|min:1', |
123 | 'target_job_titles.*' => 'string|max:255', |
124 | 'customer_profiles' => 'required|array|min:1', |
125 | 'customer_profiles.*.id' => 'required|integer', |
126 | 'customer_profiles.*.company_name' => 'required|string|max:255', |
127 | 'customer_profiles.*.company_size' => ['required', 'string', Rule::in([ |
128 | RolePlayProjects::$COMPANY_SIZE_SMALL, |
129 | RolePlayProjects::$COMPANY_SIZE_MEDIUM, |
130 | RolePlayProjects::$COMPANY_SIZE_LARGE |
131 | ])], |
132 | 'customer_profiles.*.budget' => 'required|string', |
133 | 'customer_profiles.*.decision_making' => 'required|string', |
134 | 'customer_profiles.*.urgency_level' => ['required', 'string', Rule::in([ |
135 | RolePlayProjects::$LEVEL_LOW, |
136 | RolePlayProjects::$LEVEL_MODERATE, |
137 | RolePlayProjects::$LEVEL_HIGH, |
138 | RolePlayProjects::$LEVEL_CRITICAL, |
139 | ])], |
140 | 'customer_profiles.*.openess_to_new_solutions' => ['required', 'string', Rule::in([ |
141 | RolePlayProjects::$LEVEL_LOW, |
142 | RolePlayProjects::$LEVEL_MODERATE, |
143 | RolePlayProjects::$LEVEL_HIGH, |
144 | RolePlayProjects::$LEVEL_CRITICAL, |
145 | ])], |
146 | 'customer_profiles.*.communication_style' => 'required|string', |
147 | 'customer_profiles.*.pain_points' => 'required|string', |
148 | 'customer_profiles.*.current_solution' => 'required|string', |
149 | 'customer_profiles.*.personality' => 'required|string', |
150 | 'scorecard_config' => ['required', 'array', 'min:1', function ($attribute, $value, $fail) { |
151 | $criteriaNames = array_column($value, 'name'); |
152 | if (count($criteriaNames) !== count(array_unique($criteriaNames))) { |
153 | $fail('Each criterion name within a scorecard item must be unique.'); |
154 | } |
155 | |
156 | |
157 | $totalWeight = array_sum(array_column($value, 'weight')); |
158 | if ($totalWeight !== 100) { |
159 | $fail('The total weight of all scorecard items must equal 100%.'); |
160 | } |
161 | }], |
162 | 'scorecard_config.*.name' => 'required|string', |
163 | 'scorecard_config.*.is_default' => 'required|boolean', |
164 | 'scorecard_config.*.weight' => 'required|integer|min:1|max:100', |
165 | 'scorecard_config.*.criteria' => ['required', 'array', 'min:1', function ($attribute, $value, $fail) { |
166 | $criteriaNames = array_column($value, 'name'); |
167 | if (count($criteriaNames) !== count(array_unique($criteriaNames))) { |
168 | $fail('Each criterion name within a scorecard item must be unique.'); |
169 | } |
170 | |
171 | $totalWeight = array_sum(array_column($value, 'weight')); |
172 | if ($totalWeight !== 100) { |
173 | $fail('The total weight of all criteria within a scorecard item must equal 100%.'); |
174 | } |
175 | }], |
176 | 'scorecard_config.*.criteria.*.name' => 'required|string', |
177 | 'scorecard_config.*.criteria.*.weight' => 'required|integer|min:1|max:100', |
178 | 'scorecard_config.*.criteria.*.description' => 'required|string', |
179 | 'objections' => 'required|array|min:1', |
180 | 'objections.*.category' => 'required|string', |
181 | 'objections.*.options' => 'required|array|min:1', |
182 | 'objections.*.options.*' => 'required|string', |
183 | ]; |
184 | } |
185 | |
186 | private function getProjectAverageScore(string $projectId): float |
187 | { |
188 | return RolePlayConversations::where('project_id', $projectId) |
189 | ->where('status', 'done') |
190 | ->avg('score') ?? 0.0; |
191 | } |
192 | } |