Skip to content

Commit

Permalink
[Feature] Opt into search placed candidates backend (#12330)
Browse files Browse the repository at this point in the history
* create artisan command

* update placement mutations

* update qualifiedEquivalent

* test mutations

* search testing

* fix test

* switch to Eloquent

* add comment

* tests feedback

* phpstan
  • Loading branch information
vd1992 authored Dec 19, 2024
1 parent 1324206 commit c776307
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 5 deletions.
50 changes: 50 additions & 0 deletions api/app/Console/Commands/SuspendPlacedCandidates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Console\Commands;

use App\Enums\PoolCandidateStatus;
use App\Models\PoolCandidate;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;

class SuspendPlacedCandidates extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:suspend-placed-candidates';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Set suspended at date to now() for placed term and indeterminate candidates';

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$applicableStatuses = [PoolCandidateStatus::PLACED_TERM->name, PoolCandidateStatus::PLACED_INDETERMINATE->name];

PoolCandidate::whereIn('pool_candidate_status', $applicableStatuses)
->whereNull('suspended_at')
->with('user')
->chunkById(100, function (Collection $candidates) {
foreach ($candidates as $candidate) {
/** @var \App\Models\PoolCandidate $candidate */
$candidate->suspended_at = Carbon::now();
$candidate->save();
}
}
);

return Command::SUCCESS;
}
}
3 changes: 3 additions & 0 deletions api/app/Enums/PoolCandidateStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ enum PoolCandidateStatus
case EXPIRED;
case REMOVED;

// searchable candidates, so the available status and others treated as available for search purposes
public static function qualifiedEquivalentGroup(): array
{
return [
PoolCandidateStatus::QUALIFIED_AVAILABLE->name,
PoolCandidateStatus::PLACED_TENTATIVE->name,
PoolCandidateStatus::PLACED_CASUAL->name,
PoolCandidateStatus::PLACED_TERM->name,
PoolCandidateStatus::PLACED_INDETERMINATE->name,
];
}

Expand Down
8 changes: 8 additions & 0 deletions api/app/GraphQL/Mutations/PlaceCandidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\GraphQL\Mutations;

use App\Enums\PlacementType;
use App\Models\PoolCandidate;
use Carbon\Carbon;

Expand All @@ -25,6 +26,13 @@ public function __invoke($_, array $args)
$candidate->computed_final_decision = $finalDecision['decision'];
$candidate->computed_final_decision_weight = $finalDecision['weight'];

// If setting to term or indeterminate automatically suspend the candidate, otherwise null the field
if ($placementType === PlacementType::PLACED_TERM->name || $placementType === PlacementType::PLACED_INDETERMINATE->name) {
$candidate->suspended_at = $now;
} else {
$candidate->suspended_at = null;
}

$candidate->save();

return $candidate;
Expand Down
1 change: 1 addition & 0 deletions api/app/GraphQL/Mutations/RevertPlaceCandidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __invoke($_, array $args)
$candidate->pool_candidate_status = PoolCandidateStatus::QUALIFIED_AVAILABLE->name;
$candidate->placed_at = null;
$candidate->placed_department_id = null;
$candidate->suspended_at = null;

$candidate->save();

Expand Down
2 changes: 1 addition & 1 deletion api/app/GraphQL/Queries/CountPoolCandidatesByPool.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function __invoke($_, array $args)
}
});

// available candidates scope (scope CANDIDATE_STATUS_QUALIFIED_AVAILABLE or CANDIDATE_STATUS_PLACED_CASUAL, or PLACED_TENTATIVE)
// available candidates scope (qualifiedEquivalentGroup, not expired, not suspended)
PoolCandidate::scopeAvailable($queryBuilder);

// Only display IT & OTHER publishing group candidates
Expand Down
2 changes: 1 addition & 1 deletion api/app/Models/PoolCandidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
* @property ?int $status_weight
* @property string $pool_id
* @property string $user_id
* @property ?\Illuminate\Support\Carbon $suspended_at
* @property ?\Carbon\Carbon $suspended_at
* @property \Illuminate\Support\Carbon $created_at
* @property ?\Illuminate\Support\Carbon $updated_at
* @property array $submitted_steps
Expand Down
54 changes: 54 additions & 0 deletions api/tests/Feature/CountPoolCandidatesByPoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -828,4 +828,58 @@ public function testAdditionalAvailabilityScopes()
],
]);
}

// asserts placed term/indeterminate candidates can appear in this query, can't check by id though so check that 2 out of 3 appear
public function testPlacedSuspendedNotSuspendedCandidates()
{
$itPool = Pool::factory()->create([
...$this->poolData(),
'publishing_group' => PublishingGroup::IT_JOBS->name,
]);
PoolCandidate::truncate();
PoolCandidate::factory()->availableInSearch()->create([
'pool_id' => $itPool,
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'suspended_at' => null,
]);
PoolCandidate::factory()->availableInSearch()->create([
'pool_id' => $itPool,
'pool_candidate_status' => PoolCandidateStatus::PLACED_INDETERMINATE->name,
'suspended_at' => null,
]);
PoolCandidate::factory()->availableInSearch()->create([
'pool_id' => $itPool,
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'suspended_at' => config('constants.far_past_datetime'),
]);

// expect 2, the 2 un-suspended ones
$this->graphQL(
/** @lang GraphQL */
'
query ($where: ApplicantFilterInput) {
countPoolCandidatesByPool(where: $where) {
pool { id }
candidateCount
}
}
',
[
'where' => [
'pools' => [
['id' => $itPool->id],
],
],
]
)->assertSimilarJson([
'data' => [
'countPoolCandidatesByPool' => [
[
'pool' => ['id' => $itPool->id],
'candidateCount' => 2,
],
],
],
]);
}
}
33 changes: 33 additions & 0 deletions api/tests/Feature/PoolCandidateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
use Tests\TestCase;
use Tests\UsesProtectedGraphqlEndpoint;

use function PHPUnit\Framework\assertEqualsCanonicalizing;

class PoolCandidateTest extends TestCase
{
use MakesGraphQLRequests;
Expand Down Expand Up @@ -849,4 +851,35 @@ public function testScopeCandidatesInCommunity(): void
])->assertJsonFragment(['total' => 1])
->assertJsonFragment(['id' => $communityCandidate->id]);
}

// test scopeAvailable
public function testScopeAvailable(): void
{
PoolCandidate::truncate();
$candidate = PoolCandidate::factory()->availableInSearch()->create([
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'expiry_date' => null,
'suspended_at' => null,
]);

$suspendedCandidate = PoolCandidate::factory()->availableInSearch()->create([
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'expiry_date' => null,
'suspended_at' => config('constants.far_past_datetime'),
]);

$expiredCandidate = PoolCandidate::factory()->availableInSearch()->create([
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'expiry_date' => config('constants.past_date'),
'suspended_at' => null,
]);

$queryBuilder = PoolCandidate::query();
$candidateIds = PoolCandidate::scopeAvailable($queryBuilder)->get()->pluck('id')->toArray();

// suspended and expired not present
assertEqualsCanonicalizing([
$candidate->id,
], $candidateIds);
}
}
57 changes: 57 additions & 0 deletions api/tests/Feature/PoolCandidateUpdateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ protected function setUp(): void
placedDepartment {
id
}
suspendedAt
}
}
';
Expand All @@ -175,6 +176,7 @@ protected function setUp(): void
placedDepartment {
id
}
suspendedAt
}
}
';
Expand Down Expand Up @@ -502,12 +504,66 @@ public function testPlaceCandidateMutation(): void
assertSame($response['placedDepartment']['id'], $department->id);
}

// placing candidates sets suspended_at automatically or keeps/makes it null appropriately
public function testPlaceCandidateMutationSuspensionLogic(): void
{
$department = Department::factory()->create();
$this->poolCandidate->pool_candidate_status = PoolCandidateStatus::QUALIFIED_AVAILABLE->name;
$this->poolCandidate->placed_at = null;
$this->poolCandidate->save();
$placedDoNotSuspend = [
PlacementType::PLACED_TENTATIVE->name,
PlacementType::PLACED_CASUAL->name,
];
$placedDoSuspend = [
PlacementType::PLACED_TERM->name,
PlacementType::PLACED_INDETERMINATE->name,
];

foreach ($placedDoNotSuspend as $placementType) {
$response = $this->actingAs($this->poolOperatorUser, 'api')
->graphQL(
$this->placeCandidateMutation,
[
'id' => $this->poolCandidate->id,
'placeCandidate' => [
'placementType' => $placementType,
'departmentId' => $department->id,
],
]
)->json('data.placeCandidate');

assertSame($response['status']['value'], $placementType);
assertNotNull($response['placedAt']);
assertNull($response['suspendedAt']);
}

foreach ($placedDoSuspend as $placementType) {
$response = $this->actingAs($this->poolOperatorUser, 'api')
->graphQL(
$this->placeCandidateMutation,
[
'id' => $this->poolCandidate->id,
'placeCandidate' => [
'placementType' => $placementType,
'departmentId' => $department->id,
],
]
)->json('data.placeCandidate');

assertSame($response['status']['value'], $placementType);
assertNotNull($response['placedAt']);
assertNotNull($response['suspendedAt']);
}
}

public function testRevertPlaceCandidateMutation(): void
{
$department = Department::factory()->create();
$this->poolCandidate->pool_candidate_status = PoolCandidateStatus::NEW_APPLICATION->name;
$this->poolCandidate->placed_department_id = $department->id;
$this->poolCandidate->placed_at = config('constants.far_past_date');
$this->poolCandidate->suspended_at = config('constants.far_past_date');
$this->poolCandidate->save();

// cannot execute mutation due to candidate not being placed
Expand Down Expand Up @@ -535,6 +591,7 @@ public function testRevertPlaceCandidateMutation(): void
assertSame($response['status']['value'], PoolCandidateStatus::QUALIFIED_AVAILABLE->name);
assertNull($response['placedAt']);
assertNull($response['placedDepartment']);
assertNull($response['suspendedAt']);
}

public function testQualifyCandidateMutation(): void
Expand Down
15 changes: 12 additions & 3 deletions api/tests/Feature/UserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,7 @@ public function testCountApplicantsQuery(): void
'user_id' => $user['id'],
]);

// group one
PoolCandidate::factory()->count(8)->create([
'pool_id' => $pool1['id'],
'expiry_date' => config('constants.far_future_date'),
Expand All @@ -1739,6 +1740,7 @@ public function testCountApplicantsQuery(): void
'looking_for_bilingual' => false,
]),
]);
// group two
PoolCandidate::factory()->count(5)->create([
'pool_id' => $pool1['id'],
'expiry_date' => config('constants.far_future_date'),
Expand All @@ -1750,6 +1752,7 @@ public function testCountApplicantsQuery(): void
]),
]);
// Should appear in searches, but in pool 2.
// group three
PoolCandidate::factory()->create([
'pool_id' => $pool2['id'],
'expiry_date' => config('constants.far_future_date'),
Expand All @@ -1761,6 +1764,7 @@ public function testCountApplicantsQuery(): void
]),
]);
// Expired in pool - should not appear in searches
// group four
PoolCandidate::factory()->create([
'pool_id' => $pool1['id'],
'expiry_date' => '2000-01-01',
Expand All @@ -1771,18 +1775,21 @@ public function testCountApplicantsQuery(): void
'looking_for_bilingual' => false,
]),
]);
// Already placed - should not appear in searches
// Already placed - should appear in searches
// group five
PoolCandidate::factory()->create([
'pool_id' => $pool1['id'],
'expiry_date' => config('constants.far_future_date'),
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'suspended_at' => null,
'user_id' => User::factory([
'looking_for_english' => true,
'looking_for_french' => false,
'looking_for_bilingual' => false,
]),
]);
// User status inactive - should not appear in searches
// group six
PoolCandidate::factory()->create([
'pool_id' => $pool1['id'],
'expiry_date' => config('constants.far_future_date'),
Expand Down Expand Up @@ -1812,7 +1819,8 @@ public function testCountApplicantsQuery(): void
);
$response->assertJson([
'data' => [
'countApplicants' => 14, // including base admin user
// contains groups one, two, and five
'countApplicants' => 15, // including base admin user
],
]);

Expand All @@ -1834,7 +1842,8 @@ public function testCountApplicantsQuery(): void
]
)->assertJson([
'data' => [
'countApplicants' => 9, //including base admin user
// counts groups one and five, filtered out two due to added language
'countApplicants' => 10, // including base admin user
],
]);
}
Expand Down

0 comments on commit c776307

Please sign in to comment.