Files
alrahma_sunday_school/app/Services/EnrollmentStatusService.php
T
root d906a915d6
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Failing after 1m20s
add refund logic and fix books inventory logic
2026-08-22 13:44:25 -04:00

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'),
]);
}
}