Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.37% covered (warning)
80.37%
86 / 107
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RolePlayProjects
80.37% covered (warning)
80.37%
86 / 107
50.00% covered (danger)
50.00%
4 / 8
20.45
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
 companyProject
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeUserOwned
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 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
14    protected $table = 'role_play_projects';
15
16    public static $LEVEL_LOW = 'Low';
17
18    public static $LEVEL_MODERATE = 'Moderate';
19
20    public static $LEVEL_HIGH = 'High';
21
22    public static $LEVEL_CRITICAL = 'Critical';
23
24    public static $COMPANY_SIZE_SMALL = 'Small (10-99 employees)';
25
26    public static $COMPANY_SIZE_MEDIUM = 'Medium (100-999 employees)';
27
28    public static $COMPANY_SIZE_LARGE = 'Large (1000+ employees)';
29
30    public static $COLD_CALL = 'cold-call';
31
32    public static $DISCOVERY_CALL = 'discovery-call';
33
34    protected $fillable = [
35        'name',
36        'user_id',
37        'type',
38        'description',
39        'difficulty_level',
40        'key_features',
41        'industry',
42        'target_job_titles',
43        'scorecard_config',
44        'objections',
45        'customer_profiles',
46        'training_personalities',
47        'progression',
48        'average_score',
49        'company_project_id',
50        'is_clone',
51        'source',
52        'updated_at',
53        'created_at',
54    ];
55
56    protected static function newFactory()
57    {
58        return RolePlayProjectsFactory::new();
59    }
60
61    public function conversations()
62    {
63        return $this->hasMany(RolePlayConversations::class, 'project_id');
64    }
65
66    public function user()
67    {
68        return $this->belongsTo(User::class, 'user_id');
69    }
70
71    /**
72     * Get the company project this was cloned from, if any.
73     *
74     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
75     */
76    public function companyProject()
77    {
78        return $this->belongsTo(CompanyRolePlayProject::class, 'company_project_id');
79    }
80
81    /**
82     * Scope query to user-created projects only.
83     *
84     * @param \Illuminate\Database\Eloquent\Builder $query
85     * @return \Illuminate\Database\Eloquent\Builder
86     */
87    public function scopeUserOwned($query)
88    {
89        return $query->where(function ($q) {
90            $q->whereNull('source')->orWhere('source', 'user');
91        });
92    }
93
94    public function calculateProgression(array $feedback)
95    {
96        $project = $this;
97        $averageScore = $this->getProjectAverageScore($project->id);
98        $project->average_score = $averageScore;
99
100        $progressions = $project->progression ?? [];
101
102        foreach ($feedback['scores'] ?? [] as $score) {
103            if (empty($score['criteria']) || ! is_array($score['criteria'])) {
104                continue;
105            }
106
107            $currentProgress = collect($progressions)->first(function ($prog) use ($score) {
108                return $prog['name'] === $score['name'];
109            });
110
111            if (! $currentProgress) {
112                $progressions[] = [
113                    'name' => $score['name'],
114                    'feedback' => $score['feedback'] ?? '',
115                    'score' => max($score['score'] ?? 0, 0),
116                    'improve' => $score['improve'] ?? '',
117                    'criteria' => $score['criteria'] ?? [],
118                ];
119            } else {
120                $currentProgress['feedback'] = $score['feedback'] ?? $currentProgress['feedback'];
121                $currentProgress['score'] = max(($currentProgress['score'] + ($score['score'] ?? 0)) / 2, 0);
122
123                foreach ($score['criteria'] as $criteria) {
124                    $currentCriteria = collect($currentProgress['criteria'] ?? [])->first(function ($crit) use ($criteria) {
125                        return $crit['name'] === $criteria['name'];
126                    });
127
128                    if (! $currentCriteria) {
129                        $currentProgress['criteria'][] = [
130                            'name' => $criteria['name'],
131                            'feedback' => $criteria['feedback'] ?? '',
132                            'score' => max($criteria['score'] ?? 0, 0),
133                        ];
134                    } else {
135                        $currentCriteria['feedback'] = $criteria['feedback'] ?? $currentCriteria['feedback'];
136                        $currentCriteria['score'] = max(($currentCriteria['score'] + ($criteria['score'] ?? 0)) / 2, 0);
137                    }
138                }
139            }
140        }
141
142        $project->progression = $progressions;
143        $project->save();
144    }
145
146    public static function getRules(): array
147    {
148        return [
149            'name' => 'required|string|max:255',
150            'type' => ['required', 'string', Rule::in([RolePlayProjects::$COLD_CALL, RolePlayProjects::$DISCOVERY_CALL])],
151            'description' => 'nullable|string',
152            'difficulty_level' => 'required|integer|min:1|max:5',
153            'key_features' => 'nullable|array',
154            'key_features.*' => 'string|max:255',
155            'industry' => 'required|string',
156            'target_job_titles' => 'required|array|min:1',
157            'target_job_titles.*' => 'string|max:255',
158            'customer_profiles' => 'required|array|min:1',
159            'customer_profiles.*.id' => 'required|integer',
160            'customer_profiles.*.company_name' => 'required|string|max:255',
161            'customer_profiles.*.company_size' => ['required', 'string', Rule::in([
162                RolePlayProjects::$COMPANY_SIZE_SMALL,
163                RolePlayProjects::$COMPANY_SIZE_MEDIUM,
164                RolePlayProjects::$COMPANY_SIZE_LARGE,
165            ])],
166            'customer_profiles.*.budget' => 'required|string',
167            'customer_profiles.*.decision_making' => 'required|string',
168            'customer_profiles.*.urgency_level' => ['required', 'string', Rule::in([
169                RolePlayProjects::$LEVEL_LOW,
170                RolePlayProjects::$LEVEL_MODERATE,
171                RolePlayProjects::$LEVEL_HIGH,
172                RolePlayProjects::$LEVEL_CRITICAL,
173            ])],
174            'customer_profiles.*.openess_to_new_solutions' => ['required', 'string', Rule::in([
175                RolePlayProjects::$LEVEL_LOW,
176                RolePlayProjects::$LEVEL_MODERATE,
177                RolePlayProjects::$LEVEL_HIGH,
178                RolePlayProjects::$LEVEL_CRITICAL,
179            ])],
180            'customer_profiles.*.communication_style' => 'required|string',
181            'customer_profiles.*.pain_points' => 'required|string',
182            'customer_profiles.*.current_solution' => 'required|string',
183            'customer_profiles.*.personality' => 'required|string',
184            'scorecard_config' => ['required', 'array', 'min:1', function ($attribute, $value, $fail) {
185                $criteriaNames = array_column($value, 'name');
186                if (count($criteriaNames) !== count(array_unique($criteriaNames))) {
187                    $fail('Each criterion name within a scorecard item must be unique.');
188                }
189
190                $totalWeight = array_sum(array_column($value, 'weight'));
191                if ($totalWeight !== 100) {
192                    $fail('The total weight of all scorecard items must equal 100%.');
193                }
194            }],
195            'scorecard_config.*.name' => 'required|string',
196            'scorecard_config.*.is_default' => 'required|boolean',
197            'scorecard_config.*.weight' => 'required|integer|min:1|max:100',
198            'scorecard_config.*.criteria' => ['required', 'array', 'min:1', function ($attribute, $value, $fail) {
199                $criteriaNames = array_column($value, 'name');
200                if (count($criteriaNames) !== count(array_unique($criteriaNames))) {
201                    $fail('Each criterion name within a scorecard item must be unique.');
202                }
203
204                $totalWeight = array_sum(array_column($value, 'weight'));
205                if ($totalWeight !== 100) {
206                    $fail('The total weight of all criteria within a scorecard item must equal 100%.');
207                }
208            }],
209            'scorecard_config.*.criteria.*.name' => 'required|string',
210            'scorecard_config.*.criteria.*.weight' => 'required|integer|min:1|max:100',
211            'scorecard_config.*.criteria.*.description' => 'required|string',
212            'objections' => 'required|array|min:1',
213            'objections.*.category' => 'required|string',
214            'objections.*.options' => 'required|array|min:1',
215            'objections.*.options.*' => 'required|string',
216        ];
217    }
218
219    private function getProjectAverageScore(string $projectId): float
220    {
221        return RolePlayConversations::where('project_id', $projectId)
222            ->where('status', 'done')
223            ->avg('score') ?? 0.0;
224    }
225}