fix closing year
Tests / PHPUnit (push) Successful in 1m20s

This commit is contained in:
root
2026-08-10 01:11:16 -04:00
parent 1ef2800f12
commit c12bb59372
5 changed files with 416 additions and 126 deletions
+126 -5
View File
@@ -595,9 +595,10 @@ final class SchoolYearClosingService
$builder = $this->db->table('student_class sc')
->select('sc.student_id, sc.class_section_id, sc.created_at, sc.updated_at')
->select('s.school_id, s.firstname, s.lastname')
->select('cs.class_section_name')
->select('cs.class_section_name, cs.class_id, c.class_name')
->join('students s', 's.id = sc.student_id', 'inner')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
->join('classes c', 'c.id = cs.class_id', 'left')
->where('sc.school_year', $schoolYear)
->where('sc.class_section_id IS NOT NULL', null, false);
@@ -639,6 +640,8 @@ final class SchoolYearClosingService
'school_id' => (string) ($row['school_id'] ?? ''),
'student_name' => trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')),
'class_section_id' => (int) ($row['class_section_id'] ?? 0),
'class_id' => (int) ($row['class_id'] ?? 0),
'class_name' => (string) ($row['class_name'] ?? ''),
'class_section_name' => (string) ($row['class_section_name'] ?? ''),
'dob' => (string) ($row['dob'] ?? ''),
'registration_grade' => (string) ($row['registration_grade'] ?? ''),
@@ -703,6 +706,14 @@ final class SchoolYearClosingService
$student['target_section'] = $queue['target_section'];
}
if ($this->isKgStudent($student)) {
$kgPlacement = $this->kgPlacementLabelByTargetYearStartCutoff((string) ($student['dob'] ?? ''), $targetSchoolYear);
if ($kgPlacement !== '') {
$student['target_section'] = '';
$student['target_class'] = $kgPlacement;
}
}
$summary['total_students']++;
if ($student['status'] === 'missing') {
$summary['missing_decision']++;
@@ -712,10 +723,22 @@ final class SchoolYearClosingService
$summary['with_decision']++;
if (($student['normalized_decision'] ?? null) === DeliberationDecision::PASSED) {
$summary['pass']++;
if ($student['target_class'] === '') {
$student['target_class'] = $this->nextClassLabelForPromotionPreview(
(string) ($student['class_name'] ?: $student['class_section_name']),
$targetSchoolYear
);
}
if ($queue === null && $student['auto_kg_pass'] !== true) {
$summary['missing_queue']++;
}
} else {
if (($student['normalized_decision'] ?? null) === DeliberationDecision::REPEAT_CLASS) {
$student['target_section'] = '';
$student['target_class'] = $student['target_class'] !== ''
? $student['target_class']
: $this->classOnlyLabel((string) ($student['class_name'] ?: $student['class_section_name']));
}
$summary['other_decision']++;
}
}
@@ -740,9 +763,9 @@ final class SchoolYearClosingService
private function isKgStudent(array $student): bool
{
foreach (['class_section_name', 'registration_grade'] as $field) {
foreach (['class_name', 'class_section_name'] as $field) {
$value = strtoupper(trim((string) ($student[$field] ?? '')));
if ($value === 'KG' || str_starts_with($value, 'KG-') || str_contains($value, 'KINDERGARTEN')) {
if (preg_match('/(^|[^A-Z0-9])KG([^A-Z0-9]|$)/', $value) === 1 || str_contains($value, 'KINDERGARTEN')) {
return true;
}
}
@@ -753,13 +776,13 @@ final class SchoolYearClosingService
private function kgAgeStatusByTargetYearStartCutoff(string $dob, ?string $targetSchoolYear): string
{
$dob = trim($dob);
if ($dob === '' || $targetSchoolYear === null || ! preg_match('/^(\d{4})-\d{4}$/', $targetSchoolYear, $matches)) {
if ($dob === '') {
return '';
}
try {
$birthDate = new \DateTimeImmutable($dob);
$cutoff = new \DateTimeImmutable($matches[1] . '-09-01');
$cutoff = $this->septemberFirstCutoff($targetSchoolYear);
} catch (\Throwable) {
return '';
}
@@ -776,6 +799,27 @@ final class SchoolYearClosingService
return 'keep_kg';
}
private function kgPlacementLabelByTargetYearStartCutoff(string $dob, ?string $targetSchoolYear): string
{
return match ($this->kgAgeStatusByTargetYearStartCutoff($dob, $targetSchoolYear)) {
'pass' => '1',
'keep_kg' => 'KG',
default => '',
};
}
private function septemberFirstCutoff(?string $targetSchoolYear): \DateTimeImmutable
{
if ($targetSchoolYear !== null && preg_match('/^(\d{4})-\d{4}$/', $targetSchoolYear, $matches) === 1) {
return new \DateTimeImmutable($matches[1] . '-09-01');
}
$today = new \DateTimeImmutable('today');
$cutoff = new \DateTimeImmutable($today->format('Y') . '-09-01');
return $today <= $cutoff ? $cutoff : $cutoff->modify('+1 year');
}
private function promotionDecisionRows(array $studentIds, string $schoolYear): array
{
if (! $this->db->tableExists('student_decisions')) {
@@ -880,6 +924,83 @@ final class SchoolYearClosingService
return $queueRows;
}
private function nextClassLabelForPromotionPreview(string $sourceClassName, ?string $targetSchoolYear): string
{
$base = $this->classBaseName($sourceClassName);
$target = match (true) {
$base === 'KG' || str_contains($base, 'KINDERGARTEN') => '1',
ctype_digit($base) => (string) ((int) $base + 1),
$base === 'YOUTH' => 'YOUTH',
default => '',
};
if ($target === '') {
return '';
}
if ($targetSchoolYear !== null && $targetSchoolYear !== '' && $this->db->tableExists('classes')) {
$targetClass = $this->classByNameForPromotionPreview($target, $targetSchoolYear);
if ($targetClass !== null) {
return (string) ($targetClass['class_name'] ?? $target);
}
if (ctype_digit($base) && (int) $base >= 9) {
$youthClass = $this->classByNameForPromotionPreview('YOUTH', $targetSchoolYear);
if ($youthClass !== null) {
return (string) ($youthClass['class_name'] ?? 'YOUTH');
}
}
}
return $target;
}
private function classOnlyLabel(string $className): string
{
return trim((string) preg_replace('/-.+$/', '', $className));
}
private function classBaseName(string $className): string
{
$base = strtoupper(trim((string) preg_replace('/-.+$/', '', $className)));
$base = preg_replace('/\b(CLASS|GRADE)\b/i', '', $base) ?? $base;
$base = trim(preg_replace('/\s+/', ' ', $base) ?? $base);
if (str_contains($base, 'KINDERGARTEN')) {
return 'KG';
}
if (preg_match('/(^|[^A-Z0-9])KG([^A-Z0-9]|$)/', $base) === 1) {
return 'KG';
}
if (preg_match('/\d+/', $base, $matches) === 1) {
return (string) (int) $matches[0];
}
return $base;
}
private function classByNameForPromotionPreview(string $className, string $schoolYear): ?array
{
$builder = $this->db->table('classes')->where('UPPER(class_name)', strtoupper($className));
if ($this->db->fieldExists('school_year', 'classes')) {
$builder->where('school_year', $schoolYear);
}
$row = $builder->orderBy('id', 'DESC')->limit(1)->get()->getRowArray();
if ($row !== null || ! $this->db->fieldExists('school_year', 'classes')) {
return $row ?: null;
}
return $this->db->table('classes')
->where('UPPER(class_name)', strtoupper($className))
->orderBy('id', 'DESC')
->limit(1)
->get()
->getRowArray() ?: null;
}
private function countBySchoolYear(string $table, string $schoolYear, string $distinctField): int
{
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {