Passed
Push — feature/back-to-summary-button... ( b22cf7...c47ef5 )
by Yonathan
09:27
created

basicInfoSimpleRules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 5
c 2
b 0
f 0
dl 0
loc 8
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace App\Services\Validation;
4
5
use App\Models\ExperienceSkill;
6
use App\Models\JobApplication;
7
use App\Models\Lookup\CriteriaType;
8
use App\Models\Lookup\LanguageRequirement;
9
use App\Models\Lookup\SkillType;
10
use App\Services\Validation\JobApplicationAnswerValidator;
11
use App\Services\Validation\Rules\ContainsObjectWithAttributeRule;
12
use App\Services\Validation\Rules\IncludeAllRule;
13
use App\Services\Validation\Rules\IncludesAllRule;
14
use App\Services\Validation\Rules\WordLimitRule;
15
use Illuminate\Database\Eloquent\Builder;
16
use Illuminate\Support\Facades\Validator;
17
18
class ApplicationTimelineValidator
19
{
20
    /**
21
     * @var mixed $backendRules Rule array shared by validator()
22
     * and detailedValidationErrors(). Requires a Job Poster ID,
23
     * an Application Status ID, and an Applicant ID
24
     */
25
    public $backendRules = [
26
        'job_poster_id' => 'required|integer',
27
        'application_status_id' => 'required|integer',
28
        'applicant_id' => 'required|integer',
29
    ];
30
31
    /* Step 1 ------------------------------------------------------------------------------------------------------- */
32
33
    /**
34
     * Validation rules for the Basic Info step.
35
     *
36
     * @return mixed[]
37
     */
38
    protected function basicInfoSimpleRules()
39
    {
40
        // Field 'language_test_confirmed' needs to be validated conditionally.
41
        return [
42
            'language_requirement_confirmed' => ['required', 'boolean', 'accepted'],
43
            'citizenship_declaration_id' => ['required', 'exists:citizenship_declarations,id'],
44
            'veteran_status_id' => ['required', 'exists:veteran_statuses,id'],
45
            'education_requirement_confirmed' => ['required', 'boolean', 'accepted'],
46
        ];
47
    }
48
49
    /**
50
     * Validator instance for the Basic Info step.
51
     *
52
     * @param JobApplication $application Job Application object.
53
     *
54
     * @return \Illuminate\Contracts\Validation\Validator
55
     */
56
    public function basicsValidator(JobApplication $application)
57
    {
58
        $application->loadMissing('job_poster');
59
60
        // Check to see if this application is related to a Job Poster with
61
        // a bilingual requirement.
62
        $langRequirement = $application->job_poster->language_requirement_id;
63
        $langTestRequirement = LanguageRequirement::where('name', 'bilingual_intermediate')
64
            ->orWhere('name', 'bilingual_advanced')
65
            ->pluck('id');
66
67
        $validator = Validator::make($application->toArray(), $this->basicInfoSimpleRules());
68
69
        // Conditionally check for 'language_test_confirmed' if the
70
        // closure returns true.
71
        $validator->sometimes(
72
            'language_test_confirmed',
73
            'required|boolean|accepted',
74
            function ($input) use ($langRequirement, $langTestRequirement) {
75
                return in_array($langRequirement, $langTestRequirement->toArray());
76
            }
77
        );
78
79
        return $validator;
80
    }
81
82
    /**
83
     * Helper function to return completeness for the Basic Info step.
84
     *
85
     * @param JobApplication $application Job Application object.
86
     *
87
     * @return boolean
88
     */
89
    public function basicsComplete(JobApplication $application)
90
    {
91
        $validator = $this->basicsValidator($application);
92
        return $validator->passes();
93
    }
94
95
    /* End Step 1 --------------------------------------------------------------------------------------------------- */
96
97
    /* Step 2 ------------------------------------------------------------------------------------------------------- */
98
99
    /**
100
     * Validator instance for the Experience step.
101
     *
102
     * @param JobApplication $application Job Application object.
103
     *
104
     * @return \Illuminate\Contracts\Validation\Validator
105
     */
106
    public function experienceValidator(JobApplication $application)
107
    {
108
        $application->loadMissing('job_poster', 'job_poster.criteria');
109
110
        $essentialCriteriaType = CriteriaType::where('name', 'essential')->first()->id;
111
        $hardSkillType = SkillType::where('name', 'hard')->first()->id;
112
113
        $jobCriteria = $application->job_poster->criteria;
114
        $requiredCriteriaSkillIds = $jobCriteria
115
            ->filter(function ($criterion) use ($essentialCriteriaType, $hardSkillType) {
116
                return $criterion->criteria_type_id === $essentialCriteriaType
117
                    && $criterion->skill->skill_type_id === $hardSkillType;
118
            })
119
            ->pluck('skill_id')
120
            ->all();
121
122
        // Get all Experiences belonging to the application, or applicant (if draft),
123
        // that are assigned to required Criteria.
124
        $experiences = ExperienceSkill::whereHasMorph(
125
            'experience',
126
            '*',
127
            function (Builder $query) use ($application): void {
128
                $query->where([
129
                    ['experienceable_type', $application->isDraft() ? 'applicant' : 'application'],
130
                    ['experienceable_id', $application->isDraft() ? $application->applicant->id : $application->id]
131
                ]);
132
            }
133
        )
134
        ->whereIn('skill_id', $requiredCriteriaSkillIds)
135
        ->get();
136
137
        $validator = Validator::make(
138
            ['skill_ids' => $experiences->pluck('skill_id')->all()],
139
            ['skill_ids' => new IncludesAllRule($requiredCriteriaSkillIds)]
140
        );
141
142
        return $validator;
143
    }
144
145
    /**
146
     * Helper function to return completeness for the Experience step.
147
     *
148
     * @param JobApplication $application Job Application object.
149
     *
150
     * @return boolean
151
     */
152
    public function experienceComplete(JobApplication $application)
153
    {
154
        $validator = $this->experienceValidator($application);
155
        return $validator->passes();
156
    }
157
158
    /* End Step 2 --------------------------------------------------------------------------------------------------- */
159
160
    /* Step 3 ------------------------------------------------------------------------------------------------------- */
161
162
    /**
163
     * Validator instance for the Skills step.
164
     *
165
     * @param JobApplication $application Job Application object.
166
     *
167
     * @return \Illuminate\Contracts\Validation\Validator
168
     */
169
    public function skillsValidator(JobApplication $application)
170
    {
171
        $application->loadMissing('job_poster', 'job_poster.criteria');
172
173
        $essentialCriteriaType = CriteriaType::where('name', 'essential')->first()->id;
174
        $hardSkillType = SkillType::where('name', 'hard')->first()->id;
175
176
        $jobCriteria = $application->job_poster->criteria;
177
        $requiredCriteriaSkillIds = $jobCriteria
178
            ->filter(function ($criterion) use ($essentialCriteriaType, $hardSkillType) {
179
                return $criterion->criteria_type_id === $essentialCriteriaType
180
                    && $criterion->skill->skill_type_id === $hardSkillType;
181
            })
182
            ->pluck('skill_id')
183
            ->all();
184
185
        // Get all Experiences belonging to the application, or applicant (if draft),
186
        // that are assigned to required Criteria.
187
        $experiences = ExperienceSkill::whereHasMorph(
188
            'experience',
189
            '*',
190
            function (Builder $query) use ($application): void {
191
                $query->where([
192
                    ['experienceable_type', $application->isDraft() ? 'applicant' : 'application'],
193
                    ['experienceable_id', $application->isDraft() ? $application->applicant->id : $application->id]
194
                ]);
195
            }
196
        )
197
        ->whereIn('skill_id', $requiredCriteriaSkillIds)
198
        ->get();
199
200
        $validator = Validator::make(
201
            [
202
                'experiences' => $experiences->toArray(),
203
                'skill_ids' => $experiences->pluck('skill_id')->all()
204
            ],
205
            [
206
                'experiences' => 'required',
207
                'skill_ids' => new IncludesAllRule(($requiredCriteriaSkillIds)),
208
                'experiences.*.justification' => ['required', 'string', new WordLimitRule(100)],
209
            ]
210
        );
211
212
213
        return $validator;
214
    }
215
216
    /**
217
     * Helper function to return completeness for the Skills step.
218
     *
219
     * @param JobApplication $application Job Application object.
220
     *
221
     * @return boolean
222
     */
223
    public function skillsComplete(JobApplication $application)
224
    {
225
        $validator = $this->skillsValidator($application);
226
        return $validator->passes();
227
    }
228
229
    /* End Step 3 --------------------------------------------------------------------------------------------------- */
230
231
    /* Step 4 ------------------------------------------------------------------------------------------------------- */
232
233
    /**
234
     * Validator instance for the Fit step.
235
     *
236
     * @param JobApplication $application Job Application object.
237
     *
238
     * @return \Illuminate\Contracts\Validation\Validator
239
     */
240
    public function fitValidator(JobApplication $application)
241
    {
242
        // Load application answers so they are included in application->toArray().
243
        $application->load('job_application_answers');
244
245
        // Start with Answer rules, that ensure each answer is complete.
246
        $answerValidator = new JobApplicationAnswerValidator($application);
247
248
        $rules = $this->addNestedValidatorRules(
249
            'job_application_answers.*',
250
            $answerValidator->rules(),
251
            [ 'job_application_answers.*.answer' => ['required', 'string', new WordLimitRule(250)]]
252
        );
253
254
        // Validate that each question has been answered.
255
        $jobPosterQuestionRules = [];
256
        foreach ($application->job_poster->job_poster_questions as $question) {
257
            $jobPosterQuestionRules[] = new ContainsObjectWithAttributeRule('job_poster_question_id', $question->id);
258
        }
259
        $rules['job_application_answers'] = $jobPosterQuestionRules;
260
261
        $validator = Validator::make($application->toArray(), $rules);
262
263
        return $validator;
264
    }
265
266
    /**
267
     * Helper function to return completeness for the Fit step.
268
     *
269
     * @param JobApplication $application Job Application object.
270
     *
271
     * @return boolean
272
     */
273
    public function fitComplete(JobApplication $application)
274
    {
275
        $validator = $this->fitValidator($application);
276
        return $validator->passes();
277
    }
278
279
    /* End Step 4 --------------------------------------------------------------------------------------------------- */
280
281
    /* Step 5 ------------------------------------------------------------------------------------------------------- */
282
283
    /**
284
     * Validator instance for the entire Application process.
285
     *
286
     * @param JobApplication $application Job Application object.
287
     *
288
     * @return \Illuminate\Contracts\Validation\Validator
289
     */
290
    public function validator(JobApplication $application)
291
    {
292
        $data = $application->toArray();
293
294
        $rules = $this->backendRules;
295
296
        // Combining and simplifying error messages.
297
        $rules = array_merge(
298
            $rules,
299
            ['timeline_step_1' => 'required|boolean|accepted'],
300
            ['timeline_step_2' => 'required|boolean|accepted'],
301
            ['timeline_step_3' => 'required|boolean|accepted'],
302
            ['timeline_step_4' => 'required|boolean|accepted'],
303
            ['timeline_step_5' => 'required|boolean|accepted']
304
        );
305
        $data = array_merge(
306
            $data,
307
            ['timeline_step_1' => $this->basicsComplete($application)],
308
            ['timeline_step_2' => $this->experienceComplete($application)],
309
            ['timeline_step_3' => $this->skillsComplete($application)],
310
            ['timeline_step_4' => $this->fitComplete($application)],
311
            ['timeline_step_5' => $this->affirmationComplete($application)]
312
        );
313
314
        // Validate basic data is filled in.
315
        return Validator::make($data, $rules);
316
    }
317
318
    public function validate(JobApplication $application)
319
    {
320
        $this->validator($application)->validate();
321
    }
322
323
    public function validateComplete(JobApplication $application)
324
    {
325
        $validator = $this->validator($application);
326
        return $validator->passes();
327
    }
328
329
    /* End Step 5 --------------------------------------------------------------------------------------------------- */
330
331
    /* Step 6 ------------------------------------------------------------------------------------------------------- */
332
333
    /**
334
     * Validator instance for the Final Submit step.
335
     *
336
     * @param JobApplication $application Job Application object.
337
     *
338
     * @return \Illuminate\Contracts\Validation\Validator
339
     */
340
    public function affirmationValidator(JobApplication $application)
341
    {
342
        return Validator::make($application->toArray(), [
343
            'submission_signature' => [
344
                'required',
345
                'string',
346
                'max:191',
347
            ],
348
            'submission_date' => [
349
                'required',
350
                'string',
351
                'max:191',
352
            ]
353
        ]);
354
    }
355
356
    /**
357
     * Helper function to return completeness for the Final Submit step.
358
     *
359
     * @param JobApplication $application Job Application object.
360
     *
361
     * @return boolean
362
     */
363
    public function affirmationComplete(JobApplication $application)
364
    {
365
        return $this->affirmationValidator($application)->passes();
366
    }
367
368
    /* End Step 6 --------------------------------------------------------------------------------------------------- */
369
370
    /* Helpers ------------------------------------------------------------------------------------------------------ */
371
372
    /**
373
     * Helper function to return detailed error messages for each step.
374
     *
375
     * @param JobApplication $application Job Application object.
376
     *
377
     * @return mixed
378
     */
379
    public function detailedValidatorErrors(JobApplication $application)
380
    {
381
        return array_merge(
382
            Validator::make($application->toArray(), $this->backendRules)->errors()->all(),
383
            $this->basicsValidator($application)->errors()->all(),
384
            $this->experienceValidator($application)->errors()->all(),
385
            $this->skillsValidator($application)->errors()->all(),
386
            $this->fitValidator($application)->errors()->all(),
387
            $this->affirmationValidator($application)->errors()->all()
388
        );
389
    }
390
391
    /**
392
     * Return a copy of $array, with function $fn applied to each key, but values left unchanged.
393
     *
394
     * @param function $fn    Function applied to each key.
395
     * @param mixed    $array Array to operate on.
396
     * @return array
397
     */
398
    protected function arrayMapKeys($fn, $array): array
399
    {
400
        $newArray = [];
401
        foreach ($array as $key => $value) {
402
            $newArray[$fn($key)] = $value;
403
        }
404
        return $newArray;
405
    }
406
407
    /**
408
     * Return a copy of $array, with function $fn applied to each key, but values left unchanged.
409
     *
410
     * @param string $nestedAttribute Attribute name to apply to each rule.
411
     * @param mixed  $validatorRules  Rules array that requires an attribute name to be prepended.
412
     * @param mixed  $rules           Optional existing validation rules to merge with.
413
     *
414
     * @return array
415
     */
416
    protected function addNestedValidatorRules($nestedAttribute, $validatorRules, $rules = [])
417
    {
418
        // Prepend the attribute name of each validator rule with the nested attribute name.
419
        $newRules = $this->arrayMapKeys(
420
            function ($key) use ($nestedAttribute) {
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type App\Services\Validation\function expected by parameter $fn of App\Services\Validation\...lidator::arrayMapKeys(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

420
            /** @scrutinizer ignore-type */ function ($key) use ($nestedAttribute) {
Loading history...
421
                return implode('.', [$nestedAttribute, $key]);
422
            },
423
            $validatorRules
424
        );
425
        // Merge new rules with old rules.
426
        $rules = array_merge($newRules, $rules);
427
        return $rules;
428
    }
429
    /* End Helpers -------------------------------------------------------------------------------------------------- */
430
}
431