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
+66 -1
View File
@@ -514,6 +514,8 @@ class DiscountController extends BaseController
$affected = count($rowsToUpdate);
$db->transCommit();
$this->triggerStudentEnrolledNotification($parentId, $rowsToUpdate);
log_message(
'info',
'Enrollment status -> enrolled for {n} row(s). parent={p}, year={y}, sem={s}, ids=[{ids}]',
@@ -534,10 +536,60 @@ class DiscountController extends BaseController
}
}
private function triggerStudentEnrolledNotification(int $parentId, array $enrollmentRows): void
{
if ($parentId <= 0 || $enrollmentRows === []) {
return;
}
try {
$parent = $this->userModel->find($parentId) ?? [];
$studentIds = array_values(array_unique(array_filter(
array_map(static fn (array $row): int => (int) ($row['student_id'] ?? 0), $enrollmentRows),
static fn (int $studentId): bool => $studentId > 0
)));
if ($studentIds === []) {
return;
}
$students = [];
foreach ($this->db->table('students')->whereIn('id', $studentIds)->get()->getResultArray() as $student) {
$studentId = (int) ($student['id'] ?? 0);
if ($studentId <= 0) {
continue;
}
$students[] = [
'student_id' => $studentId,
'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student #' . $studentId,
];
}
if ($students === []) {
return;
}
Events::trigger('studentEnrolled', [
'user_id' => $parent['id'] ?? $parentId,
'email' => $parent['email'] ?? null,
'firstname' => $parent['firstname'] ?? '',
'lastname' => $parent['lastname'] ?? '',
'school_year' => (string) $this->schoolYear,
'portalLink' => base_url('/login'),
], $students);
} catch (\Throwable $e) {
log_message('error', 'Enrollment confirmation notification after discount failed for parent {parent_id}: {error}', [
'parent_id' => $parentId,
'error' => $e->getMessage(),
]);
}
}
public function listVouchers()
{
$this->schoolYear = $this->activeSchoolYearName();
$this->schoolYear = $this->currentSchoolYearName();
$vouchers = $this->voucherModel
->where('school_year', $this->schoolYear)
@@ -684,6 +736,19 @@ class DiscountController extends BaseController
}
}
protected function currentSchoolYearName(?string $fallback = null): string
{
try {
return service('schoolYearContext')->resolve($this->request)->yearName();
} catch (\Throwable) {
if ($fallback !== null && $fallback !== '') {
return $fallback;
}
return (string) ($this->configModel->getConfig('school_year') ?? '');
}
}
/**
* 🔄 Helper: Current invoice balance (school-year scoped) = total - payments - discounts - refundsPaid
*/
+28 -11
View File
@@ -2010,8 +2010,14 @@ class ParentController extends BaseController
private function enrollmentTuitionDue(array $students): float
{
$tuitionStudents = [];
$existingTuitionStudentCount = 0;
foreach ($students as $student) {
if ($this->countsTowardCurrentYearTuition($student)) {
$existingTuitionStudentCount++;
continue;
}
$evaluation = is_array($student['transition_evaluation'] ?? null) ? $student['transition_evaluation'] : [];
if (
($evaluation['can_enroll'] ?? false) !== true
@@ -2034,9 +2040,29 @@ class ParentController extends BaseController
return 0.0;
}
if ($existingTuitionStudentCount > 0) {
$additionalStudentFee = (float) ($this->configModel->getConfig('second_student_fee') ?? 280);
return round(count($tuitionStudents) * $additionalStudentFee, 2);
}
return (new FeeCalculationService())->calculateEnrollmentTuition($tuitionStudents);
}
private function countsTowardCurrentYearTuition(array $student): bool
{
$status = strtolower(trim((string) ($student['enrollment_status'] ?? '')));
if (in_array($status, ['withdrawn', 'withdraw under review', 'refund pending', 'denied', 'not enrolled'], true)) {
return false;
}
if (in_array($status, ['admission under review', 'review & decision', 'payment pending', 'enrolled', 'waitlist'], true)) {
return true;
}
return strtolower(trim((string) ($student['admission_status'] ?? ''))) === 'accepted';
}
private function enrollmentFeeSchedule(string $selectedYear): array
{
$schoolYearConfig = $this->schoolYearConfig($selectedYear);
@@ -2673,7 +2699,6 @@ class ParentController extends BaseController
$this->db->transStart();
$studentAdded = false;
$registeredStudentIds = [];
// Gather all POST data for last year flags
$rawPost = $this->request->getPost();
@@ -2700,9 +2725,6 @@ class ParentController extends BaseController
if ($result) {
$studentAdded = true;
if (is_numeric($result) && (int) $result > 0) {
$registeredStudentIds[] = (int) $result;
}
}
}
}
@@ -2724,13 +2746,8 @@ class ParentController extends BaseController
}
if ($studentAdded) {
$enrollmentQuery = ['start' => 2];
if ($registeredStudentIds !== []) {
$enrollmentQuery['students'] = implode(',', array_values(array_unique($registeredStudentIds)));
}
return redirect()->to(base_url('/parent/enroll_classes?' . http_build_query($enrollmentQuery)))
->with('success', 'Registration successful! Continue enrollment by confirming your home address and phone number.');
return redirect()->to(base_url('/parent/enroll_classes?' . http_build_query(['start' => 1])))
->with('success', 'Registration successful! Continue enrollment by selecting all students you want to enroll.');
}
return redirect()->to(base_url('/parent/child_register'))->with('success', 'Registration successful!');
+1 -1
View File
@@ -332,7 +332,7 @@ class PaymentController extends ResourceController
// Read the search term (email or phone)
$searchTerm = trim((string) $this->request->getGet('search_term'));
$manualPaySchoolYear = $this->activeSchoolYearName();
$manualPaySchoolYear = $this->currentSchoolYearName();
// --- Installment end date comes ONLY from config ---
$installmentDateRaw = (string) ($this->installmentDate ?? '');
+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')) {