Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.50% covered (warning)
83.50%
86 / 103
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
RolePlayProjects
83.50% covered (warning)
83.50%
86 / 103
66.67% covered (warning)
66.67%
4 / 6
17.15
0.00% covered (danger)
0.00%
0 / 1
 newFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 conversations
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 user
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 calculateProgression
55.88% covered (warning)
55.88%
19 / 34
0.00% covered (danger)
0.00%
0 / 1
11.21
 getRules
96.83% covered (success)
96.83%
61 / 63
0.00% covered (danger)
0.00%
0 / 1
5
 getProjectAverageScore
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Models;
4
5use App\Http\Models\Auth\User;
6use database\factories\http\models\RolePlayProjectsFactory;
7use Illuminate\Database\Eloquent\Factories\HasFactory;
8use Illuminate\Validation\Rule;
9
10class 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}