fix all issues before deploy
This commit is contained in:
@@ -484,12 +484,13 @@ class EnrollmentAdminController extends BaseController
|
|||||||
if (is_numeric($assignedTo) && (int) $assignedTo > 0) {
|
if (is_numeric($assignedTo) && (int) $assignedTo > 0) {
|
||||||
$builder->where('ef.assigned_to', (int) $assignedTo);
|
$builder->where('ef.assigned_to', (int) $assignedTo);
|
||||||
}
|
}
|
||||||
$builder->where('ef.flag_type !=', 'CLASS_CAPACITY_EXCEPTION_REQUIRED');
|
$ignoredFlagTypes = ['CLASS_CAPACITY_EXCEPTION_REQUIRED', 'CLASS_REASSIGNMENT_REQUIRED'];
|
||||||
|
$builder->whereNotIn('ef.flag_type', $ignoredFlagTypes);
|
||||||
|
|
||||||
$rows = $builder->get()->getResultArray();
|
$rows = $builder->get()->getResultArray();
|
||||||
$rows = array_values(array_filter(
|
$rows = array_values(array_filter(
|
||||||
$rows,
|
$rows,
|
||||||
static fn (array $row): bool => (string) ($row['flag_type'] ?? '') !== 'CLASS_CAPACITY_EXCEPTION_REQUIRED'
|
static fn (array $row): bool => ! in_array((string) ($row['flag_type'] ?? ''), $ignoredFlagTypes, true)
|
||||||
));
|
));
|
||||||
foreach ($rows as &$row) {
|
foreach ($rows as &$row) {
|
||||||
$row['student_name'] = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')) ?: 'Student #' . (int) ($row['student_id'] ?? 0);
|
$row['student_name'] = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')) ?: 'Student #' . (int) ($row['student_id'] ?? 0);
|
||||||
@@ -663,7 +664,6 @@ class EnrollmentAdminController extends BaseController
|
|||||||
|
|
||||||
$followupTypes = [
|
$followupTypes = [
|
||||||
'PENDING_MAKE_UP_EXAM_PROMOTION' => 'temporary_same_grade',
|
'PENDING_MAKE_UP_EXAM_PROMOTION' => 'temporary_same_grade',
|
||||||
'CLASS_REASSIGNMENT_REQUIRED' => 'manual_class_required',
|
|
||||||
'COMPLETION_OR_EXIT_PROCESS_REQUIRED' => 'exit_required',
|
'COMPLETION_OR_EXIT_PROCESS_REQUIRED' => 'exit_required',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1251,7 +1251,7 @@ class EnrollmentAdminController extends BaseController
|
|||||||
|
|
||||||
$builder = $this->db->table('enrollment_flags')
|
$builder = $this->db->table('enrollment_flags')
|
||||||
->where('status', 'open')
|
->where('status', 'open')
|
||||||
->where('flag_type !=', 'CLASS_CAPACITY_EXCEPTION_REQUIRED');
|
->whereNotIn('flag_type', ['CLASS_CAPACITY_EXCEPTION_REQUIRED', 'CLASS_REASSIGNMENT_REQUIRED']);
|
||||||
if ($schoolYear !== '') {
|
if ($schoolYear !== '') {
|
||||||
$builder->where('school_year', $schoolYear);
|
$builder->where('school_year', $schoolYear);
|
||||||
}
|
}
|
||||||
@@ -1268,7 +1268,7 @@ class EnrollmentAdminController extends BaseController
|
|||||||
$types = array_column(
|
$types = array_column(
|
||||||
$this->db->table('enrollment_flags')
|
$this->db->table('enrollment_flags')
|
||||||
->select('flag_type')
|
->select('flag_type')
|
||||||
->where('flag_type !=', 'CLASS_CAPACITY_EXCEPTION_REQUIRED')
|
->whereNotIn('flag_type', ['CLASS_CAPACITY_EXCEPTION_REQUIRED', 'CLASS_REASSIGNMENT_REQUIRED'])
|
||||||
->distinct()
|
->distinct()
|
||||||
->orderBy('flag_type')
|
->orderBy('flag_type')
|
||||||
->get()
|
->get()
|
||||||
@@ -1276,7 +1276,10 @@ class EnrollmentAdminController extends BaseController
|
|||||||
'flag_type'
|
'flag_type'
|
||||||
);
|
);
|
||||||
|
|
||||||
return array_values(array_filter($types, static fn ($type): bool => (string) $type !== 'CLASS_CAPACITY_EXCEPTION_REQUIRED'));
|
return array_values(array_filter(
|
||||||
|
$types,
|
||||||
|
static fn ($type): bool => ! in_array((string) $type, ['CLASS_CAPACITY_EXCEPTION_REQUIRED', 'CLASS_REASSIGNMENT_REQUIRED'], true)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function schoolYears(): array
|
private function schoolYears(): array
|
||||||
|
|||||||
@@ -730,12 +730,19 @@ class InvoiceController extends ResourceController
|
|||||||
|
|
||||||
$grade = 'N/A';
|
$grade = 'N/A';
|
||||||
if ($studentClass && isset($studentClass['class_section_id'])) {
|
if ($studentClass && isset($studentClass['class_section_id'])) {
|
||||||
$classSection = $this->db->table('classSection')
|
$classSectionBuilder = $this->db->table('classSection')
|
||||||
->where('class_section_id', $studentClass['class_section_id'])
|
->select('classSection.class_section_id, classSection.class_section_name, classSection.class_id, classes.class_name')
|
||||||
->get()
|
->join('classes', 'classes.id = classSection.class_id', 'left')
|
||||||
->getRowArray();
|
->where('classSection.class_section_id', $studentClass['class_section_id']);
|
||||||
if ($classSection && isset($classSection['class_section_name'])) {
|
if ($this->db->fieldExists('school_year', 'classSection')) {
|
||||||
$grade = $classSection['class_section_name'];
|
$classSectionBuilder->orderBy('classSection.school_year = ' . $this->db->escape($schoolYear), 'DESC', false);
|
||||||
|
}
|
||||||
|
$classSectionBuilder->orderBy('classSection.id', 'DESC');
|
||||||
|
$classSection = $classSectionBuilder->get()->getRowArray();
|
||||||
|
if ($classSection) {
|
||||||
|
$grade = $this->invoiceManagementGradeLabel($classSection, $student);
|
||||||
|
} elseif ($this->isKindergarten((string) ($student['registration_grade'] ?? ''))) {
|
||||||
|
$grade = 'KG';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,6 +778,24 @@ class InvoiceController extends ResourceController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function invoiceManagementGradeLabel(array $classSection, array $student): string
|
||||||
|
{
|
||||||
|
$registrationGrade = trim((string) ($student['registration_grade'] ?? ''));
|
||||||
|
$classId = (int) ($classSection['class_id'] ?? 0);
|
||||||
|
$className = trim((string) ($classSection['class_name'] ?? ''));
|
||||||
|
$sectionName = trim((string) ($classSection['class_section_name'] ?? ''));
|
||||||
|
|
||||||
|
if ($classId === 13 || $this->isKindergarten($className) || $this->isKindergarten($registrationGrade)) {
|
||||||
|
return 'KG';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sectionName === '13') {
|
||||||
|
return 'KG';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sectionName !== '' ? $sectionName : ($className !== '' ? $className : 'N/A');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<array<string, mixed>>
|
* @return list<array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2897,12 +2897,17 @@ class ParentController extends BaseController
|
|||||||
$lnKey = mb_strtolower(trim(preg_replace('/\s+/', ' ', $lastName)), 'UTF-8');
|
$lnKey = mb_strtolower(trim(preg_replace('/\s+/', ' ', $lastName)), 'UTF-8');
|
||||||
$dobStr = $dobObj->format('Y-m-d');
|
$dobStr = $dobObj->format('Y-m-d');
|
||||||
|
|
||||||
$existing = $this->studentModel
|
$existingBuilder = $this->studentModel
|
||||||
->where('school_year', $schoolYear)
|
->where('parent_id', (int) $parentId)
|
||||||
->where('dob', $dobStr)
|
->where('dob', $dobStr)
|
||||||
->where('firstname', $firstName) // already normalized above
|
->where('firstname', $firstName) // already normalized above
|
||||||
->where('lastname', $lastName) // already normalized above
|
->where('lastname', $lastName); // already normalized above
|
||||||
->first();
|
|
||||||
|
if ($this->db->fieldExists('school_year', 'students')) {
|
||||||
|
$existingBuilder->where('school_year', $schoolYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $existingBuilder->first();
|
||||||
|
|
||||||
if (!$studentId && $existing) {
|
if (!$studentId && $existing) {
|
||||||
session()->setFlashdata(
|
session()->setFlashdata(
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class StudentModel extends Model
|
|||||||
'tuition_paid',
|
'tuition_paid',
|
||||||
'year_of_registration',
|
'year_of_registration',
|
||||||
'rfid_tag',
|
'rfid_tag',
|
||||||
|
'semester',
|
||||||
|
'school_year',
|
||||||
'is_active'
|
'is_active'
|
||||||
];
|
];
|
||||||
protected $useTimestamps = false;
|
protected $useTimestamps = false;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Services;
|
|||||||
|
|
||||||
use App\Models\ConfigurationModel;
|
use App\Models\ConfigurationModel;
|
||||||
use App\Support\Enrollment\DeliberationDecision;
|
use App\Support\Enrollment\DeliberationDecision;
|
||||||
|
use App\Support\Enrollment\EnrollmentEligibility;
|
||||||
use CodeIgniter\Database\BaseConnection;
|
use CodeIgniter\Database\BaseConnection;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
|
|
||||||
@@ -132,6 +133,7 @@ final class EnrollmentRegistrationEmailService
|
|||||||
$studentSummaries[] = [
|
$studentSummaries[] = [
|
||||||
'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student #' . $studentId,
|
'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student #' . $studentId,
|
||||||
'adult_student' => ! empty($evaluation['adult_student']),
|
'adult_student' => ! empty($evaluation['adult_student']),
|
||||||
|
'issue_code' => $this->studentIssueCode($evaluation),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +295,13 @@ final class EnrollmentRegistrationEmailService
|
|||||||
|
|
||||||
private function decisionMessage(string $name, array $evaluation, string $opens, string $deadline, array $schoolYear = []): string
|
private function decisionMessage(string $name, array $evaluation, string $opens, string $deadline, array $schoolYear = []): string
|
||||||
{
|
{
|
||||||
|
if (! empty($evaluation['kg_missing_decision_eligible']) && ! $this->hasNonKgMissingDecisionBlocker($evaluation)) {
|
||||||
|
$placement = $this->placementText($evaluation);
|
||||||
|
$placementText = $placement !== '' ? ' The student placement for the new school year is: ' . $placement . '.' : '';
|
||||||
|
|
||||||
|
return 'KG students may complete registration now. ' . $name . ' is eligible for re-enrollment even though no final deliberation decision is recorded.' . $placementText;
|
||||||
|
}
|
||||||
|
|
||||||
if (($evaluation['can_enroll'] ?? false) === false && ! empty($evaluation['primary_parent_message'])) {
|
if (($evaluation['can_enroll'] ?? false) === false && ! empty($evaluation['primary_parent_message'])) {
|
||||||
return (string) $evaluation['primary_parent_message'];
|
return (string) $evaluation['primary_parent_message'];
|
||||||
}
|
}
|
||||||
@@ -518,6 +527,18 @@ final class EnrollmentRegistrationEmailService
|
|||||||
: 'Eligible';
|
: 'Eligible';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! empty($evaluation['kg_missing_decision_eligible']) && ! $this->hasNonKgMissingDecisionBlocker($evaluation)) {
|
||||||
|
return 'Eligible';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
strtoupper(trim((string) ($evaluation['primary_block_reason'] ?? ''))) === 'ALREADY_ENROLLED'
|
||||||
|
|| in_array('ALREADY_ENROLLED', array_map('strval', $evaluation['blocking_rule_codes'] ?? []), true)
|
||||||
|
|| strtoupper(trim((string) ($evaluation['decision'] ?? ''))) === 'ALREADY_ENROLLED'
|
||||||
|
) {
|
||||||
|
return EnrollmentEligibility::alreadyEnrolledTitle($evaluation['enrollment_status'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
if (! empty($evaluation['adult_student'])) {
|
if (! empty($evaluation['adult_student'])) {
|
||||||
return 'Not Eligible';
|
return 'Not Eligible';
|
||||||
}
|
}
|
||||||
@@ -529,6 +550,53 @@ final class EnrollmentRegistrationEmailService
|
|||||||
return 'Not Eligible';
|
return 'Not Eligible';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function hasNonKgMissingDecisionBlocker(array $evaluation): bool
|
||||||
|
{
|
||||||
|
$ignoredCodes = ['NO_FINAL_DECISION', 'UNRECOGNIZED_DECISION'];
|
||||||
|
$blockingCodes = array_values(array_filter(array_map(
|
||||||
|
static fn ($code): string => strtoupper(trim((string) $code)),
|
||||||
|
$evaluation['blocking_rule_codes'] ?? []
|
||||||
|
)));
|
||||||
|
$reviewCodes = array_values(array_filter(array_map(
|
||||||
|
static fn ($code): string => strtoupper(trim((string) $code)),
|
||||||
|
$evaluation['review_rule_codes'] ?? []
|
||||||
|
)));
|
||||||
|
$codes = array_values(array_unique(array_merge($blockingCodes, $reviewCodes)));
|
||||||
|
|
||||||
|
return array_values(array_diff($codes, $ignoredCodes)) !== [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function studentIssueCode(array $evaluation): string
|
||||||
|
{
|
||||||
|
if (! empty($evaluation['adult_student'])) {
|
||||||
|
return 'ADULT_STUDENT_PARENT_BLOCKED';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (($evaluation['flags'] ?? []) as $flag) {
|
||||||
|
if (! is_array($flag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$details = is_array($flag['details'] ?? null) ? $flag['details'] : [];
|
||||||
|
$ruleCode = strtoupper(trim((string) ($details['rule_code'] ?? '')));
|
||||||
|
if ($ruleCode !== '') {
|
||||||
|
return $ruleCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$flagType = strtoupper(trim((string) ($flag['flag_type'] ?? '')));
|
||||||
|
if ($flagType !== '') {
|
||||||
|
return $flagType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$primaryBlockReason = strtoupper(trim((string) ($evaluation['primary_block_reason'] ?? '')));
|
||||||
|
if ($primaryBlockReason !== '') {
|
||||||
|
return $primaryBlockReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
private function placementText(array $evaluation): string
|
private function placementText(array $evaluation): string
|
||||||
{
|
{
|
||||||
$placement = match ((string) ($evaluation['placement_status'] ?? '')) {
|
$placement = match ((string) ($evaluation['placement_status'] ?? '')) {
|
||||||
@@ -623,6 +691,10 @@ final class EnrollmentRegistrationEmailService
|
|||||||
return $fallback;
|
return $fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (in_array(strtoupper($grade), ['KG', 'K', 'KINDERGARTEN'], true)) {
|
||||||
|
return 'KG';
|
||||||
|
}
|
||||||
|
|
||||||
return preg_match('/^grade\b/i', $grade) === 1 ? $grade : 'Grade ' . $grade;
|
return preg_match('/^grade\b/i', $grade) === 1 ? $grade : 'Grade ' . $grade;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ final class EnrollmentTransitionService
|
|||||||
$this->addBlocker($evaluation, 'TARGET_YEAR_NOT_FOUND', 'Target school year configuration was not found.');
|
$this->addBlocker($evaluation, 'TARGET_YEAR_NOT_FOUND', 'Target school year configuration was not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sourceSchoolYear === '' || $this->sourceAssignment($studentId, $sourceSchoolYear) === null) {
|
if (
|
||||||
|
empty($evaluation['first_enrollment'])
|
||||||
|
&& ($sourceSchoolYear === '' || $this->sourceAssignment($studentId, $sourceSchoolYear) === null)
|
||||||
|
) {
|
||||||
$this->addBlocker($evaluation, 'SOURCE_YEAR_NOT_FOUND', 'Student does not belong to the closing school year.');
|
$this->addBlocker($evaluation, 'SOURCE_YEAR_NOT_FOUND', 'Student does not belong to the closing school year.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ final class EnrollmentTransitionService
|
|||||||
$this->addBlocker($evaluation, $code, 'Student has a target-year enrollment status that requires administration review.');
|
$this->addBlocker($evaluation, $code, 'Student has a target-year enrollment status that requires administration review.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->applyHouseholdLastNameRule($evaluation, $parentId);
|
$this->applyHouseholdLastNameRule($evaluation, $parentId, $sourceSchoolYear);
|
||||||
$this->applyFinancialRule($evaluation, $parentId, $sourceSchoolYear, $targetSchoolYear);
|
$this->applyFinancialRule($evaluation, $parentId, $sourceSchoolYear, $targetSchoolYear);
|
||||||
$this->applyCarriedForwardLastNameException($evaluation, $parentId, $studentId, $sourceSchoolYear, $targetSchoolYear);
|
$this->applyCarriedForwardLastNameException($evaluation, $parentId, $studentId, $sourceSchoolYear, $targetSchoolYear);
|
||||||
$this->deriveAcademicRuleCodes($evaluation);
|
$this->deriveAcademicRuleCodes($evaluation);
|
||||||
@@ -250,6 +253,7 @@ final class EnrollmentTransitionService
|
|||||||
$decisionRow = $this->decisionRow($studentId, $sourceSchoolYear);
|
$decisionRow = $this->decisionRow($studentId, $sourceSchoolYear);
|
||||||
$decision = DeliberationDecision::normalize($decisionRow['deliberation_decision_standard'] ?? null)
|
$decision = DeliberationDecision::normalize($decisionRow['deliberation_decision_standard'] ?? null)
|
||||||
?? DeliberationDecision::normalize($decisionRow['decision'] ?? null);
|
?? DeliberationDecision::normalize($decisionRow['decision'] ?? null);
|
||||||
|
$rawDecision = trim((string) ($decisionRow['deliberation_decision_standard'] ?? $decisionRow['decision'] ?? ''));
|
||||||
|
|
||||||
$result = [
|
$result = [
|
||||||
'student_id' => $studentId,
|
'student_id' => $studentId,
|
||||||
@@ -312,6 +316,24 @@ final class EnrollmentTransitionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($sourceAssignment === null) {
|
if ($sourceAssignment === null) {
|
||||||
|
if ($this->isFirstEnrollmentStudent($studentId, $student, $targetSchoolYear)) {
|
||||||
|
$result = array_replace($result, $this->firstEnrollmentPlacement($student, $targetSchoolYear));
|
||||||
|
$result['first_enrollment'] = true;
|
||||||
|
$result['decision_label'] = 'New student registration';
|
||||||
|
$result['academic_eligible'] = true;
|
||||||
|
$result['parent_enrollment_allowed'] = true;
|
||||||
|
$result['student_self_enrollment_allowed'] = false;
|
||||||
|
|
||||||
|
$this->applyRegistrationWindow($result, $targetYear, $now, $actorRole);
|
||||||
|
$this->applyAgeRules($result, $targetSchoolYear, $age);
|
||||||
|
|
||||||
|
if ($result['blockers'] !== [] && $actorRole === 'parent') {
|
||||||
|
$result['parent_enrollment_allowed'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
$result['blockers'][] = 'Student does not belong to the closing school year.';
|
$result['blockers'][] = 'Student does not belong to the closing school year.';
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
@@ -322,7 +344,18 @@ final class EnrollmentTransitionService
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($decisionRow === null || $decision === null) {
|
if (($decisionRow === null || $decision === null) && $sourceAssignment !== null) {
|
||||||
|
$kgDecision = $this->decisionForKgWithoutFinalDecision($sourceAssignment, $age, $rawDecision);
|
||||||
|
if ($kgDecision !== null) {
|
||||||
|
$decision = $kgDecision;
|
||||||
|
$result['deliberation_decision'] = $decision;
|
||||||
|
$result['decision_label'] = 'KG age placement';
|
||||||
|
$result['kg_missing_decision_eligible'] = true;
|
||||||
|
$result['warnings'][] = EnrollmentEligibility::KG_MISSING_DECISION_ELIGIBLE_MESSAGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($decision === null) {
|
||||||
$result['blockers'][] = EnrollmentEligibility::MISSING_DECISION_MESSAGE;
|
$result['blockers'][] = EnrollmentEligibility::MISSING_DECISION_MESSAGE;
|
||||||
$result['flags'][] = $this->flag('DEFERRED_DELIBERATION', 'high', [
|
$result['flags'][] = $this->flag('DEFERRED_DELIBERATION', 'high', [
|
||||||
'rule_code' => $decisionRow === null ? 'NO_FINAL_DECISION' : 'UNRECOGNIZED_DECISION',
|
'rule_code' => $decisionRow === null ? 'NO_FINAL_DECISION' : 'UNRECOGNIZED_DECISION',
|
||||||
@@ -472,20 +505,12 @@ final class EnrollmentTransitionService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$flags = [];
|
|
||||||
if ($targetSection === null) {
|
|
||||||
$flags[] = $this->flag('CLASS_REASSIGNMENT_REQUIRED', 'normal', [
|
|
||||||
'previous_class_section_name' => $sourceSectionName,
|
|
||||||
'previous_class_section_id' => $sourceSectionId > 0 ? $sourceSectionId : null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'assigned_grade_id' => $targetSection['class_id'] ?? $this->sameClassInTargetYear($sourceClassName, $targetSchoolYear)['id'] ?? ($sourceClassId ?: null),
|
'assigned_grade_id' => $targetSection['class_id'] ?? $this->sameClassInTargetYear($sourceClassName, $targetSchoolYear)['id'] ?? ($sourceClassId ?: null),
|
||||||
'assigned_grade_name' => $this->classNameForId((int) ($targetSection['class_id'] ?? 0)) ?? $sourceClassName,
|
'assigned_grade_name' => $this->classNameForId((int) ($targetSection['class_id'] ?? 0)) ?? $sourceClassName,
|
||||||
'assigned_class_section_id' => $targetSection['class_section_id'] ?? null,
|
'assigned_class_section_id' => $targetSection['class_section_id'] ?? null,
|
||||||
'placement_status' => $targetSection === null ? 'manual_class_required' : 'same_class_assigned',
|
'placement_status' => $targetSection === null ? 'manual_class_required' : 'same_class_assigned',
|
||||||
'flags' => $flags,
|
'flags' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,6 +534,17 @@ final class EnrollmentTransitionService
|
|||||||
return ['assigned_grade_id' => null, 'assigned_class_section_id' => null, 'placement_status' => 'not_created', 'flags' => []];
|
return ['assigned_grade_id' => null, 'assigned_class_section_id' => null, 'placement_status' => 'not_created', 'flags' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function decisionForKgWithoutFinalDecision(array $sourceAssignment, ?int $age, string $rawDecision): ?string
|
||||||
|
{
|
||||||
|
if ($rawDecision !== '' || $this->classBaseName((string) ($sourceAssignment['class_name'] ?? $sourceAssignment['class_section_name'] ?? '')) !== 'KG') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $age !== null && $age >= 6
|
||||||
|
? DeliberationDecision::PASSED
|
||||||
|
: DeliberationDecision::REPEAT_CLASS;
|
||||||
|
}
|
||||||
|
|
||||||
private function applyRegistrationWindow(array &$result, ?array $targetYear, DateTimeImmutable $now, string $actorRole): void
|
private function applyRegistrationWindow(array &$result, ?array $targetYear, DateTimeImmutable $now, string $actorRole): void
|
||||||
{
|
{
|
||||||
if ($targetYear === null) {
|
if ($targetYear === null) {
|
||||||
@@ -638,14 +674,42 @@ final class EnrollmentTransitionService
|
|||||||
|
|
||||||
private function student(int $studentId): ?array
|
private function student(int $studentId): ?array
|
||||||
{
|
{
|
||||||
|
$select = ['id', 'firstname', 'lastname', 'dob', 'parent_id', 'registration_grade'];
|
||||||
|
if ($this->db->fieldExists('school_year', 'students')) {
|
||||||
|
$select[] = 'school_year';
|
||||||
|
}
|
||||||
|
|
||||||
return $this->db->table('students')
|
return $this->db->table('students')
|
||||||
->select('id, firstname, lastname, dob, parent_id')
|
->select(implode(', ', $select))
|
||||||
->where('id', $studentId)
|
->where('id', $studentId)
|
||||||
->limit(1)
|
->limit(1)
|
||||||
->get()
|
->get()
|
||||||
->getRowArray() ?: null;
|
->getRowArray() ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isFirstEnrollmentStudent(int $studentId, array $student, string $targetSchoolYear): bool
|
||||||
|
{
|
||||||
|
if ($studentId <= 0 || $targetSchoolYear === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->db->tableExists('student_year_status')) {
|
||||||
|
$row = $this->db->table('student_year_status')
|
||||||
|
->select('is_new')
|
||||||
|
->where('student_id', $studentId)
|
||||||
|
->where('school_year', $targetSchoolYear)
|
||||||
|
->limit(1)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if (is_array($row)) {
|
||||||
|
return (int) ($row['is_new'] ?? 1) === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string) ($student['school_year'] ?? '')) === $targetSchoolYear;
|
||||||
|
}
|
||||||
|
|
||||||
private function schoolYearByName(string $schoolYear): ?array
|
private function schoolYearByName(string $schoolYear): ?array
|
||||||
{
|
{
|
||||||
if (! $this->db->tableExists('school_years')) {
|
if (! $this->db->tableExists('school_years')) {
|
||||||
@@ -730,6 +794,42 @@ final class EnrollmentTransitionService
|
|||||||
->getRowArray() ?: null;
|
->getRowArray() ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function firstEnrollmentPlacement(array $student, string $targetSchoolYear): array
|
||||||
|
{
|
||||||
|
$grade = $this->classBaseName((string) ($student['registration_grade'] ?? ''));
|
||||||
|
$targetClass = $grade !== '' ? $this->classByName($grade, $targetSchoolYear) : null;
|
||||||
|
$targetSection = is_array($targetClass)
|
||||||
|
? $this->baseSectionForClass((int) ($targetClass['id'] ?? 0), $targetSchoolYear)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'assigned_grade_id' => $targetClass['id'] ?? null,
|
||||||
|
'assigned_grade_name' => $targetClass['class_name'] ?? ($grade !== '' ? $grade : null),
|
||||||
|
'assigned_class_section_id' => $targetSection['class_section_id'] ?? null,
|
||||||
|
'placement_status' => $targetSection === null ? 'manual_class_required' : 'same_class_assigned',
|
||||||
|
'flags' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function baseSectionForClass(int $classId, string $targetSchoolYear): ?array
|
||||||
|
{
|
||||||
|
if ($classId <= 0 || ! $this->db->tableExists('classSection')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->db->table('classSection')
|
||||||
|
->select('class_section_id, class_id, class_section_name')
|
||||||
|
->where('class_id', $classId)
|
||||||
|
->where("class_section_name NOT LIKE '%-%'", null, false)
|
||||||
|
->orderBy('id', 'ASC');
|
||||||
|
|
||||||
|
if ($this->db->fieldExists('school_year', 'classSection')) {
|
||||||
|
$builder->where('school_year', $targetSchoolYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder->limit(1)->get()->getRowArray() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
private function classNameForId(int $classId): ?string
|
private function classNameForId(int $classId): ?string
|
||||||
{
|
{
|
||||||
if ($classId <= 0 || ! $this->db->tableExists('classes')) {
|
if ($classId <= 0 || ! $this->db->tableExists('classes')) {
|
||||||
@@ -1033,6 +1133,7 @@ final class EnrollmentTransitionService
|
|||||||
$written += $this->syncSiblingLastNameFlags($students, $enrolledIds, $bypassCodesByStudent, $targetSchoolYear, $sourceSchoolYear, $performedBy);
|
$written += $this->syncSiblingLastNameFlags($students, $enrolledIds, $bypassCodesByStudent, $targetSchoolYear, $sourceSchoolYear, $performedBy);
|
||||||
$written += $this->syncFinancialFlags($students, $enrolledIds, $bypassCodesByStudent, $targetSchoolYear, $sourceSchoolYear, $performedBy);
|
$written += $this->syncFinancialFlags($students, $enrolledIds, $bypassCodesByStudent, $targetSchoolYear, $sourceSchoolYear, $performedBy);
|
||||||
$written += $this->syncRepeatClassReassignmentFlags($targetSchoolYear, $sourceSchoolYear, $performedBy);
|
$written += $this->syncRepeatClassReassignmentFlags($targetSchoolYear, $sourceSchoolYear, $performedBy);
|
||||||
|
$this->retireClassReassignmentFlags($targetSchoolYear);
|
||||||
$this->retireClassCapacityExceptionFlags($targetSchoolYear);
|
$this->retireClassCapacityExceptionFlags($targetSchoolYear);
|
||||||
|
|
||||||
return $written;
|
return $written;
|
||||||
@@ -1063,7 +1164,7 @@ final class EnrollmentTransitionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$flagType = (string) ($flag['flag_type'] ?? '');
|
$flagType = (string) ($flag['flag_type'] ?? '');
|
||||||
if ($flagType === '' || $flagType === 'CLASS_CAPACITY_EXCEPTION_REQUIRED') {
|
if ($flagType === '' || in_array($flagType, ['CLASS_CAPACITY_EXCEPTION_REQUIRED', 'CLASS_REASSIGNMENT_REQUIRED'], true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,6 +1214,26 @@ final class EnrollmentTransitionService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function retireClassReassignmentFlags(string $targetSchoolYear): void
|
||||||
|
{
|
||||||
|
if (! $this->db->tableExists('enrollment_flags')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->db->table('enrollment_flags')
|
||||||
|
->where('flag_type', 'CLASS_REASSIGNMENT_REQUIRED')
|
||||||
|
->where('status', 'open');
|
||||||
|
if ($targetSchoolYear !== '') {
|
||||||
|
$builder->where('school_year', $targetSchoolYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder->update([
|
||||||
|
'status' => 'resolved',
|
||||||
|
'resolved_at' => date('Y-m-d H:i:s'),
|
||||||
|
'resolution_notes' => 'Class reassignment flags are no longer used for registration emails.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function retireClassCapacityExceptionFlags(string $targetSchoolYear): void
|
private function retireClassCapacityExceptionFlags(string $targetSchoolYear): void
|
||||||
{
|
{
|
||||||
if (! $this->db->tableExists('enrollment_flags')) {
|
if (! $this->db->tableExists('enrollment_flags')) {
|
||||||
@@ -1886,9 +2007,22 @@ final class EnrollmentTransitionService
|
|||||||
|| EnrollmentEligibility::isMarkedWithdrawn($enrollment);
|
|| EnrollmentEligibility::isMarkedWithdrawn($enrollment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyHouseholdLastNameRule(array &$evaluation, int $parentId): void
|
private function applyHouseholdLastNameRule(array &$evaluation, int $parentId, ?string $sourceSchoolYear = null): void
|
||||||
{
|
{
|
||||||
|
if (! empty($evaluation['first_enrollment'])) {
|
||||||
|
$evaluation['family_name_check_ok'] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$students = $this->linkedStudentsForParent($parentId);
|
$students = $this->linkedStudentsForParent($parentId);
|
||||||
|
$sourceSchoolYear = trim((string) $sourceSchoolYear);
|
||||||
|
if ($sourceSchoolYear !== '') {
|
||||||
|
$students = array_values(array_filter(
|
||||||
|
$students,
|
||||||
|
fn (array $student): bool => $this->sourceAssignment((int) ($student['id'] ?? 0), $sourceSchoolYear) !== null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if (count($students) <= 1) {
|
if (count($students) <= 1) {
|
||||||
$evaluation['family_name_check_ok'] = true;
|
$evaluation['family_name_check_ok'] = true;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 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 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 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 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_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 ADULT_STUDENT_PARENT_PORTAL_MESSAGE = self::ADULT_STUDENT_MESSAGE;
|
||||||
|
|||||||
@@ -49,17 +49,22 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.enrollment-admin .issue-badge.adult-student,
|
.enrollment-admin .issue-badge.adult-student,
|
||||||
|
.enrollment-admin .email-student-name.adult-student,
|
||||||
.enrollment-admin .adult-student-name {
|
.enrollment-admin .adult-student-name {
|
||||||
background-color: #b02a37;
|
background-color: #b02a37;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.enrollment-admin .adult-student-name {
|
.enrollment-admin .adult-student-name,
|
||||||
|
.enrollment-admin .email-student-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: .15rem .35rem;
|
padding: .15rem .35rem;
|
||||||
margin: .1rem .15rem .1rem 0;
|
margin: .1rem .15rem .1rem 0;
|
||||||
}
|
}
|
||||||
|
.enrollment-admin .email-student-name.bg-dark {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
@@ -766,7 +771,10 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
|
|||||||
<?php if (!empty($example['students']) && is_array($example['students'])): ?>
|
<?php if (!empty($example['students']) && is_array($example['students'])): ?>
|
||||||
<?php foreach ($example['students'] as $studentIndex => $student): ?>
|
<?php foreach ($example['students'] as $studentIndex => $student): ?>
|
||||||
<?= $studentIndex > 0 ? ', ' : '' ?>
|
<?= $studentIndex > 0 ? ', ' : '' ?>
|
||||||
<?php if (!empty($student['adult_student'])): ?>
|
<?php $studentIssueCode = strtoupper(trim((string) ($student['issue_code'] ?? ''))); ?>
|
||||||
|
<?php if ($studentIssueCode !== ''): ?>
|
||||||
|
<span class="email-student-name <?= esc(enrollment_admin_issue_badge_class($studentIssueCode)) ?>" title="<?= esc(enrollment_admin_flag_label($studentIssueCode)) ?>"><?= esc($student['name'] ?? '') ?></span>
|
||||||
|
<?php elseif (!empty($student['adult_student'])): ?>
|
||||||
<span class="adult-student-name"><?= esc($student['name'] ?? '') ?></span>
|
<span class="adult-student-name"><?= esc($student['name'] ?? '') ?></span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span><?= esc($student['name'] ?? '') ?></span>
|
<span><?= esc($student['name'] ?? '') ?></span>
|
||||||
|
|||||||
@@ -124,8 +124,15 @@ $selectedYear = trim((string)($selectedYear ?? ''));
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input-safe values
|
// Browser date inputs require ISO dates even when the table displays local mm-dd-YYYY.
|
||||||
$dobVal = $dobDisp;
|
$dobVal = '';
|
||||||
|
if (!empty($student['dob'])) {
|
||||||
|
try {
|
||||||
|
$dobVal = (new DateTime($student['dob']))->format('Y-m-d');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$dobVal = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
$regDateLocal = '';
|
$regDateLocal = '';
|
||||||
if (!empty($student['registration_date'])) {
|
if (!empty($student['registration_date'])) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -85,6 +85,51 @@ final class EnrollmentRegistrationEmailServiceTest extends TestCase
|
|||||||
$this->assertSame('', $action);
|
$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
|
public function testMakeupExamDecisionMessageUsesSchoolYearDateBeforeConfig(): void
|
||||||
{
|
{
|
||||||
$configModel = $this->createMock(ConfigurationModel::class);
|
$configModel = $this->createMock(ConfigurationModel::class);
|
||||||
@@ -231,6 +276,33 @@ final class EnrollmentRegistrationEmailServiceTest extends TestCase
|
|||||||
$this->assertStringContainsString('parent/enroll_classes', $html);
|
$this->assertStringContainsString('parent/enroll_classes', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testStudentIssueCodeUsesFlagRuleCode(): void
|
||||||
|
{
|
||||||
|
$service = $this->service();
|
||||||
|
|
||||||
|
$code = $this->invoke($service, 'studentIssueCode', [[
|
||||||
|
'flags' => [[
|
||||||
|
'flag_type' => 'DEFERRED_DELIBERATION',
|
||||||
|
'details' => ['rule_code' => 'NO_FINAL_DECISION'],
|
||||||
|
]],
|
||||||
|
]]);
|
||||||
|
|
||||||
|
$this->assertSame('NO_FINAL_DECISION', $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStudentIssueCodeDoesNotColorNormalDecision(): void
|
||||||
|
{
|
||||||
|
$service = $this->service();
|
||||||
|
|
||||||
|
$code = $this->invoke($service, 'studentIssueCode', [[
|
||||||
|
'can_enroll' => true,
|
||||||
|
'deliberation_decision' => DeliberationDecision::PASSED,
|
||||||
|
'flags' => [],
|
||||||
|
]]);
|
||||||
|
|
||||||
|
$this->assertSame('', $code);
|
||||||
|
}
|
||||||
|
|
||||||
public function testParentReEnrollmentEligibilityUsesTransitionAllowedFlag(): void
|
public function testParentReEnrollmentEligibilityUsesTransitionAllowedFlag(): void
|
||||||
{
|
{
|
||||||
$service = $this->service();
|
$service = $this->service();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Tests\App\Services;
|
namespace Tests\App\Services;
|
||||||
|
|
||||||
use App\Services\EnrollmentTransitionService;
|
use App\Services\EnrollmentTransitionService;
|
||||||
|
use App\Support\Enrollment\DeliberationDecision;
|
||||||
use CodeIgniter\Database\BaseBuilder;
|
use CodeIgniter\Database\BaseBuilder;
|
||||||
use CodeIgniter\Database\BaseConnection;
|
use CodeIgniter\Database\BaseConnection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -27,6 +28,71 @@ final class EnrollmentTransitionServiceTest extends TestCase
|
|||||||
$this->assertSame(10, (int) $result['id']);
|
$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
|
public function testClassLookupFallsBackToGlobalRowsWhenSchoolYearSpecificRowIsMissing(): void
|
||||||
{
|
{
|
||||||
$firstBuilder = $this->builderReturning(null);
|
$firstBuilder = $this->builderReturning(null);
|
||||||
@@ -145,6 +211,66 @@ final class EnrollmentTransitionServiceTest extends TestCase
|
|||||||
$this->assertContains('SIBLING_LAST_NAME_MISMATCH', $evaluation['blocking_rule_codes']);
|
$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
|
public function testPreviousYearBalanceBlocksEnrollment(): void
|
||||||
{
|
{
|
||||||
$service = $this->serviceWithFinance(125.50, 'submission_blocked_until_payment');
|
$service = $this->serviceWithFinance(125.50, 'submission_blocked_until_payment');
|
||||||
@@ -343,6 +469,29 @@ final class EnrollmentTransitionServiceTest extends TestCase
|
|||||||
$this->assertArrayNotHasKey('parent_enrollment_allowed', $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
|
public function testPreviousLastNameExceptionDoesNotCarryForwardWhenNewStudentIsAdded(): void
|
||||||
{
|
{
|
||||||
$service = $this->serviceWithLastNameCarryForward(
|
$service = $this->serviceWithLastNameCarryForward(
|
||||||
|
|||||||
Reference in New Issue
Block a user