Passed
Push — task/application-handle-step-s... ( 5f39f9 )
by Yonathan
08:23
created

JobApplication   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 513
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 63
eloc 248
c 4
b 0
f 0
dl 0
loc 513
rs 3.36

33 Methods

Rating   Name   Duplication   Size   Complexity  
A work_experiences() 0 3 1
A projects() 0 3 1
A saveProfileSnapshot() 0 77 4
A references() 0 3 1
A experiences_education() 0 4 1
A job_application_version() 0 3 1
A experiences_work() 0 4 1
A preferred_language() 0 3 1
A meetsEssentialCriteria() 0 18 6
A saveProfileSnapshotTimeline() 0 40 4
A courses() 0 3 1
A applicant_snapshot() 0 3 1
A job_poster() 0 3 1
A citizenship_declaration() 0 3 1
A application_review() 0 3 1
A applicationTimelineSteps() 0 31 3
A degrees() 0 3 1
A veteran_status() 0 3 1
A job_application_steps() 0 8 1
A experiences_personal() 0 4 1
A isDraft() 0 3 1
C getSectionStatus() 0 48 17
A getMeetsEssentialCriteriaAttribute() 0 3 1
A applicant() 0 3 1
A experiences_community() 0 4 1
A application_status() 0 3 1
A security_clearance() 0 3 1
A createApplicantSnapshot() 0 5 1
A attachSteps() 0 16 2
A experiences_award() 0 3 1
A work_samples() 0 3 1
A job_application_answers() 0 3 1
A skill_declarations() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like JobApplication often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JobApplication, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Created by Reliese Model.
5
 * Date: Thu, 12 Jul 2018 22:39:27 +0000.
6
 */
7
8
namespace App\Models;
9
10
use App\Events\ApplicationRetrieved;
11
use App\Events\ApplicationSaved;
12
use App\Models\Applicant;
13
use App\Models\ApplicationReview;
14
use App\Models\Lookup\Step;
15
use App\Services\Validation\ApplicationValidator;
16
use App\Services\Validation\StrategicResponseApplicationValidator;
17
use Illuminate\Notifications\Notifiable;
18
use App\Traits\TalentCloudCrudTrait as CrudTrait;
19
20
/**
21
 * Class JobApplication
22
 *
23
 * @property int $id
24
 * @property int $job_poster_id
25
 * @property int $application_status_id
26
 * @property int $citizenship_declaration_id
27
 * @property int $veteran_status_id
28
 * @property int $preferred_language_id
29
 * @property int $applicant_id
30
 * @property int $applicant_snapshot_id
31
 * @property string $submission_signature
32
 * @property string $submission_date
33
 * @property boolean $experience_saved
34
 * @property boolean $language_requirement_confirmed
35
 * @property boolean $language_test_confirmed
36
 * @property boolean $education_requirement_confirmed
37
 * @property string $user_name
38
 * @property string $user_email
39
 * @property int $version_id
40
 * @property string $director_name
41
 * @property string $director_title
42
 * @property string $director_email
43
 * @property string $reference_name
44
 * @property string $reference_title
45
 * @property string $reference_email
46
 * @property string $gov_email
47
 * @property boolean $physical_office_willing
48
 * @property int $security_clearance_id
49
 * @property boolean $share_with_mangers
50
 * @property \Jenssegers\Date\Date $created_at
51
 * @property \Jenssegers\Date\Date $updated_at
52
 *
53
 * @property \App\Models\Applicant $applicant
54
 * @property \App\Models\Applicant $applicant_snapshot
55
 * @property \App\Models\Lookup\ApplicationStatus $application_status
56
 * @property \App\Models\Lookup\CitizenshipDeclaration $citizenship_declaration
57
 * @property \App\Models\Lookup\VeteranStatus $veteran_status
58
 * @property \App\Models\Lookup\PreferredLanguage $preferred_language
59
 * @property \App\Models\JobPoster $job_poster
60
 * @property \Illuminate\Database\Eloquent\Collection $job_application_answers
61
 * @property \Illuminate\Database\Eloquent\Collection $skill_declarations
62
 * @property \App\Models\ApplicationReview $application_review
63
 * @property \Illuminate\Database\Eloquent\Collection $degrees
64
 * @property \Illuminate\Database\Eloquent\Collection $courses
65
 * @property \Illuminate\Database\Eloquent\Collection $work_experiences
66
 * @property \Illuminate\Database\Eloquent\Collection $references
67
 * @property \Illuminate\Database\Eloquent\Collection $work_samples
68
 * @property \Illuminate\Database\Eloquent\Collection $projects
69
 * @property \App\Models\JobApplicationVersion $job_application_version
70
 * @property \App\Models\Lookup\SecurityClearance $security_clearance
71
 *
72
 * Version 2 application models.
73
 * @property \Illuminate\Database\Eloquent\Collection $experiences_work
74
 * @property \Illuminate\Database\Eloquent\Collection $experiences_personal
75
 * @property \Illuminate\Database\Eloquent\Collection $experiences_education
76
 * @property \Illuminate\Database\Eloquent\Collection $experiences_award
77
 * @property \Illuminate\Database\Eloquent\Collection $experiences_community
78
 * @property \Illuminate\Database\Eloquent\Collection $job_application_steps
79
 */
80
class JobApplication extends BaseModel
81
{
82
    // Trait for Backpack.
83
    use CrudTrait;
84
85
    use Notifiable;
86
87
    protected $dispatchesEvents = [
88
        'retrieved' => ApplicationRetrieved::class,
89
        'saved' => ApplicationSaved::class,
90
    ];
91
92
    protected $casts = [
93
        'job_poster_id' => 'int',
94
        'application_status_id' => 'int',
95
        'citizenship_declaration_id' => 'int',
96
        'veteran_status_id' => 'int',
97
        'preferred_language_id' => 'int',
98
        'applicant_id' => 'int',
99
        'applicant_snapshot_id' => 'int',
100
        'submission_signature' => 'string',
101
        'submission_date' => 'string',
102
        'experience_saved' => 'boolean',
103
        'language_requirement_confirmed' => 'boolean',
104
        'language_test_confirmed' => 'boolean',
105
        'education_requirement_confirmed' => 'boolean',
106
        'version_id' => 'int',
107
        'director_name' => 'string',
108
        'director_title' => 'string',
109
        'director_email' => 'string',
110
        'reference_name' => 'string',
111
        'reference_title' => 'string',
112
        'reference_email' => 'string',
113
        'gov_email' => 'string',
114
        'physical_office_willing' => 'boolean',
115
        'security_clearance_id' => 'int',
116
        'share_with_managers' => 'boolean',
117
    ];
118
    protected $fillable = [
119
        'citizenship_declaration_id',
120
        'veteran_status_id',
121
        'preferred_language_id',
122
        'language_requirement_confirmed',
123
        'language_test_confirmed',
124
        'education_requirement_confirmed',
125
        'veteran_status_id',
126
        'preferred_language_id',
127
        'submission_signature',
128
        'submission_date',
129
        'experience_saved',
130
        'director_name',
131
        'director_title',
132
        'director_email',
133
        'reference_name',
134
        'reference_title',
135
        'reference_email',
136
        'gov_email',
137
        'physical_office_willing',
138
        'security_clearance_id',
139
        'share_with_managers',
140
    ];
141
142
    /**
143
     * The accessors to append to the model's array/json form.
144
     *
145
     * @var array
146
     */
147
    protected $appends = ['meets_essential_criteria'];
148
149
    protected function createApplicantSnapshot($applicant_id)
150
    {
151
        $applicant = Applicant::where('id', $applicant_id)->firstOrFail();
152
153
        $snapshot = $applicant->replicate();
0 ignored issues
show
Unused Code introduced by
The assignment to $snapshot is dead and can be removed.
Loading history...
154
    }
155
156
    public function applicant()
157
    {
158
        return $this->belongsTo(\App\Models\Applicant::class);
159
    }
160
161
    public function applicant_snapshot() //phpcs:ignore
162
    {
163
        return $this->belongsTo(\App\Models\Applicant::class, 'applicant_snapshot_id');
164
    }
165
166
    public function application_status() //phpcs:ignore
167
    {
168
        return $this->belongsTo(\App\Models\Lookup\ApplicationStatus::class);
169
    }
170
171
    public function citizenship_declaration() //phpcs:ignore
172
    {
173
        return $this->belongsTo(\App\Models\Lookup\CitizenshipDeclaration::class);
174
    }
175
176
    public function veteran_status() //phpcs:ignore
177
    {
178
        return $this->belongsTo(\App\Models\Lookup\VeteranStatus::class);
179
    }
180
181
    public function preferred_language() //phpcs:ignore
182
    {
183
        return $this->belongsTo(\App\Models\Lookup\PreferredLanguage::class);
184
    }
185
186
    public function job_poster() //phpcs:ignore
187
    {
188
        return $this->belongsTo(\App\Models\JobPoster::class);
189
    }
190
191
    public function job_application_answers() //phpcs:ignore
192
    {
193
        return $this->hasMany(\App\Models\JobApplicationAnswer::class);
194
    }
195
196
    public function skill_declarations() //phpcs:ignore
197
    {
198
        return $this->morphMany(\App\Models\SkillDeclaration::class, 'skillable');
199
    }
200
201
    public function application_review() //phpcs:ignore
202
    {
203
        return $this->hasOne(ApplicationReview::class);
204
    }
205
206
    public function degrees()
207
    {
208
        return $this->morphMany(\App\Models\Degree::class, 'degreeable')->orderBy('end_date', 'desc');
209
    }
210
211
    public function courses()
212
    {
213
        return $this->morphMany(\App\Models\Course::class, 'courseable')->orderBy('end_date', 'desc');
214
    }
215
216
    public function work_experiences() //phpcs:ignore
217
    {
218
        return $this->morphMany(\App\Models\WorkExperience::class, 'experienceable')->orderBy('end_date', 'desc');
219
    }
220
221
    public function references()
222
    {
223
        return $this->morphMany(\App\Models\Reference::class, 'referenceable');
224
    }
225
226
    public function projects()
227
    {
228
        return $this->morphMany(\App\Models\Project::class, 'projectable');
229
    }
230
231
    public function work_samples() //phpcs:ignore
232
    {
233
        return $this->morphMany(\App\Models\WorkSample::class, 'work_sampleable');
234
    }
235
236
    public function job_application_version() //phpcs:ignore
237
    {
238
        return $this->hasOne(\App\Models\JobApplicationVersion::class);
239
    }
240
241
    public function security_clearance() //phpcs:ignore
242
    {
243
        return $this->belongsTo(\App\Models\Lookup\SecurityClearance::class);
244
    }
245
246
    // Version 2 application models.
247
    public function experiences_work() //phpcs:ignore
248
    {
249
        return $this->morphMany(\App\Models\ExperienceWork::class, 'experienceable')
250
            ->orderBy('end_date', 'desc');
251
    }
252
253
    public function experiences_personal() //phpcs:ignore
254
    {
255
        return $this->morphMany(\App\Models\ExperiencePersonal::class, 'experienceable')
256
            ->orderBy('end_date', 'desc');
257
    }
258
259
    public function experiences_education() //phpcs:ignore
260
    {
261
        return $this->morphMany(\App\Models\ExperienceEducation::class, 'experienceable')
262
            ->orderBy('end_date', 'desc');
263
    }
264
265
    public function experiences_award() //phpcs:ignore
266
    {
267
        return $this->morphMany(\App\Models\ExperienceAward::class, 'experienceable');
268
    }
269
270
    public function experiences_community() //phpcs:ignore
271
    {
272
        return $this->morphMany(\App\Models\ExperienceCommunity::class, 'experienceable')
273
            ->orderBy('end_date', 'desc');
274
    }
275
276
    public function job_application_steps() //phpcs:ignore
277
    {
278
        return $this->belongsToMany(
279
            \App\Models\Lookup\Step::class,
280
            'job_application_steps',
281
            'job_application_id',
282
            'step_id'
283
        )->withPivot('touched');
284
    }
285
286
    /**
287
     * Return either 'complete', 'incomplete' or 'error', depending on the
288
     * status of the requested section.
289
     *
290
     * @param  string $section Should be one of:
291
     *                              'basics'
292
     *                              'experience'
293
     *                              'essential_skills'
294
     *                              'asset_skills'
295
     *                              'preview'
296
     *
297
     * @return string $status   'complete', 'incomplete' or 'error'
298
     */
299
    public function getSectionStatus(string $section)
300
    {
301
        // TODO: determine whether sections are complete or invalid
302
        $jobPoster = $this->job_poster;
303
        $validator = $jobPoster->isInStrategicResponseDepartment()
304
            ? new StrategicResponseApplicationValidator()
305
            : new ApplicationValidator();
306
        $status = 'incomplete';
307
        switch ($section) {
308
            case 'basics':
309
                if ($validator->basicsComplete($this)) {
310
                    $status = 'complete';
311
                }
312
                break;
313
            case 'experience':
314
                if ($validator->experienceComplete($this)) {
315
                    $status = 'complete';
316
                }
317
                break;
318
            case 'essential_skills':
319
                if ($validator->essentialSkillsComplete($this)) {
320
                    $status = 'complete';
321
                }
322
                break;
323
            case 'asset_skills':
324
                if ($validator->assetSkillsComplete($this)) {
325
                    $status = 'complete';
326
                }
327
                break;
328
            case 'preview':
329
                if ($validator->basicsComplete($this) &&
330
                    $validator->experienceComplete($this) &&
331
                    $validator->essentialSkillsComplete($this) &&
332
                    $validator->assetSkillsComplete($this)
333
                ) {
334
                    $status = 'complete';
335
                }
336
                break;
337
            case 'confirm':
338
                if ($validator->affirmationComplete($this)) {
339
                    $status = 'complete';
340
                }
341
                break;
342
            default:
343
                $status = 'error';
344
                break;
345
        }
346
        return $status;
347
    }
348
349
    /**
350
     * Check if the status of the application is 'draft'
351
     *
352
     * @return boolean
353
     */
354
    public function isDraft(): bool
355
    {
356
        return $this->application_status->name === 'draft';
357
    }
358
359
    /**
360
     * Returns true if this meets all the HARD SKILL essential criteria.
361
     * That means it has attached an SkillDeclaration for each essential criterion,
362
     * with a level at least as high as the required level.
363
     * NOTE: If this application is in draft status, it will use
364
     *  SkillDeclarations from the the applicants profile for this check.
365
     *
366
     * @return boolean
367
     */
368
    public function meetsEssentialCriteria(): bool
369
    {
370
        $essentialCriteria = $this->job_poster->criteria->filter(
371
            function ($value, $key) {
372
                return $value->criteria_type->name == 'essential'
373
                    && $value->skill->skill_type->name == 'hard';
374
            }
375
        );
376
        $source = $this->isDraft() ? $this->applicant : $this;
377
        foreach ($essentialCriteria as $criterion) {
378
            $skillDeclaration = $source->skill_declarations->where('skill_id', $criterion->skill_id)->first();
379
            if ($skillDeclaration === null ||
380
                $skillDeclaration->skill_level_id < $criterion->skill_level_id
381
            ) {
382
                return false;
383
            }
384
        }
385
        return true;
386
    }
387
388
    /**
389
     * Accessor for meetsEssentialCriteria function, which
390
     * allows this value to be automatically appended to array/json representation.
391
     *
392
     * @return boolean
393
     */
394
    public function getMeetsEssentialCriteriaAttribute(): bool
395
    {
396
        return $this->meetsEssentialCriteria();
397
    }
398
399
    /**
400
     * Save copies of all relevant profile data to this application.
401
     *
402
     *
403
     * @return void
404
     */
405
    public function saveProfileSnapshot(): void
406
    {
407
        $applicant = $this->applicant->fresh();
408
409
        $this->user_name = $applicant->user->full_name;
410
        $this->user_email = $applicant->user->email;
411
        $this->save();
412
413
        // Delete previous snapshot.
414
        $this->degrees()->delete();
415
        $this->courses()->delete();
416
        $this->work_experiences()->delete();
417
        $this->projects()->delete();
418
        $this->references()->delete();
419
        $this->work_samples()->delete();
420
        $this->skill_declarations()->delete();
421
422
        $this->degrees()->saveMany($applicant->degrees->map->replicate());
423
        $this->courses()->saveMany($applicant->courses->map->replicate());
424
        $this->work_experiences()->saveMany($applicant->work_experiences->map->replicate());
425
426
        $copyWithHistory = function ($model) {
427
            return [
428
                'old' => $model,
429
                'new' => $model->replicate()
430
            ];
431
        };
432
433
        $projectMap = $applicant->projects->map($copyWithHistory);
434
        $referenceMap = $applicant->references->map($copyWithHistory);
435
        $workSampleMap = $applicant->work_samples->map($copyWithHistory);
436
        $skillDeclarationMap = $applicant->skill_declarations->map($copyWithHistory);
437
438
        // First link new projects, references, work samples and skill declarations to this application.
439
        $this->projects()->saveMany($projectMap->pluck('new'));
440
        $this->references()->saveMany($referenceMap->pluck('new'));
441
        $this->work_samples()->saveMany($workSampleMap->pluck('new'));
442
        $this->skill_declarations()->saveMany($skillDeclarationMap->pluck('new'));
443
444
        $findNewFromOld = function ($mapping, $old) {
445
            $matchingItem = $mapping->first(function ($value) use ($old) {
446
                return $value['old']->id === $old->id;
447
            });
448
            return $matchingItem['new'];
449
        };
450
451
        // Replicate copies shallow attributes, but not relationships. We have to copy those ourselves.
452
        $findNewReferenceFromOld = function ($old) use ($findNewFromOld, $referenceMap) {
453
            return $findNewFromOld($referenceMap, $old);
454
        };
455
456
        $findNewSkillDeclarationFromOld = function ($old) use ($findNewFromOld, $skillDeclarationMap) {
457
            return $findNewFromOld($skillDeclarationMap, $old);
458
        };
459
460
        // Link projects and references.
461
        foreach ($projectMap as $item) {
462
            $old = $item['old'];
463
            $newProj = $item['new'];
464
            $newReferences = $old->references->map($findNewReferenceFromOld);
465
            $newProj->references()->sync($newReferences);
466
        }
467
468
        // Link references and skills.
469
        foreach ($referenceMap as $item) {
470
            $old = $item['old'];
471
            $newRef = $item['new'];
472
            $newSkillDecs = $old->skill_declarations->map($findNewSkillDeclarationFromOld);
473
            $newRef->skill_declarations()->sync($newSkillDecs);
474
        }
475
476
        // Link work samples and skills.
477
        foreach ($workSampleMap as $item) {
478
            $old = $item['old'];
479
            $newSample = $item['new'];
480
            $newSkillDecs = $old->skill_declarations->map($findNewSkillDeclarationFromOld);
481
            $newSample->skill_declarations()->sync($newSkillDecs);
482
        }
483
    }
484
485
    /**
486
     * Save copies of Experiences and its linked skills (ExperienceSkills) to this application.
487
     *
488
     * @return void
489
     */
490
    public function saveProfileSnapshotTimeline(): void
491
    {
492
        $this->refresh();
493
        $applicant = $this->applicant->fresh();
494
495
        $this->user_name = $applicant->user->full_name;
496
        $this->user_email = $applicant->user->email;
497
        $this->save();
498
499
        $deleteExperiences = function ($experiences) {
500
            foreach ($experiences as $experience) {
501
                $experience->delete();
502
            }
503
        };
504
505
        // Delete experiences in previous snapshot.
506
        $deleteExperiences($this->experiences_award);
507
        $deleteExperiences($this->experiences_community);
508
        $deleteExperiences($this->experiences_education);
509
        $deleteExperiences($this->experiences_personal);
510
        $deleteExperiences($this->experiences_work);
511
512
        $replicateAndSaveExperience = function ($experiences, $experience_type) {
513
            // Iterate through applicant experiences, replicate the experience, and save to the application.
514
            foreach ($experiences as $experience) {
515
                $experienceCopy = $experience->replicate();
516
                $this->{$experience_type}()->save($experienceCopy);
517
                // Iterate through original experience experienceSkills list, replicate it, and save to the new copy.
518
                foreach ($experience->experience_skills as $experienceSkill) {
519
                    $experienceSkillCopy = $experienceSkill->replicate();
520
                    $experienceCopy->experience_skills()->save($experienceSkillCopy);
521
                }
522
            }
523
        };
524
525
        $replicateAndSaveExperience($applicant->experiences_award, 'experiences_award');
526
        $replicateAndSaveExperience($applicant->experiences_community, 'experiences_community');
527
        $replicateAndSaveExperience($applicant->experiences_education, 'experiences_education');
528
        $replicateAndSaveExperience($applicant->experiences_personal, 'experiences_personal');
529
        $replicateAndSaveExperience($applicant->experiences_work, 'experiences_work');
530
    }
531
532
    /**
533
     * Attach steps to new application (version 2).
534
     *
535
     * @return void
536
    */
537
    public function attachSteps(): void
538
    {
539
        $basicStep = Step::where('name', 'basic')->first();
540
        $experienceStep = Step::where('name', 'experience')->first();
541
        $skillsStep = Step::where('name', 'skills')->first();
542
        $fitStep = Step::where('name', 'fit')->first();
543
        $reviewStep = Step::where('name', 'review')->first();
544
        $submissionStep = Step::where('name', 'submission')->first();
545
546
        if ($this->job_application_answers->isEmpty()) {
547
            $this->job_application_steps()->attach($basicStep);
548
            $this->job_application_steps()->attach($experienceStep);
549
            $this->job_application_steps()->attach($skillsStep);
550
            $this->job_application_steps()->attach($fitStep);
551
            $this->job_application_steps()->attach($reviewStep);
552
            $this->job_application_steps()->attach($submissionStep);
553
        };
554
    }
555
556
    /**
557
     * Calculates and returns an associative array of application steps (version 2) with the value equal
558
     * to it's status ('default', 'complete', 'error').
559
     *
560
     * @return string $steps
561
     */
562
    public function applicationTimelineSteps(): array
563
    {
564
        $setState = function (bool $touched, bool $isValid) {
565
            return !$touched ? 'default' : ($isValid ? 'complete' : 'error');
566
        };
567
568
        // TODO: Replace with validator.
569
        $basicValidator = true;
570
        $experienceValidator = true;
571
        $skillsValidator = true;
572
        $fitValidator = true;
573
        $reviewValidator = true;
574
        $submissionValidator = true;
575
576
        $basicTouched = $this->job_application_steps->where('name', 'basic')->first()->pivot->touched;
577
        $experienceTouched = $this->job_application_steps->where('name', 'experience')->first()->pivot->touched;
578
        $skillsTouched = $this->job_application_steps->where('name', 'skills')->first()->pivot->touched;
579
        $fitTouched = $this->job_application_steps->where('name', 'fit')->first()->pivot->touched;
580
        $reviewTouched = $this->job_application_steps->where('name', 'review')->first()->pivot->touched;
581
        $submissionTouched = $this->job_application_steps->where('name', 'submission')->first()->pivot->touched;
582
583
        $steps = [
584
            'basic' => $setState($basicTouched, $basicValidator),
585
            'experience' => $setState($experienceTouched, $experienceValidator),
586
            'skills' => $setState($skillsTouched, $skillsValidator),
587
            'fit' => $setState($fitTouched, $fitValidator),
588
            'review' => $setState($reviewTouched, $reviewValidator),
589
            'submission' => $setState($submissionTouched, $submissionValidator)
590
        ];
591
592
        return $steps;
593
    }
594
}
595