fix parent page
Tests / PHPUnit (push) Failing after 1m10s

This commit is contained in:
root
2026-07-15 21:42:06 -04:00
parent 9de2ab2a9f
commit 7f3b24e47f
5 changed files with 254 additions and 4 deletions
+135 -3
View File
@@ -305,6 +305,7 @@ class ParentController extends BaseController
// Attach enrollment and class section data to each student
foreach ($students as &$student) {
$studentId = $student['id'];
$student['age'] = $this->calculateAgeAsOfSchoolYearStartYear($student['dob'] ?? null, $selectedYear);
// Get class section info (can be multiple sections like Grade + Arabic)
$classSections = $this->studentClassModel->getClassSectionsByStudentId($studentId, $selectedYear, true);
@@ -428,11 +429,14 @@ class ParentController extends BaseController
}
if ($existingEnrollment) {
if ($existingEnrollment['is_withdrawn'] == 1) {
$passedPreviousYear = $this->studentPassedPreviousYear((int)$studentId, (string)$this->schoolYear);
// Reactivate the enrollment if the student was previously withdrawn
$this->enrollmentModel->where('id', $existingEnrollment['id'])->update([
'is_withdrawn' => 0,
'withdrawal_date' => null,
'enrollment_status' => 'payment pending',
'enrollment_status' => $passedPreviousYear ? 'payment pending' : 'admission under review',
'admission_status' => $passedPreviousYear ? 'accepted' : 'pending',
'updated_at' => utc_now()
]);
log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) has been re-enrolled in enrollment ID {$existingEnrollment['id']}.");
@@ -442,6 +446,8 @@ class ParentController extends BaseController
log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) is already actively enrolled.");
}
} else {
$passedPreviousYear = $this->studentPassedPreviousYear((int)$studentId, (string)$this->schoolYear);
// If no enrollment record exists, insert a new enrollment record
$result = $this->enrollmentModel->insert([
'student_id' => $studentId,
@@ -450,8 +456,8 @@ class ParentController extends BaseController
'semester' => $this->semester,
'enrollment_date' => local_date(utc_now(), 'Y-m-d'),
'is_withdrawn' => 0,
'enrollment_status' => 'admission under review',
'admission_status' => 'pending',
'enrollment_status' => $passedPreviousYear ? 'payment pending' : 'admission under review',
'admission_status' => $passedPreviousYear ? 'accepted' : 'pending',
'created_at' => utc_now()
]);
@@ -625,6 +631,61 @@ class ParentController extends BaseController
}
}
private function studentPassedPreviousYear(int $studentId, string $targetSchoolYear): bool
{
$previousSchoolYear = $this->previousSchoolYearName($targetSchoolYear);
if ($studentId <= 0 || $previousSchoolYear === null) {
return false;
}
try {
if ($this->db->tableExists('promotion_queue')) {
$queuedPromotion = $this->db->table('promotion_queue')
->select('id')
->where('student_id', $studentId)
->where('school_year_from', $previousSchoolYear)
->where('school_year_to', $targetSchoolYear)
->whereIn('status', ['queued', 'assigned', 'applied'])
->limit(1)
->get()
->getRowArray();
if ($queuedPromotion !== null) {
return true;
}
}
if ($this->db->tableExists('student_decisions')) {
$passedDecision = $this->db->table('student_decisions')
->select('id')
->where('student_id', $studentId)
->where('school_year', $previousSchoolYear)
->where('LOWER(TRIM(decision)) = ' . $this->db->escape('pass'), null, false)
->limit(1)
->get()
->getRowArray();
return $passedDecision !== null;
}
} catch (\Throwable $e) {
log_message('error', 'studentPassedPreviousYear failed: ' . $e->getMessage());
}
return false;
}
private function previousSchoolYearName(string $schoolYear): ?string
{
$schoolYear = trim($schoolYear);
if (! preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $matches)) {
return null;
}
return ((int) $matches[1] - 1) . '-' . ((int) $matches[2] - 1);
}
public function enrollFailure()
{
echo view('/parent/enroll_failure');
@@ -1754,6 +1815,77 @@ $existing = $this->studentModel
}
}
private function calculateAgeAsOfToday(?string $dob): ?int
{
$dob = trim((string) $dob);
if ($dob === '') {
return null;
}
try {
$timezone = new \DateTimeZone((string) (config('School')->attendance['timezone'] ?? user_timezone()));
$birthDate = \DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $timezone);
$errors = \DateTimeImmutable::getLastErrors();
$hasParseErrors = is_array($errors)
&& (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0);
if ($birthDate === false || $hasParseErrors) {
return null;
}
$today = new \DateTimeImmutable('today', $timezone);
if ($birthDate > $today) {
return null;
}
return $birthDate->diff($today)->y;
} catch (\Throwable $e) {
log_message('warning', 'Unable to calculate current age from DOB: {message}', [
'message' => $e->getMessage(),
]);
return null;
}
}
private function calculateAgeAsOfSchoolYearStartYear(?string $dob, string $schoolYear): ?int
{
$dob = trim((string) $dob);
$schoolYear = trim($schoolYear);
if ($dob === '' || ! preg_match('/^(\d{4})/', $schoolYear, $matches)) {
return null;
}
try {
$timezone = new \DateTimeZone((string) (config('School')->attendance['timezone'] ?? user_timezone()));
$birthDate = \DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $timezone);
$errors = \DateTimeImmutable::getLastErrors();
$hasParseErrors = is_array($errors)
&& (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0);
if ($birthDate === false || $hasParseErrors) {
return null;
}
$schoolYearStartYearCutoff = new \DateTimeImmutable($matches[1] . '-12-31', $timezone);
if ($birthDate > $schoolYearStartYearCutoff) {
return null;
}
return $birthDate->diff($schoolYearStartYearCutoff)->y;
} catch (\Throwable $e) {
log_message('warning', 'Unable to calculate school-year age from DOB: {message}', [
'message' => $e->getMessage(),
]);
return null;
}
}
public function parentEventPage()
{
$schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
@@ -0,0 +1,43 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateStudentSectionDistributionDrafts extends Migration
{
public function up()
{
if ($this->db->tableExists('student_section_distribution_drafts')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'student_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'class_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'class_section_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'school_year' => ['type' => 'VARCHAR', 'constraint' => 20],
'previous_school_year' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true],
'previous_final_score' => ['type' => 'DECIMAL', 'constraint' => '6,2', 'null' => true],
'score_group' => ['type' => 'VARCHAR', 'constraint' => 20],
'status' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'pending'],
'batch_key' => ['type' => 'VARCHAR', 'constraint' => 64],
'created_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
'applied_at' => ['type' => 'DATETIME', 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['student_id', 'school_year'], 'unique_distribution_draft_student_year');
$this->forge->addKey(['class_id', 'school_year', 'status'], false, 'distribution_draft_class_year_status');
$this->forge->addKey(['class_section_id', 'school_year'], false, 'distribution_draft_section_year');
$this->forge->createTable('student_section_distribution_drafts');
}
public function down()
{
$this->forge->dropTable('student_section_distribution_drafts', true);
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class StudentSectionDistributionDraftModel extends Model
{
protected $table = 'student_section_distribution_drafts';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useAutoIncrement = true;
protected $protectFields = true;
protected $allowedFields = [
'student_id',
'class_id',
'class_section_id',
'school_year',
'previous_school_year',
'previous_final_score',
'score_group',
'status',
'batch_key',
'created_by',
'applied_at',
'created_at',
'updated_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
}
-1
View File
@@ -27,7 +27,6 @@ class UserModel extends Model
'school_id',
'failed_attempts',
'last_failed_at',
'semester',
'status',
'is_suspended',
'is_verified',
@@ -0,0 +1,42 @@
<?php
namespace Tests\App\Controllers\View;
use App\Controllers\View\ParentController;
use CodeIgniter\Test\CIUnitTestCase;
use ReflectionMethod;
final class ParentControllerAgeTest extends CIUnitTestCase
{
public function testEnrollmentAgeUsesSchoolYearStartYearCutoff(): void
{
$controller = new class extends ParentController {
public function __construct()
{
}
};
$method = new ReflectionMethod(ParentController::class, 'calculateAgeAsOfSchoolYearStartYear');
$method->setAccessible(true);
$this->assertSame(6, $method->invoke($controller, '2019-09-15', '2025-2026'));
$this->assertSame(5, $method->invoke($controller, '2020-01-01', '2025-2026'));
}
public function testEnrollmentAgeRejectsInvalidInputs(): void
{
$controller = new class extends ParentController {
public function __construct()
{
}
};
$method = new ReflectionMethod(ParentController::class, 'calculateAgeAsOfSchoolYearStartYear');
$method->setAccessible(true);
$this->assertNull($method->invoke($controller, '', '2025-2026'));
$this->assertNull($method->invoke($controller, 'not-a-date', '2025-2026'));
$this->assertNull($method->invoke($controller, '2026-01-01', '2025-2026'));
$this->assertNull($method->invoke($controller, '2019-09-15', ''));
}
}