fix enrollment for the new school-year
Tests / PHPUnit (push) Successful in 1m26s

This commit is contained in:
root
2026-08-07 23:43:31 -04:00
parent b6f3b14e7b
commit 1ef2800f12
66 changed files with 8997 additions and 498 deletions
@@ -0,0 +1,71 @@
<?php
namespace App\Support\Enrollment;
final class DeliberationDecision
{
public const PASSED = 'PASSED';
public const REPEAT_CLASS = 'REPEAT_CLASS';
public const MAKE_UP_EXAM = 'MAKE_UP_EXAM';
public const EXPELLED = 'EXPELLED';
public const WITHDRAWN = 'WITHDRAWN';
public const DEFERRED_DECISION = 'DEFERRED_DECISION';
public const ALL = [
self::PASSED,
self::REPEAT_CLASS,
self::MAKE_UP_EXAM,
self::EXPELLED,
self::WITHDRAWN,
self::DEFERRED_DECISION,
];
public static function normalize(?string $decision): ?string
{
$value = trim((string) $decision);
if ($value === '') {
return null;
}
$key = strtoupper((string) preg_replace('/[^A-Z0-9]+/', '_', $value));
$key = trim((string) preg_replace('/_+/', '_', $key), '_');
if (in_array($key, self::ALL, true)) {
return $key;
}
$compact = strtolower((string) preg_replace('/[^a-z0-9]+/', '', strtolower($value)));
return match (true) {
in_array($compact, ['pass', 'passed', 'promote', 'promoted'], true) => self::PASSED,
str_contains($compact, 'repeat') || str_contains($compact, 'keepkg') => self::REPEAT_CLASS,
str_contains($compact, 'makeupexam') || str_contains($compact, 'makeupexamfall') || str_contains($compact, 'makeup') => self::MAKE_UP_EXAM,
str_contains($compact, 'deferred') || str_contains($compact, 'pendingdecision') => self::DEFERRED_DECISION,
str_contains($compact, 'withdraw') || str_contains($compact, 'widthraw') || str_contains($compact, 'widthdraw') || str_contains($compact, 'widthrwan') => self::WITHDRAWN,
str_contains($compact, 'expel') => self::EXPELLED,
default => null,
};
}
public static function display(?string $decision): string
{
return match (self::normalize($decision) ?? $decision) {
self::PASSED => 'Passed',
self::REPEAT_CLASS => 'Repeat Class',
self::MAKE_UP_EXAM => 'Make-up Exam',
self::EXPELLED => 'Expelled',
self::WITHDRAWN => 'Withdrawn',
self::DEFERRED_DECISION => 'Deferred Decision',
default => trim((string) $decision),
};
}
public static function blocksEnrollment(?string $decision): bool
{
return in_array(self::normalize($decision), [
self::EXPELLED,
self::WITHDRAWN,
self::DEFERRED_DECISION,
], true);
}
}
@@ -0,0 +1,105 @@
<?php
namespace App\Support\Enrollment;
final class EnrollmentEligibility
{
public const EXPELLED_MESSAGE = 'Re-enrollment is not available because the final deliberation decision is expelled. Please contact the school administration for further information.';
public const WITHDRAWN_MESSAGE = 'Re-enrollment is not available because the final deliberation decision is withdrawn. Please contact the school administration if this status needs to be reviewed.';
public const DEFERRED_MESSAGE = 'Re-enrollment cannot currently be completed because the final deliberation decision is deferred. Please contact the school administration for the next required step.';
public const MISSING_DECISION_MESSAGE = '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.';
public const ADULT_STUDENT_MESSAGE = 'The student will be 18 years old or older on September 1. A parent or guardian cannot complete registration on the students behalf. The student must complete the authorized adult-student registration process or contact the school administration.';
public static function ageOnSeptemberFirst(?string $dob, string $targetSchoolYear): ?int
{
$dob = trim((string) $dob);
if ($dob === '' || ! preg_match('/^(\d{4})/', trim($targetSchoolYear), $matches)) {
return null;
}
try {
$birthDate = \DateTimeImmutable::createFromFormat('!Y-m-d', $dob);
$errors = \DateTimeImmutable::getLastErrors();
if (
$birthDate === false
|| (is_array($errors) && (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0))
) {
return null;
}
$referenceDate = new \DateTimeImmutable($matches[1] . '-09-01');
} catch (\Throwable) {
return null;
}
return $birthDate > $referenceDate ? null : $birthDate->diff($referenceDate)->y;
}
public static function parentDecisionMessage(
array $student,
?array $decisionRow,
string $targetSchoolYear,
?string $fallMakeupExamOn = null
): array {
$name = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? ''));
$name = $name !== '' ? $name : 'The student';
$decision = DeliberationDecision::normalize($decisionRow['decision'] ?? null);
$source = strtolower(trim((string) ($decisionRow['source'] ?? '')));
if ($decision === DeliberationDecision::EXPELLED) {
return self::message($name . ': ' . self::EXPELLED_MESSAGE, true, 'danger');
}
if ($decision === DeliberationDecision::WITHDRAWN || ($student['enrollment_status'] ?? null) === 'withdrawn') {
return self::message($name . ': ' . self::WITHDRAWN_MESSAGE, true, 'warning');
}
if ($decision === DeliberationDecision::DEFERRED_DECISION) {
return self::message($name . ': ' . self::DEFERRED_MESSAGE, true, 'warning');
}
if ($decision === null && $source === 'pending') {
return self::message($name . ': ' . self::MISSING_DECISION_MESSAGE, true, 'warning');
}
$age = self::ageOnSeptemberFirst($student['dob'] ?? null, $targetSchoolYear);
if ($age !== null && $age >= 18) {
return self::message(str_replace('The student', $name, self::ADULT_STUDENT_MESSAGE), true, 'danger');
}
if ($decision === DeliberationDecision::MAKE_UP_EXAM) {
$dateText = $fallMakeupExamOn !== null ? ' on ' . local_date($fallMakeupExamOn, 'm-d-Y') : '';
return self::message(
$name . ' has a make-up exam decision. Enrollment is allowed, but the student will initially remain in the same grade as the closed school year until the fall make-up exam' . $dateText . ' is passed and administration confirms promotion.',
false,
'warning'
);
}
if ($decision === DeliberationDecision::REPEAT_CLASS) {
$previousClass = trim((string) ($decisionRow['class_section_name'] ?? ''));
$previousClass = $previousClass !== '' ? $previousClass : 'the same class';
return self::message($name . ' has a repeat class decision and is accepted only in ' . $previousClass . '.', false, 'info');
}
if ($decision === DeliberationDecision::PASSED || ($decision === null && $source !== 'pending' && trim((string) ($decisionRow['decision'] ?? '')) === '')) {
return self::message('', false, 'info');
}
return self::message(
$name . ' does not have a final academic decision that permits online re-enrollment. Please contact the administration.',
true,
'warning'
);
}
private static function message(string $message, bool $blocking, string $level): array
{
return [
'message' => $message,
'blocking' => $blocking,
'level' => $level,
];
}
}