diff --git a/api/app/Console/Commands/SyncAssessmentStatus.php b/api/app/Console/Commands/SyncAssessmentStatus.php index 4431131d8ec..fa668251302 100644 --- a/api/app/Console/Commands/SyncAssessmentStatus.php +++ b/api/app/Console/Commands/SyncAssessmentStatus.php @@ -2,6 +2,7 @@ namespace App\Console\Commands; +use App\Events\CandidateStatusChanged; use App\Models\PoolCandidate; use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Collection; @@ -34,6 +35,9 @@ public function handle() $candidate->computed_assessment_status = $assessmentStatus; $candidate->save(); + + // If assessment status changes, "final decision" may as well. + CandidateStatusChanged::dispatch($candidate); } }); } diff --git a/api/app/Models/PoolCandidate.php b/api/app/Models/PoolCandidate.php index 20367098034..b843056eef4 100644 --- a/api/app/Models/PoolCandidate.php +++ b/api/app/Models/PoolCandidate.php @@ -941,6 +941,11 @@ public function setApplicationSnapshot(bool $save = true) * and if step is Application Assessment then repeat the Essential switch statement education assessment result * stepStatus is first of UNSUCCESSFUL, TO ASSESS, HOLD, and else QUALIFIED * no decision for steps that are TO ASSESS but have no results so we can tell when they've been started + * + * overallAssessmentStatus is then: + * if any step is UNSUCCESSFUL, then DISQUALIFIED + * else if all steps are fully assessed, and final step is not HOLD, then QUALIFIED + * else TO ASSESS */ public function computeAssessmentStatus() { @@ -1105,24 +1110,22 @@ public function computeAssessmentStatus() $totalSteps = $this->pool->assessmentSteps->count(); $overallAssessmentStatus = OverallAssessmentStatus::TO_ASSESS->name; - if ($currentStep >= $totalSteps && $totalSteps === count($decisions)) { + $unsuccessfulDecisions = Arr::where($decisions, function ($stepDecision) { + return $stepDecision['decision'] === AssessmentDecision::UNSUCCESSFUL->name; + }); + if (! empty($unsuccessfulDecisions)) { + $overallAssessmentStatus = OverallAssessmentStatus::DISQUALIFIED->name; + } elseif ($currentStep >= $totalSteps && $totalSteps === count($decisions)) { $lastStepDecision = end($decisions); if ($lastStepDecision && $lastStepDecision['decision'] !== AssessmentDecision::HOLD->name && ! is_null($lastStepDecision['decision'])) { $overallAssessmentStatus = OverallAssessmentStatus::QUALIFIED->name; $currentStep = null; } - } else { - $unsuccessfulDecisions = Arr::where($decisions, function ($stepDecision) { - return $stepDecision['decision'] === AssessmentDecision::UNSUCCESSFUL->name; - }); - if (! empty($unsuccessfulDecisions)) { - $overallAssessmentStatus = OverallAssessmentStatus::DISQUALIFIED->name; - } } // While unlikely, current step could go over. // So, set it back to total steps - if ($currentStep > $totalSteps) { + if ($currentStep && $currentStep > $totalSteps) { $currentStep = $totalSteps; } diff --git a/api/database/seeders/AssessmentResultTestSeeder.php b/api/database/seeders/AssessmentResultTestSeeder.php index 697809e2033..b6afda07ba2 100644 --- a/api/database/seeders/AssessmentResultTestSeeder.php +++ b/api/database/seeders/AssessmentResultTestSeeder.php @@ -6,6 +6,7 @@ use App\Enums\AssessmentDecisionLevel; use App\Enums\AssessmentResultJustification; use App\Enums\AssessmentResultType; +use App\Enums\AssessmentStepType; use App\Enums\PoolSkillType; use App\Models\AssessmentResult; use App\Models\AssessmentStep; @@ -74,107 +75,116 @@ public function run() ]); $publishedPool = Pool::select('id')->where('name->en', 'Published – Complex')->sole(); - $user1 = User::select('id')->where('email', 'perfect@test.com')->sole(); - $poolCandidate1 = PoolCandidate::select('id')->where('user_id', $user1->id)->where('pool_id', $publishedPool->id)->sole(); - $assessmentStep = AssessmentStep::factory()->create([ - 'pool_id' => $publishedPool->id, - ]); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate1, - $publishedPool->poolSkills()->pluck('id')->toArray(), - $assessmentStep, - AssessmentDecisionLevel::ABOVE_REQUIRED->name, - [AssessmentResultJustification::EDUCATION_ACCEPTED_INFORMATION->name], - AssessmentDecision::SUCCESSFUL->name, - AssessmentResultType::EDUCATION); - - $user2 = User::select('id')->where('email', 'veteran@test.com')->sole(); - $poolCandidate2 = PoolCandidate::select('id')->where('user_id', $user2->id)->where('pool_id', $publishedPool->id)->sole(); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate2, - $publishedPool->poolSkills()->where('type', PoolSkillType::ESSENTIAL->name)->pluck('id')->toArray(), - $assessmentStep, - AssessmentDecisionLevel::AT_REQUIRED->name, - [AssessmentResultJustification::EDUCATION_ACCEPTED_WORK_EXPERIENCE_EQUIVALENCY->name], - AssessmentDecision::SUCCESSFUL->name, - AssessmentResultType::SKILL); - - $user3 = User::select('id')->where('email', 'assertive@test.com')->sole(); - $poolCandidate3 = PoolCandidate::select('id')->where('user_id', $user3->id)->where('pool_id', $publishedPool->id)->sole(); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate3, - $publishedPool->poolSkills()->pluck('id')->toArray(), - $assessmentStep, - AssessmentDecisionLevel::ABOVE_REQUIRED->name, - [AssessmentResultJustification::EDUCATION_ACCEPTED_WORK_EXPERIENCE_EQUIVALENCY->name], - AssessmentDecision::SUCCESSFUL->name, - AssessmentResultType::SKILL); - - $user4 = User::select('id')->where('email', 'absent@test.com')->sole(); - $poolCandidate4 = PoolCandidate::select('id')->where('user_id', $user4->id)->where('pool_id', $publishedPool->id)->sole(); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate4, - $publishedPool->poolSkills()->where('type', PoolSkillType::ESSENTIAL->name)->pluck('id')->toArray(), - $assessmentStep, AssessmentDecisionLevel::AT_REQUIRED->name, - [AssessmentResultJustification::EDUCATION_ACCEPTED_WORK_EXPERIENCE_EQUIVALENCY->name], - AssessmentDecision::HOLD->name, - AssessmentResultType::SKILL); - - $user5 = User::select('id')->where('email', 'screened-out@test.com')->sole(); - $poolCandidate5 = PoolCandidate::select('id')->where('user_id', $user5->id)->where('pool_id', $publishedPool->id)->sole(); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate5, - $publishedPool->poolSkills()->where('type', PoolSkillType::ESSENTIAL->name)->pluck('id')->toArray(), - $assessmentStep, - AssessmentDecisionLevel::AT_REQUIRED->name, - [AssessmentResultJustification::EDUCATION_FAILED_NOT_RELEVANT->name, - AssessmentResultJustification::EDUCATION_FAILED_REQUIREMENT_NOT_MET->name], - AssessmentDecision::UNSUCCESSFUL->name, - AssessmentResultType::EDUCATION); - - $user6 = User::select('id')->where('email', 'failed@test.com')->sole(); - $poolCandidate6 = PoolCandidate::select('id')->where('user_id', $user6->id)->where('pool_id', $publishedPool->id)->sole(); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate6, - $publishedPool->poolSkills()->pluck('id')->toArray(), $assessmentStep, - null, - [AssessmentResultJustification::EDUCATION_FAILED_REQUIREMENT_NOT_MET->name], - AssessmentDecision::UNSUCCESSFUL->name, - AssessmentResultType::EDUCATION); - - $user7 = User::select('id')->where('email', 'entry-level-holder@test.com')->sole(); - $poolCandidate7 = PoolCandidate::select('id')->where('user_id', $user7->id)->where('pool_id', $publishedPool->id)->sole(); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate7, - null, - $assessmentStep, - null, - [AssessmentResultJustification::EDUCATION_ACCEPTED_WORK_EXPERIENCE_EQUIVALENCY->name], - AssessmentDecision::HOLD->name, - assessmentResultType::EDUCATION); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate7, - $publishedPool->poolSkills()->pluck('id')->toArray(), - $assessmentStep, - null, - [AssessmentResultJustification::SKILL_FAILED_INSUFFICIENTLY_DEMONSTRATED->name], - AssessmentDecision::HOLD->name, - assessmentResultType::SKILL); - - $user8 = User::select('id')->where('email', 'unsuccessful@test.com')->sole(); - $poolCandidate8 = PoolCandidate::select('id')->where('user_id', $user8->id)->where('pool_id', $publishedPool->id)->sole(); - // select first essential skill from pool skills - $firstEssentialSkill = $publishedPool->poolSkills()->where('type', PoolSkillType::ESSENTIAL->name)->first(); - $this->assessSkillsWithLevelAndJustification( - $poolCandidate8, - [$firstEssentialSkill->id], - $assessmentStep, - null, - [AssessmentResultJustification::SKILL_FAILED_INSUFFICIENTLY_DEMONSTRATED->name, - AssessmentResultJustification::FAILED_NOT_ENOUGH_INFORMATION->name, - AssessmentResultJustification::FAILED_OTHER->name], - AssessmentDecision::UNSUCCESSFUL->name, - AssessmentResultType::SKILL); + $publishedPool->load('assessmentSteps.poolSkills'); + foreach ($publishedPool->assessmentSteps as $assessmentStep) { + $user1 = User::select('id')->where('email', 'perfect@test.com')->sole(); + $poolCandidate1 = PoolCandidate::select('id')->where('user_id', $user1->id)->where('pool_id', $publishedPool->id)->sole(); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate1, + $assessmentStep->poolSkills, + $assessmentStep, + AssessmentDecisionLevel::ABOVE_REQUIRED->name, + [AssessmentResultJustification::EDUCATION_ACCEPTED_INFORMATION->name], + AssessmentDecision::SUCCESSFUL->name, + AssessmentResultType::EDUCATION); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate1, + $assessmentStep->poolSkills, + $assessmentStep, + AssessmentDecisionLevel::ABOVE_REQUIRED->name, + null, + AssessmentDecision::SUCCESSFUL->name, + AssessmentResultType::SKILL); + + $user2 = User::select('id')->where('email', 'veteran@test.com')->sole(); + $poolCandidate2 = PoolCandidate::select('id')->where('user_id', $user2->id)->where('pool_id', $publishedPool->id)->sole(); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate2, + $assessmentStep->poolSkills()->where('type', PoolSkillType::ESSENTIAL->name)->get(), + $assessmentStep, + AssessmentDecisionLevel::AT_REQUIRED->name, + null, + AssessmentDecision::SUCCESSFUL->name, + AssessmentResultType::SKILL); + + $user3 = User::select('id')->where('email', 'assertive@test.com')->sole(); + $poolCandidate3 = PoolCandidate::select('id')->where('user_id', $user3->id)->where('pool_id', $publishedPool->id)->sole(); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate3, + $assessmentStep->poolSkills, + $assessmentStep, + AssessmentDecisionLevel::ABOVE_REQUIRED->name, + null, + AssessmentDecision::SUCCESSFUL->name, + AssessmentResultType::SKILL); + + $user4 = User::select('id')->where('email', 'absent@test.com')->sole(); + $poolCandidate4 = PoolCandidate::select('id')->where('user_id', $user4->id)->where('pool_id', $publishedPool->id)->sole(); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate4, + $assessmentStep->poolSkills()->where('type', PoolSkillType::ESSENTIAL->name)->get(), + $assessmentStep, AssessmentDecisionLevel::AT_REQUIRED->name, + null, + AssessmentDecision::HOLD->name, + AssessmentResultType::SKILL); + + $user5 = User::select('id')->where('email', 'screened-out@test.com')->sole(); + $poolCandidate5 = PoolCandidate::select('id')->where('user_id', $user5->id)->where('pool_id', $publishedPool->id)->sole(); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate5, + $assessmentStep->poolSkills()->where('type', PoolSkillType::ESSENTIAL->name)->get(), + $assessmentStep, + AssessmentDecisionLevel::AT_REQUIRED->name, + [AssessmentResultJustification::EDUCATION_FAILED_NOT_RELEVANT->name, + AssessmentResultJustification::EDUCATION_FAILED_REQUIREMENT_NOT_MET->name], + AssessmentDecision::UNSUCCESSFUL->name, + AssessmentResultType::EDUCATION); + + $user6 = User::select('id')->where('email', 'failed@test.com')->sole(); + $poolCandidate6 = PoolCandidate::select('id')->where('user_id', $user6->id)->where('pool_id', $publishedPool->id)->sole(); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate6, + $assessmentStep->poolSkills, + $assessmentStep, + null, + [AssessmentResultJustification::EDUCATION_FAILED_REQUIREMENT_NOT_MET->name], + AssessmentDecision::UNSUCCESSFUL->name, + AssessmentResultType::EDUCATION); + + $user7 = User::select('id')->where('email', 'entry-level-holder@test.com')->sole(); + $poolCandidate7 = PoolCandidate::select('id')->where('user_id', $user7->id)->where('pool_id', $publishedPool->id)->sole(); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate7, + null, + $assessmentStep, + null, + [AssessmentResultJustification::EDUCATION_ACCEPTED_WORK_EXPERIENCE_EQUIVALENCY->name], + AssessmentDecision::HOLD->name, + assessmentResultType::EDUCATION); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate7, + $assessmentStep->poolSkills, + $assessmentStep, + null, + null, + AssessmentDecision::HOLD->name, + assessmentResultType::SKILL); + + $user8 = User::select('id')->where('email', 'unsuccessful@test.com')->sole(); + $poolCandidate8 = PoolCandidate::select('id')->where('user_id', $user8->id)->where('pool_id', $publishedPool->id)->sole(); + // select first essential skill from pool skills + $firstEssentialSkill = $assessmentStep->poolSkills()->where('type', PoolSkillType::ESSENTIAL->name)->get()->first(); + $this->assessSkillsWithLevelAndJustification( + $poolCandidate8, + [$firstEssentialSkill], + $assessmentStep, + null, + [AssessmentResultJustification::SKILL_FAILED_INSUFFICIENTLY_DEMONSTRATED->name, + AssessmentResultJustification::FAILED_NOT_ENOUGH_INFORMATION->name, + AssessmentResultJustification::FAILED_OTHER->name], + AssessmentDecision::UNSUCCESSFUL->name, + AssessmentResultType::SKILL); + } } private function assessSkillsWithLevelAndJustification($poolCandidate, @@ -185,6 +195,11 @@ private function assessSkillsWithLevelAndJustification($poolCandidate, $assessmentDecision, $assessmentResultType) { + // Only save education assessment for the first assessment step + if ($assessmentResultType == AssessmentResultType::EDUCATION && $assessmentStep->type != AssessmentStepType::APPLICATION_SCREENING->name) { + return; + } + $nullJustifications = $assessmentDecision === AssessmentDecision::HOLD->name || is_null($assessmentDecision); if ($assessmentResultType == null) { @@ -201,7 +216,7 @@ private function assessSkillsWithLevelAndJustification($poolCandidate, AssessmentResult::factory()->withResultType($assessmentResultType)->create([ 'assessment_step_id' => $assessmentStep->id, 'pool_candidate_id' => $poolCandidate->id, - 'pool_skill_id' => $poolSkill, + 'pool_skill_id' => $poolSkill->id, 'assessment_decision_level' => $level, 'justifications' => $nullJustifications ? null : $justifications, 'assessment_decision' => $assessmentDecision, diff --git a/api/database/seeders/DatabaseSeeder.php b/api/database/seeders/DatabaseSeeder.php index 5d0154f3bf4..c693d3417b8 100644 --- a/api/database/seeders/DatabaseSeeder.php +++ b/api/database/seeders/DatabaseSeeder.php @@ -37,7 +37,7 @@ public function run(): void TeamRandomSeeder::class, PoolRandomSeeder::class, UserRandomSeeder::class, - AssessmentResultRandomSeeder::class, + // AssessmentResultRandomSeeder::class, SearchRequestRandomSeeder::class, DigitalContractingQuestionnaireRandomSeeder::class, DepartmentSpecificRecruitmentProcessFormRandomSeeder::class, diff --git a/api/tests/Feature/CandidateAssessmentStatusTest.php b/api/tests/Feature/CandidateAssessmentStatusTest.php index c331797c615..cffca93dfaf 100644 --- a/api/tests/Feature/CandidateAssessmentStatusTest.php +++ b/api/tests/Feature/CandidateAssessmentStatusTest.php @@ -922,4 +922,63 @@ public function testApplicationScreeningStepEducationResult() ], ]); } + + /** + * This regression test ensures that if all skills are assessed as Successful, + * except for a single skill in the final step which is Unsuccessful, + * then the overall status is Disqualified. + */ + public function testUnsuccessfulEssentialInFinalStepMeansDisqualified(): void + { + $steps = $this->pool->assessmentSteps; + + AssessmentResult::factory() + ->withResultType(AssessmentResultType::EDUCATION) + ->create([ + 'assessment_step_id' => $steps[0]->id, + 'pool_candidate_id' => $this->candidate->id, + 'assessment_decision' => AssessmentDecision::SUCCESSFUL->name, + 'pool_skill_id' => $this->poolSkill->id, + ]); + AssessmentResult::factory() + ->withResultType(AssessmentResultType::SKILL) + ->create([ + 'assessment_step_id' => $steps[0]->id, + 'pool_candidate_id' => $this->candidate->id, + 'assessment_decision' => AssessmentDecision::SUCCESSFUL->name, + 'pool_skill_id' => $this->poolSkill->id, + ]); + + AssessmentResult::factory() + ->withResultType(AssessmentResultType::SKILL) + ->create([ + 'assessment_step_id' => $steps[1]->id, + 'pool_candidate_id' => $this->candidate->id, + 'assessment_decision' => AssessmentDecision::UNSUCCESSFUL->name, + 'pool_skill_id' => $this->poolSkill->id, + ]); + + $this->actingAs($this->adminUser, 'api') + ->graphQL($this->query, $this->queryVars) + ->assertJson([ + 'data' => [ + 'poolCandidate' => [ + 'assessmentStatus' => [ + 'assessmentStepStatuses' => [ + [ + 'step' => $steps[0]->id, + 'decision' => AssessmentDecision::SUCCESSFUL->name, + ], + [ + 'step' => $steps[1]->id, + 'decision' => AssessmentDecision::UNSUCCESSFUL->name, + ], + ], + 'overallAssessmentStatus' => OverallAssessmentStatus::DISQUALIFIED->name, + 'currentStep' => 2, + ], + ], + ], + ]); + } }