normalizeStatus($status), self::ACTIVE_STATUSES, true); } public function activeFlagForStatus(string $status): int { return $this->isActiveStatus($status) ? 1 : 0; } public function normalizeStatus(string $status): string { $status = strtolower(trim(str_replace("\xc2\xa0", ' ', $status))); $status = preg_replace('/\s+/', ' ', $status) ?? $status; if (! in_array($status, self::VALID_STATUSES, true)) { throw new InvalidArgumentException("Invalid enrollment status: {$status}"); } return $status; } public function upsertStatus(array $payload, ?int $performedBy = null, string $reason = 'enrollment_status_changed'): array { $studentId = (int) ($payload['student_id'] ?? 0); $schoolYear = trim((string) ($payload['school_year'] ?? '')); $status = $this->normalizeStatus((string) ($payload['enrollment_status'] ?? '')); if ($studentId <= 0 || $schoolYear === '') { throw new InvalidArgumentException('student_id and school_year are required.'); } $now = function_exists('utc_now') ? utc_now() : date('Y-m-d H:i:s'); $payload['student_id'] = $studentId; $payload['school_year'] = $schoolYear; $payload['enrollment_status'] = $status; $payload['updated_at'] = $payload['updated_at'] ?? $now; if (! isset($payload['admission_status'])) { $payload['admission_status'] = $this->admissionStatusFor($status); } if (! isset($payload['is_withdrawn']) && in_array($status, ['withdraw under review', 'refund pending', 'withdrawn'], true)) { $payload['is_withdrawn'] = 1; } elseif (! isset($payload['is_withdrawn']) && in_array($status, ['admission under review', 'review & decision', 'payment pending', 'enrolled', 'waitlist', 'denied'], true)) { $payload['is_withdrawn'] = 0; } $this->db->transStart(); $existing = $this->controllingEnrollment($studentId, $schoolYear); $oldStatus = $existing['enrollment_status'] ?? null; if ($existing !== null) { $this->db->table('enrollments') ->where('id', (int) $existing['id']) ->update($this->filterPayload('enrollments', $payload)); $enrollmentId = (int) $existing['id']; } else { $payload['created_at'] = $payload['created_at'] ?? $now; $insertPayload = $this->filterPayload('enrollments', $payload); $this->db->table('enrollments')->insert($insertPayload); $enrollmentId = (int) $this->db->insertID(); } $currentSchoolYear = $this->currentSchoolYear(); $updatedStudent = false; if ($currentSchoolYear === '' || $schoolYear === $currentSchoolYear) { $updatedStudent = $this->syncStudentActivityForEnrollment($studentId, $status); } $newSnapshot = $payload; $newSnapshot['id'] = $enrollmentId; $newSnapshot['old_enrollment_status'] = $oldStatus; $newSnapshot['new_enrollment_status'] = $status; $newSnapshot['student_is_active'] = $this->activeFlagForStatus($status); $newSnapshot['student_activity_synced'] = $updatedStudent; $this->audit($studentId, $schoolYear, $performedBy, $existing, $newSnapshot, $reason); $this->db->transComplete(); if ($this->db->transStatus() === false) { throw new RuntimeException('Unable to update enrollment status.'); } return [ 'id' => $enrollmentId, 'old_status' => $oldStatus, 'new_status' => $status, 'student_is_active' => $this->activeFlagForStatus($status), 'student_activity_synced' => $updatedStudent, ]; } public function syncStudentActivityForEnrollment(int $studentId, string $status): bool { if ($studentId <= 0 || ! $this->db->fieldExists('is_active', 'students')) { return false; } return (bool) $this->db->table('students') ->where('id', $studentId) ->update(['is_active' => $this->activeFlagForStatus($status)]); } public function controllingEnrollment(int $studentId, string $schoolYear, ?string $semester = null): ?array { $builder = $this->db->table('enrollments') ->where('student_id', $studentId) ->where('school_year', $schoolYear); if ($semester !== null && $semester !== '') { $builder->where('semester', $semester); } return $builder ->orderBy('updated_at', 'DESC') ->orderBy('enrollment_date', 'DESC') ->orderBy('id', 'DESC') ->limit(1) ->get() ->getRowArray() ?: null; } public function currentSchoolYear(): string { if (! $this->db->tableExists('configuration')) { return ''; } $row = $this->db->table('configuration') ->select('config_value') ->where('config_key', 'school_year') ->limit(1) ->get() ->getRowArray(); return trim((string) ($row['config_value'] ?? '')); } private function admissionStatusFor(string $status): string { if ($status === 'denied') { return 'denied'; } return in_array($status, ['payment pending', 'enrolled'], true) ? 'accepted' : 'pending'; } private function filterPayload(string $table, array $payload): array { $fields = $this->db->getFieldNames($table); return array_intersect_key($payload, array_flip($fields)); } private function audit(int $studentId, string $schoolYear, ?int $performedBy, ?array $original, array $new, string $reason): void { if (! $this->db->tableExists('enrollment_transition_audits')) { return; } $this->db->table('enrollment_transition_audits')->insert([ 'student_id' => $studentId, 'school_year' => $schoolYear, 'source_school_year' => $original['source_school_year'] ?? $new['source_school_year'] ?? null, 'action' => 'enrollment_status_sync', 'performed_by' => $performedBy, 'original_values_json' => $original !== null ? json_encode($original, JSON_UNESCAPED_SLASHES) : null, 'new_values_json' => json_encode($new, JSON_UNESCAPED_SLASHES), 'reason' => $reason, 'created_at' => function_exists('utc_now') ? utc_now() : date('Y-m-d H:i:s'), ]); } }