216 lines
7.4 KiB
PHP
216 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use CodeIgniter\Database\BaseConnection;
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
|
|
final class EnrollmentStatusService
|
|
{
|
|
public const ACTIVE_STATUSES = [
|
|
'admission under review',
|
|
'review & decision',
|
|
'payment pending',
|
|
'enrolled',
|
|
'withdraw under review',
|
|
];
|
|
|
|
public const INACTIVE_STATUSES = [
|
|
'denied',
|
|
'refund pending',
|
|
'withdrawn',
|
|
'waitlist',
|
|
];
|
|
|
|
public const VALID_STATUSES = [
|
|
'admission under review',
|
|
'review & decision',
|
|
'payment pending',
|
|
'enrolled',
|
|
'withdraw under review',
|
|
'refund pending',
|
|
'withdrawn',
|
|
'waitlist',
|
|
'denied',
|
|
];
|
|
|
|
public function __construct(private readonly BaseConnection $db)
|
|
{
|
|
}
|
|
|
|
public function isActiveStatus(string $status): bool
|
|
{
|
|
return in_array($this->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'),
|
|
]);
|
|
}
|
|
}
|