fix all issues before deploy
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Support\Enrollment\DeliberationDecision;
|
||||
use App\Support\Enrollment\EnrollmentEligibility;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use DateTimeInterface;
|
||||
|
||||
@@ -132,6 +133,7 @@ final class EnrollmentRegistrationEmailService
|
||||
$studentSummaries[] = [
|
||||
'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student #' . $studentId,
|
||||
'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
|
||||
{
|
||||
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'])) {
|
||||
return (string) $evaluation['primary_parent_message'];
|
||||
}
|
||||
@@ -518,6 +527,18 @@ final class EnrollmentRegistrationEmailService
|
||||
: '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'])) {
|
||||
return 'Not Eligible';
|
||||
}
|
||||
@@ -529,6 +550,53 @@ final class EnrollmentRegistrationEmailService
|
||||
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
|
||||
{
|
||||
$placement = match ((string) ($evaluation['placement_status'] ?? '')) {
|
||||
@@ -623,6 +691,10 @@ final class EnrollmentRegistrationEmailService
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
if (in_array(strtoupper($grade), ['KG', 'K', 'KINDERGARTEN'], true)) {
|
||||
return 'KG';
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
@@ -77,7 +80,7 @@ final class EnrollmentTransitionService
|
||||
$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->applyCarriedForwardLastNameException($evaluation, $parentId, $studentId, $sourceSchoolYear, $targetSchoolYear);
|
||||
$this->deriveAcademicRuleCodes($evaluation);
|
||||
@@ -250,6 +253,7 @@ final class EnrollmentTransitionService
|
||||
$decisionRow = $this->decisionRow($studentId, $sourceSchoolYear);
|
||||
$decision = DeliberationDecision::normalize($decisionRow['deliberation_decision_standard'] ?? null)
|
||||
?? DeliberationDecision::normalize($decisionRow['decision'] ?? null);
|
||||
$rawDecision = trim((string) ($decisionRow['deliberation_decision_standard'] ?? $decisionRow['decision'] ?? ''));
|
||||
|
||||
$result = [
|
||||
'student_id' => $studentId,
|
||||
@@ -312,6 +316,24 @@ final class EnrollmentTransitionService
|
||||
}
|
||||
|
||||
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.';
|
||||
return $result;
|
||||
}
|
||||
@@ -322,7 +344,18 @@ final class EnrollmentTransitionService
|
||||
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['flags'][] = $this->flag('DEFERRED_DELIBERATION', 'high', [
|
||||
'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 [
|
||||
'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_class_section_id' => $targetSection['class_section_id'] ?? null,
|
||||
'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' => []];
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
if ($targetYear === null) {
|
||||
@@ -638,14 +674,42 @@ final class EnrollmentTransitionService
|
||||
|
||||
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')
|
||||
->select('id, firstname, lastname, dob, parent_id')
|
||||
->select(implode(', ', $select))
|
||||
->where('id', $studentId)
|
||||
->limit(1)
|
||||
->get()
|
||||
->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
|
||||
{
|
||||
if (! $this->db->tableExists('school_years')) {
|
||||
@@ -730,6 +794,42 @@ final class EnrollmentTransitionService
|
||||
->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
|
||||
{
|
||||
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->syncFinancialFlags($students, $enrolledIds, $bypassCodesByStudent, $targetSchoolYear, $sourceSchoolYear, $performedBy);
|
||||
$written += $this->syncRepeatClassReassignmentFlags($targetSchoolYear, $sourceSchoolYear, $performedBy);
|
||||
$this->retireClassReassignmentFlags($targetSchoolYear);
|
||||
$this->retireClassCapacityExceptionFlags($targetSchoolYear);
|
||||
|
||||
return $written;
|
||||
@@ -1063,7 +1164,7 @@ final class EnrollmentTransitionService
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
if (! $this->db->tableExists('enrollment_flags')) {
|
||||
@@ -1886,9 +2007,22 @@ final class EnrollmentTransitionService
|
||||
|| 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);
|
||||
$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) {
|
||||
$evaluation['family_name_check_ok'] = true;
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user