595 lines
24 KiB
PHP
595 lines
24 KiB
PHP
<?php
|
|
|
|
namespace Tests\App\Services;
|
|
|
|
use App\Services\EnrollmentTransitionService;
|
|
use App\Support\Enrollment\DeliberationDecision;
|
|
use CodeIgniter\Database\BaseBuilder;
|
|
use CodeIgniter\Database\BaseConnection;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class EnrollmentTransitionServiceTest extends TestCase
|
|
{
|
|
public function testPassedClassNameWithPrefixFindsNextNumericGrade(): void
|
|
{
|
|
$service = $this->serviceExpectingClassLookup('9', ['id' => 9, 'class_name' => '9']);
|
|
|
|
$result = $this->invoke($service, 'nextClass', ['Class 8', '2026-2027']);
|
|
|
|
$this->assertSame(9, (int) $result['id']);
|
|
}
|
|
|
|
public function testPassedClassNinePromotesToTenWhenGradeTenExists(): void
|
|
{
|
|
$service = $this->serviceExpectingClassLookup('10', ['id' => 10, 'class_name' => '10']);
|
|
|
|
$result = $this->invoke($service, 'nextClass', ['Class 9', '2026-2027']);
|
|
|
|
$this->assertSame(10, (int) $result['id']);
|
|
}
|
|
|
|
public function testKgMissingDecisionUsesAgePlacementDecision(): void
|
|
{
|
|
$service = new EnrollmentTransitionService($this->createMock(BaseConnection::class));
|
|
|
|
$this->assertSame(
|
|
DeliberationDecision::PASSED,
|
|
$this->invoke($service, 'decisionForKgWithoutFinalDecision', [['class_name' => 'KG'], 6, ''])
|
|
);
|
|
$this->assertSame(
|
|
DeliberationDecision::REPEAT_CLASS,
|
|
$this->invoke($service, 'decisionForKgWithoutFinalDecision', [['class_name' => 'Kindergarten'], 5, ''])
|
|
);
|
|
$this->assertNull($this->invoke($service, 'decisionForKgWithoutFinalDecision', [['class_name' => 'KG'], 6, 'Needs review']));
|
|
$this->assertNull($this->invoke($service, 'decisionForKgWithoutFinalDecision', [['class_name' => '1'], 6, '']));
|
|
}
|
|
|
|
public function testExplicitNewStudentStatusMarksFirstEnrollmentCandidate(): void
|
|
{
|
|
$statusBuilder = $this->builderReturning(['is_new' => 1]);
|
|
$statusBuilder->method('where')->willReturnSelf();
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->with('student_year_status')->willReturn(true);
|
|
$db->method('table')->with('student_year_status')->willReturn($statusBuilder);
|
|
|
|
$service = new EnrollmentTransitionService($db);
|
|
|
|
$this->assertTrue($this->invoke($service, 'isFirstEnrollmentStudent', [
|
|
42,
|
|
['school_year' => '2026-2027'],
|
|
'2026-2027',
|
|
]));
|
|
}
|
|
|
|
public function testFirstEnrollmentPlacementUsesRegistrationGradeBaseSection(): void
|
|
{
|
|
$classBuilder = $this->builderReturning(['id' => 3, 'class_name' => '3']);
|
|
$classBuilder->method('where')->willReturnSelf();
|
|
|
|
$sectionBuilder = $this->builderReturning([
|
|
'class_section_id' => 30,
|
|
'class_id' => 3,
|
|
'class_section_name' => '3',
|
|
]);
|
|
$sectionBuilder->method('where')->willReturnSelf();
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->willReturnCallback(static fn (string $table): bool => $table === 'classSection');
|
|
$db->method('fieldExists')->willReturn(false);
|
|
$db->method('table')->willReturnCallback(static function (string $table) use ($classBuilder, $sectionBuilder) {
|
|
return $table === 'classes' ? $classBuilder : $sectionBuilder;
|
|
});
|
|
|
|
$service = new EnrollmentTransitionService($db);
|
|
|
|
$placement = $this->invoke($service, 'firstEnrollmentPlacement', [
|
|
['registration_grade' => 'Grade 3'],
|
|
'2026-2027',
|
|
]);
|
|
|
|
$this->assertSame(3, (int) $placement['assigned_grade_id']);
|
|
$this->assertSame(30, (int) $placement['assigned_class_section_id']);
|
|
$this->assertSame('same_class_assigned', $placement['placement_status']);
|
|
}
|
|
|
|
public function testClassLookupFallsBackToGlobalRowsWhenSchoolYearSpecificRowIsMissing(): void
|
|
{
|
|
$firstBuilder = $this->builderReturning(null);
|
|
$firstBuilder->expects($this->exactly(2))
|
|
->method('where')
|
|
->willReturnSelf();
|
|
|
|
$fallbackBuilder = $this->builderReturning(['id' => 10, 'class_name' => '10']);
|
|
$fallbackBuilder->expects($this->once())
|
|
->method('where')
|
|
->with('UPPER(class_name)', '10')
|
|
->willReturnSelf();
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->expects($this->exactly(2))
|
|
->method('table')
|
|
->with('classes')
|
|
->willReturnOnConsecutiveCalls($firstBuilder, $fallbackBuilder);
|
|
$db->method('fieldExists')->with('school_year', 'classes')->willReturn(true);
|
|
|
|
$service = new EnrollmentTransitionService($db);
|
|
|
|
$result = $this->invoke($service, 'nextClass', ['Class 9', '2026-2027']);
|
|
|
|
$this->assertSame(10, (int) $result['id']);
|
|
}
|
|
|
|
private function serviceExpectingClassLookup(string $expectedClassName, array $row): EnrollmentTransitionService
|
|
{
|
|
$builder = $this->builderReturning($row);
|
|
$builder->expects($this->once())
|
|
->method('where')
|
|
->with('UPPER(class_name)', $expectedClassName)
|
|
->willReturnSelf();
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->expects($this->once())
|
|
->method('table')
|
|
->with('classes')
|
|
->willReturn($builder);
|
|
$db->method('fieldExists')->with('school_year', 'classes')->willReturn(false);
|
|
|
|
return new EnrollmentTransitionService($db);
|
|
}
|
|
|
|
private function builderReturning(?array $row): BaseBuilder
|
|
{
|
|
$builder = $this->createMock(BaseBuilder::class);
|
|
$builder->method('select')->willReturnSelf();
|
|
$builder->method('orderBy')->willReturnSelf();
|
|
$builder->method('limit')->willReturnSelf();
|
|
$builder->method('get')->willReturn(new class($row) {
|
|
public function __construct(private readonly ?array $row)
|
|
{
|
|
}
|
|
|
|
public function getRowArray(): ?array
|
|
{
|
|
return $this->row;
|
|
}
|
|
|
|
public function getResultArray(): array
|
|
{
|
|
return $this->row === null ? [] : [$this->row];
|
|
}
|
|
});
|
|
|
|
return $builder;
|
|
}
|
|
|
|
public function testNormalizeLastNameIgnoresPunctuationAndCase(): void
|
|
{
|
|
$service = new EnrollmentTransitionService($this->createMock(BaseConnection::class));
|
|
|
|
$this->assertSame('omalley', $this->invoke($service, 'normalizeLastName', ["O'Malley"]));
|
|
$this->assertSame('omalley', $this->invoke($service, 'normalizeLastName', ['O-Malley']));
|
|
}
|
|
|
|
public function testSingleLinkedStudentPassesLastNameRule(): void
|
|
{
|
|
$service = $this->serviceWithStudentRows([
|
|
['id' => 1, 'firstname' => 'Amina', 'lastname' => 'Hassan', 'parent_id' => 9],
|
|
]);
|
|
$evaluation = [];
|
|
|
|
$this->invoke($service, 'applyHouseholdLastNameRule', [&$evaluation, 9]);
|
|
|
|
$this->assertTrue($evaluation['family_name_check_ok']);
|
|
$this->assertArrayNotHasKey('blocking_rule_codes', $evaluation);
|
|
}
|
|
|
|
public function testMismatchedSiblingLastNamesBlockEveryStudent(): void
|
|
{
|
|
$service = $this->serviceWithStudentRows([
|
|
['id' => 1, 'firstname' => 'Amina', 'lastname' => 'Hassan', 'parent_id' => 9],
|
|
['id' => 2, 'firstname' => 'Omar', 'lastname' => 'Ali', 'parent_id' => 9],
|
|
]);
|
|
$evaluation = [];
|
|
|
|
$this->invoke($service, 'applyHouseholdLastNameRule', [&$evaluation, 9]);
|
|
|
|
$this->assertFalse($evaluation['family_name_check_ok']);
|
|
$this->assertContains('SIBLING_LAST_NAME_MISMATCH', $evaluation['blocking_rule_codes']);
|
|
}
|
|
|
|
public function testMissingSiblingLastNameBlocksFamily(): void
|
|
{
|
|
$service = $this->serviceWithStudentRows([
|
|
['id' => 1, 'firstname' => 'Amina', 'lastname' => 'Hassan', 'parent_id' => 9],
|
|
['id' => 2, 'firstname' => 'Omar', 'lastname' => ' ', 'parent_id' => 9],
|
|
]);
|
|
$evaluation = [];
|
|
|
|
$this->invoke($service, 'applyHouseholdLastNameRule', [&$evaluation, 9]);
|
|
|
|
$this->assertContains('SIBLING_LAST_NAME_MISMATCH', $evaluation['blocking_rule_codes']);
|
|
}
|
|
|
|
public function testNewStudentDoesNotMakeReturningSiblingsFailLastNameRule(): void
|
|
{
|
|
$studentBuilder = $this->createMock(BaseBuilder::class);
|
|
$studentBuilder->method('select')->willReturnSelf();
|
|
$studentBuilder->method('where')->willReturnSelf();
|
|
$studentBuilder->method('orderBy')->willReturnSelf();
|
|
$studentBuilder->method('get')->willReturn(new class {
|
|
public function getResultArray(): array
|
|
{
|
|
return [
|
|
['id' => 1, 'firstname' => 'Ibrahim', 'lastname' => 'Khalid', 'parent_id' => 9],
|
|
['id' => 2, 'firstname' => 'Eesa', 'lastname' => 'Khalid', 'parent_id' => 9],
|
|
['id' => 3, 'firstname' => 'Lamia', 'lastname' => 'Khalid', 'parent_id' => 9],
|
|
['id' => 4, 'firstname' => 'New', 'lastname' => 'Khalid', 'parent_id' => 9],
|
|
];
|
|
}
|
|
});
|
|
|
|
$assignmentBuilder = $this->createMock(BaseBuilder::class);
|
|
$assignmentBuilder->method('select')->willReturnSelf();
|
|
$assignmentBuilder->method('join')->willReturnSelf();
|
|
$assignmentBuilder->method('where')->willReturnSelf();
|
|
$assignmentBuilder->method('orderBy')->willReturnSelf();
|
|
$assignmentBuilder->method('limit')->willReturnSelf();
|
|
$assignmentBuilder->method('get')->willReturnCallback(new class {
|
|
private int $calls = 0;
|
|
|
|
public function __invoke(): object
|
|
{
|
|
$this->calls++;
|
|
$hasSourceAssignment = $this->calls <= 3;
|
|
|
|
return new class($hasSourceAssignment) {
|
|
public function __construct(private readonly bool $hasSourceAssignment)
|
|
{
|
|
}
|
|
|
|
public function getRowArray(): ?array
|
|
{
|
|
return $this->hasSourceAssignment ? ['class_section_id' => 10] : null;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->willReturnCallback(static fn (string $table): bool => in_array($table, ['students', 'student_class'], true));
|
|
$db->method('table')->willReturnCallback(static function (string $table) use ($studentBuilder, $assignmentBuilder) {
|
|
return $table === 'students' ? $studentBuilder : $assignmentBuilder;
|
|
});
|
|
|
|
$service = new EnrollmentTransitionService($db);
|
|
$evaluation = [];
|
|
|
|
$this->invoke($service, 'applyHouseholdLastNameRule', [&$evaluation, 9, '2025-2026']);
|
|
|
|
$this->assertTrue($evaluation['family_name_check_ok']);
|
|
$this->assertArrayNotHasKey('blocking_rule_codes', $evaluation);
|
|
}
|
|
|
|
public function testPreviousYearBalanceBlocksEnrollment(): void
|
|
{
|
|
$service = $this->serviceWithFinance(125.50, 'submission_blocked_until_payment');
|
|
$evaluation = [];
|
|
|
|
$this->invoke($service, 'applyFinancialRule', [&$evaluation, 9, '2025-2026', '2026-2027']);
|
|
|
|
$this->assertContains('OUTSTANDING_BALANCE_BLOCKED', $evaluation['blocking_rule_codes']);
|
|
$this->assertSame(125.50, $evaluation['financial_summary']['carry_over_balance']);
|
|
}
|
|
|
|
public function testPreviousYearBalanceWithAdminApprovalPolicyUsesFinanceReviewCode(): void
|
|
{
|
|
$service = $this->serviceWithFinance(40.00, 'admin_approval_required');
|
|
$evaluation = [];
|
|
|
|
$this->invoke($service, 'applyFinancialRule', [&$evaluation, 9, '2025-2026', '2026-2027']);
|
|
|
|
$this->assertContains('FINANCE_APPROVAL_REQUIRED', $evaluation['blocking_rule_codes']);
|
|
}
|
|
|
|
public function testZeroCarryOverBalanceDoesNotBlock(): void
|
|
{
|
|
$service = $this->serviceWithFinance(0.00, 'submission_blocked_until_payment');
|
|
$evaluation = [];
|
|
|
|
$this->invoke($service, 'applyFinancialRule', [&$evaluation, 9, '2025-2026', '2026-2027']);
|
|
|
|
$this->assertArrayNotHasKey('blocking_rule_codes', $evaluation);
|
|
}
|
|
|
|
public function testActiveEnrollmentStatusesBlockDuplicates(): void
|
|
{
|
|
$service = new EnrollmentTransitionService($this->createMock(BaseConnection::class));
|
|
|
|
$this->assertTrue($this->invoke($service, 'activeEnrollmentBlocksDuplicate', [[
|
|
'is_withdrawn' => 0,
|
|
'enrollment_status' => 'payment pending',
|
|
'admission_status' => 'pending',
|
|
]]));
|
|
$this->assertFalse($this->invoke($service, 'activeEnrollmentBlocksDuplicate', [[
|
|
'is_withdrawn' => 1,
|
|
'enrollment_status' => 'withdrawn',
|
|
'admission_status' => 'accepted',
|
|
]]));
|
|
}
|
|
|
|
public function testTargetEnrollmentReviewCodesIncludeWithdrawnRows(): void
|
|
{
|
|
$builder = $this->createMock(BaseBuilder::class);
|
|
$builder->method('select')->willReturnSelf();
|
|
$builder->method('where')->willReturnSelf();
|
|
$builder->method('orderBy')->willReturnSelf();
|
|
$builder->method('get')->willReturn(new class {
|
|
public function getResultArray(): array
|
|
{
|
|
return [[
|
|
'student_id' => 42,
|
|
'enrollment_status' => 'payment pending',
|
|
'admission_status' => 'accepted',
|
|
'is_withdrawn' => 1,
|
|
]];
|
|
}
|
|
});
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->with('enrollments')->willReturn(true);
|
|
$db->method('fieldExists')->with('is_withdrawn', 'enrollments')->willReturn(true);
|
|
$db->method('table')->with('enrollments')->willReturn($builder);
|
|
|
|
$service = new EnrollmentTransitionService($db);
|
|
$reviewCodes = $this->invoke($service, 'targetEnrollmentReviewCodesByStudent', ['2026-2027']);
|
|
|
|
$this->assertSame('WITHDRAWN', $reviewCodes[42]['rule_code']);
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $rows
|
|
*/
|
|
private function serviceWithStudentRows(array $rows): EnrollmentTransitionService
|
|
{
|
|
$builder = $this->createMock(BaseBuilder::class);
|
|
$builder->method('select')->willReturnSelf();
|
|
$builder->method('where')->willReturnSelf();
|
|
$builder->method('orderBy')->willReturnSelf();
|
|
$builder->method('get')->willReturn(new class($rows) {
|
|
public function __construct(private readonly array $rows)
|
|
{
|
|
}
|
|
|
|
public function getResultArray(): array
|
|
{
|
|
return $this->rows;
|
|
}
|
|
});
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->with('students')->willReturn(true);
|
|
$db->method('table')->with('students')->willReturn($builder);
|
|
|
|
return new EnrollmentTransitionService($db);
|
|
}
|
|
|
|
private function serviceWithFinance(float $balance, string $behavior): EnrollmentTransitionService
|
|
{
|
|
$yearBuilder = $this->builderReturning([
|
|
'name' => '2026-2027',
|
|
'carry_over_balance_behavior' => $behavior,
|
|
'registration_fee' => 0,
|
|
'tuition_due_at_registration' => 0,
|
|
'mandatory_fees' => 0,
|
|
]);
|
|
$yearBuilder->method('where')->willReturnSelf();
|
|
$invoiceBuilder = $this->builderReturning(['balance' => $balance]);
|
|
$invoiceBuilder->method('where')->willReturnSelf();
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->willReturn(true);
|
|
$db->method('table')->willReturnCallback(static function (string $table) use ($yearBuilder, $invoiceBuilder) {
|
|
return $table === 'school_years' ? $yearBuilder : $invoiceBuilder;
|
|
});
|
|
|
|
return new EnrollmentTransitionService($db);
|
|
}
|
|
|
|
public function testPreviousLastNameExceptionCarriesForwardWhenHouseholdUnchangedAndBalancePaid(): void
|
|
{
|
|
$service = $this->serviceWithLastNameCarryForward(
|
|
[
|
|
['id' => 1, 'firstname' => 'Amina', 'lastname' => 'Hassan', 'parent_id' => 9],
|
|
['id' => 2, 'firstname' => 'Omar', 'lastname' => 'Ali', 'parent_id' => 9],
|
|
],
|
|
[[
|
|
'id' => 44,
|
|
'parent_id' => 9,
|
|
'student_id' => 1,
|
|
'school_year' => '2025-2026',
|
|
'status' => 'used',
|
|
'reason_code' => 'SIBLING_LAST_NAME_REVIEWED',
|
|
'bypassed_rule_codes_json' => json_encode(['SIBLING_LAST_NAME_MISMATCH']),
|
|
'family_student_ids_json' => json_encode([1, 2]),
|
|
]]
|
|
);
|
|
$evaluation = [
|
|
'blocking_rule_codes' => ['SIBLING_LAST_NAME_MISMATCH'],
|
|
'blockers' => ['Linked siblings have different last names. Please contact administration to review the family record.'],
|
|
'warnings' => [],
|
|
];
|
|
|
|
$this->invoke($service, 'applyCarriedForwardLastNameException', [&$evaluation, 9, 1, '2025-2026', '2026-2027']);
|
|
|
|
$this->assertNotContains('SIBLING_LAST_NAME_MISMATCH', $evaluation['blocking_rule_codes']);
|
|
$this->assertContains('LAST_NAME_EXCEPTION_CARRIED_FORWARD', $evaluation['warning_rule_codes']);
|
|
$this->assertTrue($evaluation['last_name_exception_carry_forward']['eligible']);
|
|
}
|
|
|
|
public function testEmptyBypassCodesDoNotGrantException(): void
|
|
{
|
|
$builder = $this->createMock(BaseBuilder::class);
|
|
$builder->method('where')->willReturnSelf();
|
|
$builder->method('groupStart')->willReturnSelf();
|
|
$builder->method('orWhere')->willReturnSelf();
|
|
$builder->method('groupEnd')->willReturnSelf();
|
|
$builder->method('whereIn')->willReturnSelf();
|
|
$builder->method('orderBy')->willReturnSelf();
|
|
$builder->method('limit')->willReturnSelf();
|
|
$builder->method('get')->willReturn(new class {
|
|
public function getRowArray(): array
|
|
{
|
|
return [
|
|
'id' => 7,
|
|
'reason_code' => 'FINANCIAL_REVIEW_REQUIRED',
|
|
'bypassed_rule_codes_json' => '[]',
|
|
];
|
|
}
|
|
|
|
public function getResultArray(): array
|
|
{
|
|
return [$this->getRowArray()];
|
|
}
|
|
});
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->with('enrollment_exceptions')->willReturn(true);
|
|
$db->method('table')->with('enrollment_exceptions')->willReturn($builder);
|
|
|
|
$service = new EnrollmentTransitionService($db);
|
|
$evaluation = [
|
|
'blocking_rule_codes' => ['OUTSTANDING_BALANCE_BLOCKED'],
|
|
'review_rule_codes' => [],
|
|
];
|
|
|
|
$this->invoke($service, 'applyScopedException', [&$evaluation, 9, 1, '2025-2026', '2026-2027']);
|
|
|
|
$this->assertArrayNotHasKey('admin_exception', $evaluation);
|
|
$this->assertArrayNotHasKey('parent_enrollment_allowed', $evaluation);
|
|
}
|
|
|
|
public function testClassReassignmentFlagIsIgnored(): void
|
|
{
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->with('enrollment_flags')->willReturn(true);
|
|
$db->expects($this->never())->method('table');
|
|
|
|
$service = new EnrollmentTransitionService($db);
|
|
|
|
$written = $this->invoke($service, 'upsertOpenFlag', [
|
|
123,
|
|
'2026-2027',
|
|
'2025-2026',
|
|
[
|
|
'flag_type' => 'CLASS_REASSIGNMENT_REQUIRED',
|
|
'priority' => 'normal',
|
|
'details' => ['reason' => 'repeat class'],
|
|
],
|
|
null,
|
|
]);
|
|
|
|
$this->assertFalse($written);
|
|
}
|
|
|
|
public function testPreviousLastNameExceptionDoesNotCarryForwardWhenNewStudentIsAdded(): void
|
|
{
|
|
$service = $this->serviceWithLastNameCarryForward(
|
|
[
|
|
['id' => 1, 'firstname' => 'Amina', 'lastname' => 'Hassan', 'parent_id' => 9],
|
|
['id' => 2, 'firstname' => 'Omar', 'lastname' => 'Ali', 'parent_id' => 9],
|
|
['id' => 3, 'firstname' => 'Layla', 'lastname' => 'Hassan', 'parent_id' => 9],
|
|
],
|
|
[[
|
|
'id' => 44,
|
|
'parent_id' => 9,
|
|
'student_id' => 1,
|
|
'school_year' => '2025-2026',
|
|
'status' => 'used',
|
|
'reason_code' => 'SIBLING_LAST_NAME_REVIEWED',
|
|
'bypassed_rule_codes_json' => json_encode(['SIBLING_LAST_NAME_MISMATCH']),
|
|
'family_student_ids_json' => json_encode([1, 2]),
|
|
]]
|
|
);
|
|
$evaluation = [
|
|
'blocking_rule_codes' => ['SIBLING_LAST_NAME_MISMATCH'],
|
|
'blockers' => ['Linked siblings have different last names. Please contact administration to review the family record.'],
|
|
];
|
|
|
|
$this->invoke($service, 'applyCarriedForwardLastNameException', [&$evaluation, 9, 1, '2025-2026', '2026-2027']);
|
|
|
|
$this->assertContains('SIBLING_LAST_NAME_MISMATCH', $evaluation['blocking_rule_codes']);
|
|
$this->assertFalse($evaluation['last_name_exception_carry_forward']['eligible']);
|
|
$this->assertSame('NEW_STUDENT_ADDED', $evaluation['last_name_exception_carry_forward']['reason']);
|
|
}
|
|
|
|
public function testPreviousLastNameExceptionDoesNotCarryForwardWhenBalanceIsUnpaid(): void
|
|
{
|
|
$service = new EnrollmentTransitionService($this->createMock(BaseConnection::class));
|
|
$evaluation = [
|
|
'blocking_rule_codes' => ['SIBLING_LAST_NAME_MISMATCH', 'OUTSTANDING_BALANCE_BLOCKED'],
|
|
];
|
|
|
|
$this->invoke($service, 'applyCarriedForwardLastNameException', [&$evaluation, 9, 1, '2025-2026', '2026-2027']);
|
|
|
|
$this->assertContains('SIBLING_LAST_NAME_MISMATCH', $evaluation['blocking_rule_codes']);
|
|
$this->assertArrayNotHasKey('last_name_exception_carry_forward', $evaluation);
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $students
|
|
* @param list<array<string, mixed>> $exceptions
|
|
*/
|
|
private function serviceWithLastNameCarryForward(array $students, array $exceptions): EnrollmentTransitionService
|
|
{
|
|
$studentBuilder = $this->createMock(BaseBuilder::class);
|
|
$studentBuilder->method('select')->willReturnSelf();
|
|
$studentBuilder->method('where')->willReturnSelf();
|
|
$studentBuilder->method('orderBy')->willReturnSelf();
|
|
$studentBuilder->method('get')->willReturn(new class($students) {
|
|
public function __construct(private readonly array $rows)
|
|
{
|
|
}
|
|
|
|
public function getResultArray(): array
|
|
{
|
|
return $this->rows;
|
|
}
|
|
});
|
|
|
|
$exceptionBuilder = $this->createMock(BaseBuilder::class);
|
|
$exceptionBuilder->method('where')->willReturnSelf();
|
|
$exceptionBuilder->method('whereIn')->willReturnSelf();
|
|
$exceptionBuilder->method('orderBy')->willReturnSelf();
|
|
$exceptionBuilder->method('get')->willReturn(new class($exceptions) {
|
|
public function __construct(private readonly array $rows)
|
|
{
|
|
}
|
|
|
|
public function getResultArray(): array
|
|
{
|
|
return $this->rows;
|
|
}
|
|
});
|
|
|
|
$db = $this->createMock(BaseConnection::class);
|
|
$db->method('tableExists')->willReturn(true);
|
|
$db->method('table')->willReturnCallback(static function (string $table) use ($studentBuilder, $exceptionBuilder) {
|
|
return $table === 'students' ? $studentBuilder : $exceptionBuilder;
|
|
});
|
|
|
|
return new EnrollmentTransitionService($db);
|
|
}
|
|
|
|
/**
|
|
* @param list<mixed> $args
|
|
*/
|
|
private function invoke(EnrollmentTransitionService $service, string $method, array $args): mixed
|
|
{
|
|
$reflection = new \ReflectionMethod($service, $method);
|
|
$reflection->setAccessible(true);
|
|
|
|
return $reflection->invokeArgs($service, $args);
|
|
}
|
|
}
|