fix enrollment and email send to parent
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 49s
Tests / PHPUnit (push) Failing after 1m20s

This commit is contained in:
root
2026-08-27 12:39:08 -04:00
parent 7b86cf5218
commit 0ae9993d82
19 changed files with 37831 additions and 1313 deletions
+155 -38
View File
@@ -14,6 +14,7 @@ use App\Models\StudentSectionDistributionDraftModel;
use App\Models\StudentAllergyModel;
use App\Models\StudentMedicalConditionModel;
use App\Support\Enrollment\DeliberationDecision;
use CodeIgniter\Events\Events;
use CodeIgniter\Database\Exceptions\DataException;
use Throwable;
@@ -129,11 +130,21 @@ class StudentController extends BaseController
$existing = $this->studentClassModel
->where('student_id', $studentId)
->where('school_year', (string)$this->schoolYear)
->orderBy('id', 'DESC')
->findAll();
$existingBySection = [];
$scPk = 'id';
foreach ($existing as $row) {
$existingBySection[(int)($row['class_section_id'] ?? 0)] = $row;
$rowEventFlag = (int)($row['is_event_only'] ?? 0);
if ($rowEventFlag !== $isEventOnly) {
continue;
}
$sectionKey = (int)($row['class_section_id'] ?? 0);
if ($sectionKey > 0 && !isset($existingBySection[$sectionKey])) {
$existingBySection[$sectionKey] = $row;
}
}
$displayNames = [];
@@ -184,8 +195,6 @@ class StudentController extends BaseController
try {
// Upsert student_class entries per selection
$scPk = $this->studentClassModel->primaryKey ?? 'id';
foreach ($classSectionIds as $cid) {
$eventFlag = $isEventOnly ? 1 : 0;
if (isset($existingBySection[$cid])) {
@@ -215,6 +224,7 @@ class StudentController extends BaseController
$attnStats = [];
$scoreStats = [];
$invoiceParentId = 0;
$paymentPendingNotification = null;
if (!$isEventOnly) {
// Update enrollment for current term (if exists)
$enroll = \Config\Services::enrollmentStatus(false)
@@ -258,6 +268,16 @@ class StudentController extends BaseController
if ((int) ($result['id'] ?? 0) <= 0) {
throw new \RuntimeException('Failed to update enrollment.');
}
$oldEnrollmentStatus = strtolower(trim(str_replace("\xc2\xa0", ' ', (string) ($result['old_status'] ?? $enroll['enrollment_status'] ?? ''))));
$oldEnrollmentStatus = preg_replace('/\s+/', ' ', $oldEnrollmentStatus) ?? $oldEnrollmentStatus;
if (in_array($oldEnrollmentStatus, ['admission under review', 'review & decision'], true)) {
$paymentPendingNotification = [
'parent_id' => $invoiceParentId,
'student_id' => $studentId,
'student_name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student #' . $studentId,
];
}
}
// 🔄 Re-tag attendance rows for this student in the current term (section + class)
@@ -301,6 +321,10 @@ class StudentController extends BaseController
}
}
if ($paymentPendingNotification !== null) {
$this->triggerPaymentPendingNotification($paymentPendingNotification);
}
$resp = [
'ok' => true,
'student_id' => $studentId,
@@ -323,6 +347,51 @@ class StudentController extends BaseController
}
}
private function triggerPaymentPendingNotification(array $notification): void
{
$parentId = (int) ($notification['parent_id'] ?? 0);
$studentId = (int) ($notification['student_id'] ?? 0);
if ($parentId <= 0 || $studentId <= 0) {
return;
}
try {
$parent = $this->userModel->find($parentId) ?? [];
$invoice = $this->db->table('invoices')
->where('parent_id', $parentId)
->where('school_year', (string) $this->schoolYear)
->orderBy('created_at', 'DESC')
->orderBy('id', 'DESC')
->limit(1)
->get()
->getRowArray();
$parentData = [
'user_id' => $parent['id'] ?? $parentId,
'email' => $parent['email'] ?? null,
'firstname' => $parent['firstname'] ?? '',
'lastname' => $parent['lastname'] ?? '',
'school_year' => (string) $this->schoolYear,
'portalLink' => base_url('/login'),
];
if ($invoice) {
$parentData['amount'] = (float) ($invoice['balance'] ?? $invoice['amount_due'] ?? $invoice['total_amount'] ?? 0);
$parentData['due_date'] = $invoice['due_date'] ?? null;
}
Events::trigger('paymentPending', $parentData, [[
'student_id' => $studentId,
'name' => (string) ($notification['student_name'] ?? ('Student #' . $studentId)),
]]);
} catch (\Throwable $e) {
log_message('error', 'Payment pending notification after class assignment failed for parent {parent_id}: {error}', [
'parent_id' => $parentId,
'error' => $e->getMessage(),
]);
}
}
/**
* Remove a student-class assignment for the current term.
*/
@@ -1116,6 +1185,7 @@ class StudentController extends BaseController
}
$rows = [];
$activeEnrollmentStatuses = $this->distributionActiveEnrollmentStatuses();
if ($this->db->tableExists('student_class')) {
$builder = $this->db->table('student_class sc')
@@ -1141,16 +1211,16 @@ class StudentController extends BaseController
}
if ($this->db->tableExists('enrollments')) {
$classIdExpression = $this->distributionEnrollmentClassIdExpression();
$sectionIdExpression = $this->distributionEnrollmentSectionIdExpression();
$builder = $this->db->table('enrollments e')
->select('0 AS promotion_queue_id, e.student_id, e.school_year AS school_year_from, cs.class_id AS source_class_id, cs.class_section_name AS source_class_name, students.firstname, students.lastname, students.gender, students.age, students.dob, students.registration_grade', false)
->select('0 AS promotion_queue_id, e.student_id, e.school_year AS school_year_from, ' . $classIdExpression . ' AS source_class_id, cs.class_section_name AS source_class_name, students.firstname, students.lastname, students.gender, students.age, students.dob, students.registration_grade', false)
->join('students', 'students.id = e.student_id', 'inner')
->join('classSection cs', 'cs.class_section_id = e.class_section_id AND cs.school_year = ' . $this->db->escape($year), 'left', false)
->join('classSection cs', 'cs.class_section_id = ' . $sectionIdExpression . ' AND cs.school_year = ' . $this->db->escape($year), 'left', false)
->where('e.school_year', $year)
->whereIn('e.enrollment_status', ['admission under review', 'payment pending', 'enrolled'])
->groupStart()
->where('e.is_withdrawn', 0)
->orWhere('e.is_withdrawn', null)
->groupEnd();
->whereIn('e.enrollment_status', $activeEnrollmentStatuses);
$this->applyEnrollmentNotWithdrawnFilter($builder);
if ($this->db->fieldExists('is_active', 'students')) {
$builder->where('students.is_active', 1);
@@ -1272,21 +1342,22 @@ class StudentController extends BaseController
return [];
}
$classIdExpression = $this->distributionEnrollmentClassIdExpression();
$sectionIdExpression = $this->distributionEnrollmentSectionIdExpression();
$builder = $this->db->table('enrollments e')
->select('0 AS promotion_queue_id, e.student_id, e.school_year AS school_year_from, cs.class_id AS source_class_id, cs.class_section_name AS source_class_name, students.firstname, students.lastname, students.gender, students.age, students.dob, students.registration_grade', false)
->select('0 AS promotion_queue_id, e.student_id, e.school_year AS school_year_from, ' . $classIdExpression . ' AS source_class_id, cs.class_section_name AS source_class_name, students.firstname, students.lastname, students.gender, students.age, students.dob, students.registration_grade', false)
->join('students', 'students.id = e.student_id', 'inner')
->join('classSection cs', 'cs.class_section_id = e.class_section_id AND cs.school_year = ' . $this->db->escape($year), 'left', false)
->join('classSection cs', 'cs.class_section_id = ' . $sectionIdExpression . ' AND cs.school_year = ' . $this->db->escape($year), 'left', false)
->where('e.school_year', $year)
->whereIn('e.enrollment_status', ['admission under review', 'payment pending', 'enrolled'])
->groupStart()
->where('e.is_withdrawn', 0)
->orWhere('e.is_withdrawn', null)
->groupEnd()
->whereIn('e.enrollment_status', $this->distributionActiveEnrollmentStatuses())
->groupStart()
->where('cs.class_id', $classId)
->orWhereIn('UPPER(students.registration_grade)', ['KG', 'K', 'KINDERGARTEN'])
->groupEnd();
$this->applyEnrollmentNotWithdrawnFilter($builder);
if ($this->db->fieldExists('is_active', 'students')) {
$builder->where('students.is_active', 1);
}
@@ -1459,19 +1530,17 @@ class StudentController extends BaseController
}
if ($this->db->tableExists('enrollments')) {
$rows = $this->db->table('enrollments e')
$sectionIdExpression = $this->distributionEnrollmentSectionIdExpression();
$builder = $this->db->table('enrollments e')
->select('cs.class_section_name')
->join('classSection cs', 'cs.class_section_id = e.class_section_id AND cs.school_year = ' . $this->db->escape($previousYear), 'left', false)
->join('classSection cs', 'cs.class_section_id = ' . $sectionIdExpression . ' AND cs.school_year = ' . $this->db->escape($previousYear), 'left', false)
->where('e.student_id', $studentId)
->where('e.school_year', $previousYear)
->where('e.class_section_id IS NOT NULL', null, false)
->whereIn('e.enrollment_status', ['admission under review', 'payment pending', 'enrolled'])
->groupStart()
->where('e.is_withdrawn', 0)
->orWhere('e.is_withdrawn', null)
->groupEnd()
->get()
->getResultArray();
->where($sectionIdExpression . ' IS NOT NULL', null, false)
->whereIn('e.enrollment_status', $this->distributionActiveEnrollmentStatuses());
$this->applyEnrollmentNotWithdrawnFilter($builder);
$rows = $builder->get()->getResultArray();
foreach ($rows as $row) {
$baseName = $this->baseClassNameForDistribution((string)($row['class_section_name'] ?? ''));
@@ -1901,20 +1970,18 @@ class StudentController extends BaseController
}
if (empty($names) && $this->db->tableExists('enrollments')) {
$rows = $this->db->table('enrollments e')
$sectionIdExpression = $this->distributionEnrollmentSectionIdExpression();
$builder = $this->db->table('enrollments e')
->select('cs.class_section_name')
->join('classSection cs', 'cs.class_section_id = e.class_section_id AND cs.school_year = ' . $this->db->escape($previousYear), 'left', false)
->join('classSection cs', 'cs.class_section_id = ' . $sectionIdExpression . ' AND cs.school_year = ' . $this->db->escape($previousYear), 'left', false)
->where('e.student_id', $studentId)
->where('e.school_year', $previousYear)
->where('e.class_section_id IS NOT NULL', null, false)
->whereIn('e.enrollment_status', ['payment pending', 'enrolled'])
->groupStart()
->where('e.is_withdrawn', 0)
->orWhere('e.is_withdrawn', null)
->groupEnd()
->orderBy('cs.class_section_name', 'ASC')
->get()
->getResultArray();
->where($sectionIdExpression . ' IS NOT NULL', null, false)
->whereIn('e.enrollment_status', $this->distributionActiveEnrollmentStatuses())
->orderBy('cs.class_section_name', 'ASC');
$this->applyEnrollmentNotWithdrawnFilter($builder);
$rows = $builder->get()->getResultArray();
foreach ($rows as $row) {
$name = trim((string)($row['class_section_name'] ?? ''));
if ($name !== '') {
@@ -2399,6 +2466,56 @@ class StudentController extends BaseController
return $this->distributionBaseClassIdCache[$cacheKey];
}
private function distributionActiveEnrollmentStatuses(): array
{
return [
'admission under review',
'review & decision',
'payment pending',
'enrolled',
];
}
private function applyEnrollmentNotWithdrawnFilter($builder): void
{
if (! $this->db->fieldExists('is_withdrawn', 'enrollments')) {
return;
}
$builder->groupStart()
->where('e.is_withdrawn', 0)
->orWhere('e.is_withdrawn', null)
->groupEnd();
}
private function distributionEnrollmentClassIdExpression(): string
{
$parts = [];
foreach (['assigned_grade_id', 'source_grade_id'] as $field) {
if ($this->db->fieldExists($field, 'enrollments')) {
$parts[] = 'e.' . $field;
}
}
$parts[] = 'cs.class_id';
return count($parts) === 1 ? $parts[0] : 'COALESCE(' . implode(', ', $parts) . ')';
}
private function distributionEnrollmentSectionIdExpression(): string
{
$parts = [];
foreach (['assigned_class_section_id', 'class_section_id', 'source_class_section_id'] as $field) {
if ($this->db->fieldExists($field, 'enrollments')) {
$parts[] = 'e.' . $field;
}
}
if (empty($parts)) {
return 'NULL';
}
return count($parts) === 1 ? $parts[0] : 'COALESCE(' . implode(', ', $parts) . ')';
}
private function pendingDistributionDraftClassIds(string $year): array
{
if ($year === '' || ! $this->db->tableExists('student_section_distribution_drafts')) {