Skip to content

Commit

Permalink
feat: sync new S1 exam passes to roster for home members (#3565)
Browse files Browse the repository at this point in the history
  • Loading branch information
AxonC authored Apr 3, 2024
1 parent f3905bb commit 8fd6306
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 0 deletions.
56 changes: 56 additions & 0 deletions app/Console/Commands/Roster/CheckForNewS1ExamPasses.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace App\Console\Commands\Roster;

use App\Models\Cts\Member;
use App\Models\Mship\Account;
use App\Models\Mship\State;
use App\Models\Roster;
use App\Repositories\Cts\ExamResultRepository;
use Illuminate\Console\Command;

class CheckForNewS1ExamPasses extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'roster:check-new-s1-exams';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Make a query to CTS for new exam passes and add them to the roster if applicable.';

/**
* Execute the console command.
*/
public function handle(ExamResultRepository $repository)
{
$recentSuccessfulS1Exams = $repository->getRecentPassedExamsOfType('OBS');

foreach ($recentSuccessfulS1Exams as $exam) {
$ctsMember = Member::where('id', $exam->student_id)->first();
$coreAccount = Account::find($ctsMember->cid);

if (! $coreAccount) {
$this->error("Could not find account for student ID {$exam->student_id}.");

continue;
}

if (! $coreAccount->hasState(State::findByCode('DIVISION'))) {
$this->error("Account {$coreAccount->id} does not have the DIVISION state.");

continue;
}

Roster::upsert(['account_id' => $coreAccount->id], uniqueBy: ['account_id']);

$this->info("Added account {$coreAccount->id} to the roster.");
}
}
}
4 changes: 4 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ protected function schedule(Schedule $schedule)
->hourlyAt(25)
->graceTimeInMinutes(5);

$schedule->command('roster:check-new-s1-exams')
->hourlyAt(30)
->graceTimeInMinutes(5);

// === By Day === //

$schedule->command('telescope:prune')
Expand Down
23 changes: 23 additions & 0 deletions app/Models/Cts/PracticalResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace App\Models\Cts;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class PracticalResult extends Model
{
use HasFactory;

protected $connection = 'cts';

public $timestamps = false;

public const PASSED = 'P';

public const FAILED = 'F';

protected $casts = [
'date' => 'datetime',
];
}
17 changes: 17 additions & 0 deletions app/Repositories/Cts/ExamResultRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Repositories\Cts;

use App\Models\Cts\PracticalResult;
use Illuminate\Support\Collection;

class ExamResultRepository
{
public function getRecentPassedExamsOfType(string $type, int $daysConsideredRecent = 3): Collection
{
return PracticalResult::where('result', PracticalResult::PASSED)
->where('exam', $type)
->where('date', '>=', now()->subDays($daysConsideredRecent))
->get();
}
}
28 changes: 28 additions & 0 deletions database/factories/Cts/PracticalResultFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Database\Factories\Cts;

use App\Models\Cts\Member;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Cts\PracticalResult>
*/
class PracticalResultFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'examid' => $this->faker->randomNumber(3),
'student_id' => factory(Member::class)->create()->id,
'exam' => $this->faker->randomElement(['OBS', 'TWR', 'APP', 'CTR']),
'result' => $this->faker->randomElement(['P', 'F']),
'date' => $this->faker->dateTime(),
];
}
}
21 changes: 21 additions & 0 deletions tests/Database/MockCtsDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,23 @@ public static function create()
KEY `student_id` (`student_id`)
);"
);

DB::connection('cts')->statement(
"CREATE TABLE `practical_results` (
`id` smallint unsigned NOT NULL AUTO_INCREMENT,
`examid` smallint unsigned NOT NULL DEFAULT '0',
`student_id` int unsigned NOT NULL DEFAULT '0',
`exam` enum('P1','P2','P3','P4','P5','P6','P7','P8','P9','OBS','TWR','APP','CTR','S3','C1','C3') NOT NULL DEFAULT 'TWR',
`notes` longtext,
`result` char(1) NOT NULL DEFAULT '',
`date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`cert_upgrade` tinyint unsigned DEFAULT '0',
`upgrade_by` int unsigned DEFAULT '0',
`upgrade_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `student_id` (`student_id`)
);"
);
}

public static function destroy()
Expand Down Expand Up @@ -260,5 +277,9 @@ public static function destroy()
DB::connection('cts')->statement(
'DROP TABLE IF EXISTS `theory_results`;'
);

DB::connection('cts')->statement(
'DROP TABLE IF EXISTS `practical_results`;'
);
}
}
88 changes: 88 additions & 0 deletions tests/Unit/CTS/ExamResultsRepositoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Tests\Unit\CTS;

use App\Models\Cts\Member;
use App\Models\Cts\PracticalResult;
use App\Models\Mship\Account;
use App\Repositories\Cts\ExamResultRepository;
use Tests\TestCase;

class ExamResultsRepositoryTest extends TestCase
{
public function test_retrieves_passed_exam_results_of_type()
{
$account = Account::factory()->create(['id' => 1111111]);
$member = factory(Member::class)->create([
'cid' => $account->id,
]);

$examResult = PracticalResult::factory()->create([
'result' => PracticalResult::PASSED,
'student_id' => $member->id,
'exam' => 'OBS',
'date' => now()->subDays(1),
]);

// ensure failed result isn't returned
$notSuccessfulPracticalResult = PracticalResult::factory()->create([
'result' => PracticalResult::FAILED,
'student_id' => $member->id,
'exam' => 'OBS',
]);

$repository = new ExamResultRepository();
$result = $repository->getRecentPassedExamsOfType('OBS');

$this->assertNotNull($result->where('id', $examResult->id)->first());
$this->assertNull($result->where('id', $notSuccessfulPracticalResult->id)->first());
}

public function test_doesnt_return_non_recent_exam_passes()
{
$account = Account::factory()->create(['id' => 1111111]);
$member = factory(Member::class)->create([
'cid' => $account->id,
]);

$examResult = PracticalResult::factory()->create([
'result' => PracticalResult::PASSED,
'student_id' => $member->id,
'exam' => 'OBS',
'date' => now()->subDays(4),
]);

$repository = new ExamResultRepository();
$result = $repository->getRecentPassedExamsOfType('OBS');

$this->assertNull($result->where('id', $examResult->id)->first());
}

public function test_only_returns_recent_successful_exams_of_specified_type()
{
$account = Account::factory()->create(['id' => 1111111]);
$member = factory(Member::class)->create([
'cid' => $account->id,
]);

$examResult = PracticalResult::factory()->create([
'result' => PracticalResult::PASSED,
'student_id' => $member->id,
'exam' => 'TWR',
'date' => now()->subDays(1),
]);

$notSuccessfulPracticalResult = PracticalResult::factory()->create([
'result' => PracticalResult::FAILED,
'student_id' => $member->id,
'exam' => 'OBS',
'date' => now()->subDays(1),
]);

$repository = new ExamResultRepository();
$result = $repository->getRecentPassedExamsOfType('OBS');

$this->assertNull($result->where('id', $examResult->id)->first());
$this->assertNull($result->where('id', $notSuccessfulPracticalResult->id)->first());
}
}
61 changes: 61 additions & 0 deletions tests/Unit/Command/AddNewS1ToRosterCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Tests\Unit\Command;

use App\Models\Cts\Member;
use App\Models\Cts\PracticalResult;
use App\Models\Mship\Account;
use App\Models\Mship\State;
use App\Models\Roster;
use Illuminate\Support\Facades\Artisan;
use Tests\TestCase;

class AddNewS1ToRosterCommandTest extends TestCase
{
public function test_detects_recent_s1_exams_and_adds_when_not_on_roster_home_member()
{
$account = Account::factory()->create(['id' => 1111111]);
$account->addState(State::findByCode('DIVISION'));

$ctsMember = factory(Member::class)->create(['cid' => $account->id]);
PracticalResult::factory()->create(['student_id' => $ctsMember->id, 'result' => PracticalResult::PASSED, 'exam' => 'OBS', 'date' => now()->subDays(1)]);

Artisan::call('roster:check-new-s1-exams');

$this->assertDatabaseHas('roster', [
'account_id' => $account->id,
]);
}

public function test_maintains_roster_when_already_on_roster()
{
$account = Account::factory()->create(['id' => 1111111]);
$account->addState(State::findByCode('DIVISION'));

$ctsMember = factory(Member::class)->create(['cid' => $account->id]);
PracticalResult::factory()->create(['student_id' => $ctsMember->id, 'result' => PracticalResult::PASSED, 'exam' => 'OBS', 'date' => now()->subDays(1)]);

Roster::create(['account_id' => $account->id]);

Artisan::call('roster:check-new-s1-exams');

$this->assertDatabaseHas('roster', [
'account_id' => $account->id,
]);
}

public function test_does_not_add_when_not_on_roster_and_missing_division_state()
{
$account = Account::factory()->create(['id' => 1111111]);
$account->addState(State::findByCode('VISITING'));

$ctsMember = factory(Member::class)->create(['cid' => $account->id]);
PracticalResult::factory()->create(['student_id' => $ctsMember->id, 'result' => PracticalResult::PASSED, 'exam' => 'OBS', 'date' => now()->subDays(1)]);

Artisan::call('roster:check-new-s1-exams');

$this->assertDatabaseMissing('roster', [
'account_id' => $account->id,
]);
}
}
7 changes: 7 additions & 0 deletions tests/Unit/Roster/AccountCanControlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@
use App\Models\Mship\Qualification;
use App\Models\Mship\State;
use App\Models\Roster;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class AccountCanControlTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
Event::fake();
}

public function test_detects_can_control_with_rating_when_home_member()
{
$qualification = Qualification::code('S2')->first();
Expand Down

0 comments on commit 8fd6306

Please sign in to comment.