service();
$evaluation = [
'deliberation_decision' => DeliberationDecision::PASSED,
'placement_status' => 'exit_required',
'source_grade_name' => '8',
'adult_student' => false,
'blockers' => [
'The student has passed the highest available grade and must follow the school completion or exit process.',
'Registration for the new school year has not opened yet.',
],
];
$status = $this->invoke($service, 'registrationStatus', [$evaluation]);
$message = $this->invoke($service, 'decisionMessage', ['Student Name', $evaluation, 'August 1, 2026', 'August 31, 2026']);
$action = $this->invoke($service, 'requiredAction', [$evaluation, 'August 31, 2026']);
$this->assertSame('Eligible', $status);
$this->assertStringContainsString('Student Name has successfully passed Grade 8.', $message);
$this->assertStringNotContainsString('Re-enrollment for the new school year will open on August 1, 2026.', $message);
$this->assertStringNotContainsString('Please make sure to re-enroll your child before August 31, 2026', $message);
$this->assertStringNotContainsString('sign in to the parent portal', $message);
$this->assertStringContainsString('Complete re-enrollment before August 31, 2026.', $action);
$this->assertStringNotContainsString('Not Eligible', $status);
$this->assertStringNotContainsString('Registration for the new school year has not opened yet', $message);
$this->assertStringNotContainsString('Contact the school administration.', $action);
}
public function testFinancialSectionIsHiddenWhenNothingIsDue(): void
{
$service = $this->service();
$html = $this->invoke($service, 'financialSection', [10, [
'registration_fee' => 0,
'tuition_due_at_registration' => 0,
'mandatory_fees' => 0,
], null]);
$this->assertSame('', $html);
}
public function testMakeupExamDecisionMessageUsesConfiguredDate(): void
{
$configModel = $this->createMock(ConfigurationModel::class);
$configModel->expects($this->once())
->method('getConfig')
->with('make-up-exam')
->willReturn('2026-09-13');
$service = $this->service(configModel: $configModel);
$message = $this->invoke($service, 'decisionMessage', ['Student Name', [
'deliberation_decision' => DeliberationDecision::MAKE_UP_EXAM,
'blockers' => [],
], 'August 1, 2026', 'August 31, 2026']);
$this->assertStringContainsString('The final academic decision for Student Name is currently pending the result of a make-up exam.', $message);
$this->assertStringContainsString('This exam is scheduled for 09-13-2026 from 9:30 AM to 11:00 AM at ISGL.', $message);
$this->assertStringContainsString("\n" . 'The exam result will determine whether the student advances to the next grade or repeats the current class. Failure to attend the make-up exam will automatically result in the student repeating the class, as no further retake opportunities will be available.', $message);
$this->assertStringContainsString("\n" . 'You must re-enroll Student Name before the make-up exam can be taken.', $message);
$this->assertStringNotContainsString('on date at ISGL', $message);
$action = $this->invoke($service, 'requiredAction', [[
'deliberation_decision' => DeliberationDecision::MAKE_UP_EXAM,
'blockers' => [],
], 'August 31, 2026', 'Student Name']);
$this->assertSame('', $action);
}
public function testKgMissingDecisionEligibleMessageAllowsRegistration(): void
{
$service = $this->service();
$message = $this->invoke($service, 'decisionMessage', ['Student Name', [
'can_enroll' => false,
'kg_missing_decision_eligible' => true,
'deliberation_decision' => DeliberationDecision::PASSED,
'placement_status' => 'automatic_distribution_pending',
'assigned_grade_name' => '1',
'blocking_rule_codes' => ['NO_FINAL_DECISION'],
'blockers' => ['Re-enrollment cannot currently be completed because no final deliberation decision is recorded for the student. Registration will become available after the school records a final decision.'],
], 'August 1, 2026', 'August 31, 2026']);
$status = $this->invoke($service, 'registrationStatus', [[
'can_enroll' => false,
'kg_missing_decision_eligible' => true,
'deliberation_decision' => DeliberationDecision::PASSED,
'blocking_rule_codes' => ['NO_FINAL_DECISION'],
'blockers' => ['Re-enrollment cannot currently be completed because no final deliberation decision is recorded for the student. Registration will become available after the school records a final decision.'],
]]);
$this->assertSame('Eligible', $status);
$this->assertStringContainsString('KG students may complete registration now.', $message);
$this->assertStringContainsString('Student Name is eligible for re-enrollment even though no final deliberation decision is recorded.', $message);
$this->assertStringContainsString('Grade 1', $message);
$this->assertStringNotContainsString('cannot currently be completed', $message);
}
public function testAlreadyEnrolledStatusDoesNotRenderAsNotEligible(): void
{
$service = $this->service();
$status = $this->invoke($service, 'registrationStatus', [[
'can_enroll' => false,
'deliberation_decision' => DeliberationDecision::PASSED,
'decision' => 'ALREADY_ENROLLED',
'primary_block_reason' => 'ALREADY_ENROLLED',
'blocking_rule_codes' => ['ALREADY_ENROLLED'],
'blockers' => ['This student is already enrolled for the selected school year.'],
'enrollment_status' => 'enrolled',
]]);
$this->assertSame('Already Enrolled', $status);
}
public function testMakeupExamDecisionMessageUsesSchoolYearDateBeforeConfig(): void
{
$configModel = $this->createMock(ConfigurationModel::class);
$configModel->expects($this->never())->method('getConfig');
$service = $this->service(configModel: $configModel);
$message = $this->invoke($service, 'decisionMessage', ['Student Name', [
'deliberation_decision' => DeliberationDecision::MAKE_UP_EXAM,
'blockers' => [],
], 'August 1, 2026', 'August 31, 2026', [
'fall_makeup_exam_on' => '2026-09-13',
]]);
$this->assertStringContainsString('This exam is scheduled for 09-13-2026 from 9:30 AM to 11:00 AM at ISGL.', $message);
$this->assertStringNotContainsString('a date to be announced', $message);
}
public function testMakeupExamDecisionMessageFallsBackToSyncedConfigKeys(): void
{
$configModel = $this->createMock(ConfigurationModel::class);
$configModel->expects($this->exactly(2))
->method('getConfig')
->willReturnMap([
['make-up-exam', ''],
['make_up_exam', '2026-09-13'],
]);
$service = $this->service(configModel: $configModel);
$message = $this->invoke($service, 'decisionMessage', ['Student Name', [
'deliberation_decision' => DeliberationDecision::MAKE_UP_EXAM,
'blockers' => [],
], 'August 1, 2026', 'August 31, 2026']);
$this->assertStringContainsString('This exam is scheduled for 09-13-2026 from 9:30 AM to 11:00 AM at ISGL.', $message);
}
public function testMakeupExamStudentRowRendersMessageLineBreaks(): void
{
$configModel = $this->createMock(ConfigurationModel::class);
$configModel->method('getConfig')
->with('make-up-exam')
->willReturn('2026-09-13');
$service = $this->service(configModel: $configModel);
$html = $this->invoke($service, 'studentRow', [[
'firstname' => 'Student',
'lastname' => 'Name',
], [
'deliberation_decision' => DeliberationDecision::MAKE_UP_EXAM,
'placement_status' => 'temporary_same_grade',
'blockers' => [],
], 'August 1, 2026', 'August 31, 2026']);
$this->assertStringContainsString('from 9:30 AM to 11:00 AM at ISGL.
', $html);
$this->assertStringContainsString('available.
', $html);
$this->assertStringContainsString('You must re-enroll Student Name before the make-up exam can be taken.', $html);
$this->assertStringNotContainsString('before they are permitted', $html);
}
public function testFinancialSectionShowsWhenAnyAmountIsDue(): void
{
$service = $this->service();
$html = $this->invoke($service, 'financialSection', [10, [
'registration_fee' => 25,
'tuition_due_at_registration' => 10,
'mandatory_fees' => 5,
], null]);
$this->assertStringContainsString('Family Account Information', $html);
$this->assertStringContainsString('Carry-over balance: $0.00', $html);
$this->assertStringContainsString('Total currently due: $40.00', $html);
$this->assertStringNotContainsString('Registration fee:', $html);
$this->assertStringNotContainsString('New-year tuition due now:', $html);
$this->assertStringNotContainsString('Mandatory fees:', $html);
}
public function testAutomaticDistributionPlacementShowsOnlyGrade(): void
{
$service = $this->service();
$placement = $this->invoke($service, 'placementText', [[
'placement_status' => 'automatic_distribution_pending',
'assigned_grade_name' => '9',
]]);
$this->assertSame('Grade 9', $placement);
}
public function testStudentSummaryUsesStackedEmailRows(): void
{
$service = $this->service();
$html = $this->invoke($service, 'studentRow', [[
'firstname' => 'Test',
'lastname' => 'Student',
], [
'deliberation_decision' => DeliberationDecision::PASSED,
'placement_status' => 'automatic_distribution_pending',
'source_grade_name' => '4',
'assigned_grade_name' => '5',
'blockers' => [],
], 'August 1, 2026', 'August 31, 2026']);
$this->assertStringContainsString('role="presentation"', $html);
$this->assertStringContainsString('Decision', $html);
$this->assertStringContainsString('Registration Status', $html);
$this->assertStringContainsString('Next Step', $html);
$this->assertStringContainsString('word-break:break-word', $html);
$this->assertStringNotContainsString('