fix is_active student flag logic and make moving with enrollment status
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
<?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',
|
||||
'refund pending',
|
||||
];
|
||||
|
||||
public const INACTIVE_STATUSES = [
|
||||
'denied',
|
||||
'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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user