Files
alrahma_sunday_school/app/Support/Enrollment/EnrollmentEligibility.php
T
root 9899af7b37
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Failing after 1m18s
fix all issues before deploy
2026-08-25 03:10:44 -04:00

206 lines
10 KiB
PHP

<?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 KG_MISSING_DECISION_ELIGIBLE_MESSAGE = 'KG students may complete registration now. Their new-year grade placement will be based on the school age-placement rule.';
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 = 'This student is over the allowed age for enrollment or re-enrollment at the school. Please contact school administration.';
public const ADULT_STUDENT_PARENT_PORTAL_MESSAGE = self::ADULT_STUDENT_MESSAGE;
public const WITHDRAWN_PORTAL_MESSAGE = 'This student is currently marked as Withdrawn and cannot be enrolled at this time. Please contact school administration.';
public const SIBLING_PORTAL_MESSAGE = 'Enrollment cannot continue because the family record requires administrative review. Please contact school administration.';
public const BALANCE_PORTAL_MESSAGE = 'Enrollment cannot continue because there is an outstanding balance. Please contact school administration.';
public const EXPELLED_PORTAL_MESSAGE = 'This student cannot be enrolled through the parent portal. Please contact school administration.';
public const FINANCE_APPROVAL_PORTAL_MESSAGE = 'Registration requires administrative financial approval. Please contact school administration.';
public const ALREADY_ENROLLED_MESSAGE = 'This student is already enrolled for the selected school year.';
public const ALREADY_SUBMITTED_MESSAGE = 'This student already has an enrollment for the selected school year.';
public const ADULT_STUDENT_MIN_AGE = 18;
/** @var array<string, int> Lower numbers win when selecting the primary parent-facing blocker. */
public const BLOCKER_PRIORITY = [
'ALREADY_ENROLLED' => 0,
'ADULT_STUDENT_PARENT_BLOCKED' => 1,
'ADULT_STUDENT_ACTION_REQUIRED' => 1,
'EXPELLED' => 2,
'WITHDRAWN' => 3,
'OUTSTANDING_BALANCE_BLOCKED' => 4,
'SIBLING_LAST_NAME_MISMATCH' => 5,
'FINANCE_APPROVAL_REQUIRED' => 6,
'DEFERRED_DECISION' => 7,
'NO_FINAL_DECISION' => 8,
'UNRECOGNIZED_DECISION' => 9,
];
public static function messageForRuleCode(string $ruleCode): string
{
return match ($ruleCode) {
'ADULT_STUDENT_PARENT_BLOCKED', 'ADULT_STUDENT_ACTION_REQUIRED' => self::ADULT_STUDENT_PARENT_PORTAL_MESSAGE,
'EXPELLED' => self::EXPELLED_PORTAL_MESSAGE,
'WITHDRAWN' => self::WITHDRAWN_PORTAL_MESSAGE,
'OUTSTANDING_BALANCE_BLOCKED' => self::BALANCE_PORTAL_MESSAGE,
'SIBLING_LAST_NAME_MISMATCH' => self::SIBLING_PORTAL_MESSAGE,
'FINANCE_APPROVAL_REQUIRED' => self::FINANCE_APPROVAL_PORTAL_MESSAGE,
'DEFERRED_DECISION' => self::DEFERRED_MESSAGE,
'NO_FINAL_DECISION', 'UNRECOGNIZED_DECISION' => self::MISSING_DECISION_MESSAGE,
'ALREADY_ENROLLED' => self::ALREADY_ENROLLED_MESSAGE,
default => 'Enrollment cannot continue at this time. Please contact school administration.',
};
}
public static function alreadyEnrolledMessage(?string $enrollmentStatus = null): string
{
return match (strtolower(trim((string) $enrollmentStatus))) {
'payment pending' => 'This student already has an enrollment for the selected school year. Payment is still pending.',
'admission under review', 'review & decision' => 'This student already has an enrollment submitted for the selected school year and it is under review.',
'waitlist' => 'This student is already on the waitlist for the selected school year.',
'withdraw under review' => 'This student is already enrolled and has a withdrawal request under review.',
'refund pending' => 'This student already has an enrollment for the selected school year. A refund is pending.',
'enrolled' => self::ALREADY_ENROLLED_MESSAGE,
default => self::ALREADY_SUBMITTED_MESSAGE,
};
}
public static function alreadyEnrolledTitle(?string $enrollmentStatus = null): string
{
return match (strtolower(trim((string) $enrollmentStatus))) {
'payment pending', 'admission under review', 'review & decision' => 'Enrollment Already Submitted',
'waitlist' => 'Already on Waitlist',
'withdraw under review' => 'Withdrawal Under Review',
'refund pending' => 'Refund Pending',
default => 'Already Enrolled',
};
}
/**
* @param array<string, mixed> $student
*/
public static function isMarkedWithdrawn(array $student): bool
{
if ((int) ($student['is_withdrawn'] ?? 0) === 1) {
return true;
}
$status = strtolower(trim((string) ($student['enrollment_status'] ?? '')));
return $status === 'withdrawn' || $status === 'withdrawn student';
}
/**
* @param list<string> $blockers
*/
public static function selectPrimaryBlocker(array $blockers): ?string
{
$blockers = array_values(array_unique(array_filter(array_map('strval', $blockers))));
if ($blockers === []) {
return null;
}
usort($blockers, static function (string $a, string $b): int {
$priorityA = self::BLOCKER_PRIORITY[$a] ?? 999;
$priorityB = self::BLOCKER_PRIORITY[$b] ?? 999;
return $priorityA <=> $priorityB ?: strcmp($a, $b);
});
return $blockers[0];
}
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');
}
$age = self::ageOnSeptemberFirst($student['dob'] ?? null, $targetSchoolYear);
if ($age !== null && $age >= self::ADULT_STUDENT_MIN_AGE) {
return self::message(str_replace('The student', $name, self::ADULT_STUDENT_MESSAGE), true, 'danger');
}
if ($decision === DeliberationDecision::WITHDRAWN || self::isMarkedWithdrawn($student)) {
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');
}
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,
];
}
}