diff --git a/app/Commands/DeleteInactiveUsers.php b/app/Commands/DeleteInactiveUsers.php
index b22812c..1fb02d2 100644
--- a/app/Commands/DeleteInactiveUsers.php
+++ b/app/Commands/DeleteInactiveUsers.php
@@ -11,7 +11,7 @@ class DeleteInactiveUsers extends BaseCommand
{
protected $group = 'Maintenance';
protected $name = 'users:delete-inactive-users';
- protected $description = 'Delete users that are inactive and created more than 15 minutes ago, along with their entries in the parents table and user_roles table if applicable.';
+ protected $description = 'Delete unverified inactive registrations created more than 15 minutes ago, along with their entries in the parents table and user_roles table if applicable.';
public function run(array $params)
{
@@ -24,11 +24,12 @@ class DeleteInactiveUsers extends BaseCommand
log_message('debug', 'Cutoff time for deletion: ' . $cutoffTime);
// ─────────────────────────────────────────────────────
- // 1 Fetch inactive users older than 15 min
+ // 1 Fetch unfinished registrations older than 15 min
// ─────────────────────────────────────────────────────
$users = $db->table('users')
->select('id, firstname, lastname, email, created_at')
->where('status', 'Inactive')
+ ->where('is_verified', 0)
->where('created_at <', $cutoffTime)
->get()
->getResultArray();
@@ -102,4 +103,4 @@ class DeleteInactiveUsers extends BaseCommand
}
-}
\ No newline at end of file
+}
diff --git a/app/Controllers/View/ParentAttendanceReportController.php b/app/Controllers/View/ParentAttendanceReportController.php
index fe78fe8..5713e88 100644
--- a/app/Controllers/View/ParentAttendanceReportController.php
+++ b/app/Controllers/View/ParentAttendanceReportController.php
@@ -442,7 +442,7 @@ class ParentAttendanceReportController extends BaseController
}
// ✅ Build formatted success message
- $msg = '✅ Submission received successfully for ' . count($successNames) . ' item' . (count($successNames) > 1 ? 's' : '') . '.
';
+ $msg = '✅ Submission received successfully for ' . count($successNames) . ' student' . (count($successNames) > 1 ? 's' : '') . '.
';
$msg .= '
';
foreach ($successNames as $s) {
$dateLabel = $s['date_label'] ?? ($s['date'] ?? '');
diff --git a/app/Controllers/View/ParentController.php b/app/Controllers/View/ParentController.php
index b37399e..2db3087 100644
--- a/app/Controllers/View/ParentController.php
+++ b/app/Controllers/View/ParentController.php
@@ -26,6 +26,7 @@ use App\Services\Parents\ParentAttendanceService;
use App\Services\Parents\ParentEnrollmentService;
use App\Services\Parents\ParentEventParticipationService;
use App\Services\Parents\ParentPaymentService;
+use App\Services\Parents\ParentRegistrationNotificationService;
use App\Services\Parents\ParentRegistrationService;
use App\Support\Enrollment\DeliberationDecision;
use App\Support\Enrollment\EnrollmentEligibility;
@@ -71,6 +72,7 @@ class ParentController extends BaseController
protected ?ParentEnrollmentService $parentEnrollmentService = null;
protected ParentEventParticipationService $parentEventParticipationService;
protected ParentPaymentService $parentPaymentService;
+ protected ParentRegistrationNotificationService $parentRegistrationNotificationService;
protected ?ParentRegistrationService $parentRegistrationService = null;
@@ -95,6 +97,11 @@ class ParentController extends BaseController
$this->parentAccountService = new ParentAccountService($this->db, $this->userModel, $this->authorizedUsersModel);
$this->parentAttendanceService = new ParentAttendanceService($this->db);
$this->parentPaymentService = new ParentPaymentService($this->db);
+ $this->parentRegistrationNotificationService = new ParentRegistrationNotificationService(
+ $this->userModel,
+ $this->studentModel,
+ service('emailService')
+ );
$this->parentEnrollmentService = new ParentEnrollmentService(
$this->db,
$this->userModel,
@@ -180,10 +187,9 @@ class ParentController extends BaseController
public function index()
{
- // Retrieve all parents from the database where role is parent
- $parents = $this->userModel->where('role', 'parent')->findAll();
- // Pass the parents to the view
- return view('administrator/parent', ['parents' => $parents]);
+ return view('administrator/parent', [
+ 'parents' => $this->parentAccountService->administratorParentList(),
+ ]);
}
public function create()
@@ -194,44 +200,29 @@ class ParentController extends BaseController
public function store()
{
- // Handle the form submission to create a new parent
- $data = [
- 'firstname' => $this->request->getPost('firstname'),
- 'lastname' => $this->request->getPost('lastname'),
- 'email' => strtolower($this->request->getPost('email')),
- 'password' => password_hash($this->request->getPost('password'), PASSWORD_DEFAULT),
- 'role' => 'parent',
- ];
- $this->userModel->insert($data);
+ $this->parentAccountService->createAdministratorParent((array) $this->request->getPost());
+
return redirect()->to('/administrator/parent');
}
public function edit($id)
{
- // Retrieve the parent details to edit
- $parent = $this->userModel->find($id);
- return view('administrator/edit_parent', ['parent' => $parent]);
+ return view('administrator/edit_parent', [
+ 'parent' => $this->parentAccountService->parentById((int) $id),
+ ]);
}
public function update($id)
{
- // Handle the form submission to update an existing parent
- $data = [
- 'firstname' => $this->request->getPost('firstname'),
- 'lastname' => $this->request->getPost('lastname'),
- 'email' => strtolower($this->request->getPost('email')),
- ];
- if ($this->request->getPost('password')) {
- $data['password'] = password_hash($this->request->getPost('password'), PASSWORD_DEFAULT);
- }
- $this->userModel->update($id, $data);
+ $this->parentAccountService->updateAdministratorParent((int) $id, (array) $this->request->getPost());
+
return redirect()->to('/administrator/parent');
}
public function destroy($id)
{
- // Delete the parent
- $this->userModel->delete($id);
+ $this->parentAccountService->deleteParent((int) $id);
+
return redirect()->to('/administrator/parent');
}
@@ -2192,7 +2183,7 @@ class ParentController extends BaseController
}
if ($registeredStudentIds !== []) {
- $this->notifyAdminsAboutRegisteredStudents($registeredStudentIds, (int) $parentId);
+ $this->parentRegistrationNotificationService->sendAdminNewStudentEmails($registeredStudentIds, (int) $parentId);
}
if ($studentAdded) {
@@ -2208,55 +2199,6 @@ class ParentController extends BaseController
}
}
- /**
- * Send admin registration emails after commit so only saved students are reported.
- *
- * @param list $studentIds
- */
- private function notifyAdminsAboutRegisteredStudents(array $studentIds, int $parentId): void
- {
- $parent = $this->userModel->find($parentId);
- if (! is_array($parent)) {
- log_message('warning', 'Unable to send admin student registration email: parent not found for ID ' . $parentId);
- return;
- }
-
- foreach (array_values(array_unique(array_filter(array_map('intval', $studentIds)))) as $studentId) {
- $student = $this->studentModel->find($studentId);
- if (! is_array($student)) {
- log_message('warning', 'Unable to send admin student registration email: student not found for ID ' . $studentId);
- continue;
- }
-
- $payload = $student;
- $payload['parents'] = [
- 'user_id' => $parentId,
- 'firstname' => (string) ($parent['firstname'] ?? ''),
- 'lastname' => (string) ($parent['lastname'] ?? ''),
- 'email' => (string) ($parent['email'] ?? ''),
- ];
-
- $studentFullName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student ID ' . $studentId;
- $adminMessage = view('emails/admin_student_registered', ['student' => $payload], ['saveData' => true]);
-
- try {
- $sent = service('emailService')->send(
- 'registration@alrahmaisgl.org',
- 'New Student Registered: ' . $studentFullName,
- $adminMessage,
- 'notifications'
- );
-
- if (! $sent) {
- log_message('error', 'Admin student registration email failed for student ID ' . $studentId);
- }
- } catch (Throwable $e) {
- log_message('error', 'Admin student registration email failed for student ID ' . $studentId . ': ' . $e->getMessage());
- }
- }
- }
-
-
// Function to check if the parent has registered kids and redirect accordingly
public function registerKidCheck()
{
diff --git a/app/Controllers/View/UserController.php b/app/Controllers/View/UserController.php
index f39ef3c..5234daf 100644
--- a/app/Controllers/View/UserController.php
+++ b/app/Controllers/View/UserController.php
@@ -22,7 +22,7 @@ require_once APPPATH . 'Helpers/pbkdf2_helper.php';
class UserController extends BaseController
{
- private const ACTIVATION_TTL_HOURS = 48;
+ private const ACTIVATION_TTL_MINUTES = 15;
protected $userModel;
protected $roleModel;
protected $userRoleModel;
@@ -654,30 +654,7 @@ class UserController extends BaseController
public function confirm($token)
{
- log_message('info', 'Processing email confirmation.');
-
- $tokenHash = $this->hashToken($token);
- $user = $this->userModel
- ->groupStart()
- ->where('token', $tokenHash)
- ->orWhere('token', $token)
- ->groupEnd()
- ->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
- ->first();
-
- if (!$user || $user['is_verified'] == 1) {
- return redirect()->to('/invalid_token');
- }
-
-
- // Mark the user as verified and generate an account ID
- $account_id = 'ACC' . str_pad($user['id'], 8, '0', STR_PAD_LEFT); // Example: ACC00000001
- $this->userModel->update($user['id'], ['is_verified' => 1, 'token' => null, 'account_id' => $account_id]);
-
- log_message('info', 'User verified and account ID generated: ' . $account_id);
-
- // Redirect to the set password page
- return redirect()->to('/set_password/' . $user['id']);
+ return $this->setPassword($token);
}
public function setPassword($token)
@@ -690,7 +667,7 @@ class UserController extends BaseController
->where('token', $tokenHash)
->orWhere('token', $token)
->groupEnd()
- ->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
+ ->where('created_at >=', Time::now()->subMinutes(self::ACTIVATION_TTL_MINUTES)->toDateTimeString())
->first();
if (!$user || $user['is_verified'] == 1) {
@@ -747,7 +724,7 @@ class UserController extends BaseController
->where('token', $tokenHash)
->orWhere('token', $token)
->groupEnd()
- ->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
+ ->where('created_at >=', Time::now()->subMinutes(self::ACTIVATION_TTL_MINUTES)->toDateTimeString())
->first();
log_message('debug', "Attempting to set password for user $userId");
diff --git a/app/Filters/CleanupScheduler.php b/app/Filters/CleanupScheduler.php
index 555a8d7..cbad52a 100644
--- a/app/Filters/CleanupScheduler.php
+++ b/app/Filters/CleanupScheduler.php
@@ -31,6 +31,6 @@ class CleanupScheduler implements FilterInterface
{
// Call the cleanup controller method
\CodeIgniter\CLI\CLI::init();
- command('cleanup:unverified_users');
+ command('users:delete-inactive-users');
}
}
diff --git a/app/Models/UserModel.php b/app/Models/UserModel.php
index 0b056ad..d185998 100644
--- a/app/Models/UserModel.php
+++ b/app/Models/UserModel.php
@@ -85,12 +85,12 @@ class UserModel extends Model
}
/**
- * Get unverified users created more than 2 minutes ago.
+ * Get unverified users created more than 15 minutes ago.
*/
public function getUnverifiedUsers()
{
return $this->where('is_verified', 0)
- ->where('created_at <', date('Y-m-d H:i:s', time() - 120))
+ ->where('created_at <', date('Y-m-d H:i:s', time() - 15 * 60))
->findAll();
}
diff --git a/app/Services/Parents/ParentAccountService.php b/app/Services/Parents/ParentAccountService.php
index c5fc3d8..dc9482a 100644
--- a/app/Services/Parents/ParentAccountService.php
+++ b/app/Services/Parents/ParentAccountService.php
@@ -47,6 +47,49 @@ class ParentAccountService
return true;
}
+ public function administratorParentList(): array
+ {
+ return $this->userModel->where('role', 'parent')->findAll();
+ }
+
+ public function parentById(int $id): ?array
+ {
+ $parent = $this->userModel->find($id);
+
+ return is_array($parent) ? $parent : null;
+ }
+
+ public function createAdministratorParent(array $post): bool|int|string
+ {
+ return $this->userModel->insert([
+ 'firstname' => $post['firstname'] ?? null,
+ 'lastname' => $post['lastname'] ?? null,
+ 'email' => strtolower((string) ($post['email'] ?? '')),
+ 'password' => password_hash((string) ($post['password'] ?? ''), PASSWORD_DEFAULT),
+ 'role' => 'parent',
+ ]);
+ }
+
+ public function updateAdministratorParent(int $id, array $post): bool
+ {
+ $data = [
+ 'firstname' => $post['firstname'] ?? null,
+ 'lastname' => $post['lastname'] ?? null,
+ 'email' => strtolower((string) ($post['email'] ?? '')),
+ ];
+
+ if (! empty($post['password'])) {
+ $data['password'] = password_hash((string) $post['password'], PASSWORD_DEFAULT);
+ }
+
+ return (bool) $this->userModel->update($id, $data);
+ }
+
+ public function deleteParent(int $id): bool
+ {
+ return (bool) $this->userModel->delete($id);
+ }
+
public function createRelatedUser(array $userData, string $relationToStudent, string $semester, string $schoolYear): int|false
{
$schoolIdService = new SchoolIdService();
diff --git a/app/Services/Parents/ParentRegistrationNotificationService.php b/app/Services/Parents/ParentRegistrationNotificationService.php
new file mode 100644
index 0000000..92a3128
--- /dev/null
+++ b/app/Services/Parents/ParentRegistrationNotificationService.php
@@ -0,0 +1,77 @@
+ $studentIds
+ */
+ public function sendAdminNewStudentEmails(array $studentIds, int $parentId): void
+ {
+ $parent = $this->userModel->find($parentId);
+ if (! is_array($parent)) {
+ log_message('warning', 'Unable to send admin student registration email: parent not found for ID ' . $parentId);
+ return;
+ }
+
+ foreach (array_values(array_unique(array_filter(array_map('intval', $studentIds)))) as $studentId) {
+ $student = $this->studentModel->find($studentId);
+ if (! is_array($student)) {
+ log_message('warning', 'Unable to send admin student registration email: student not found for ID ' . $studentId);
+ continue;
+ }
+
+ $this->sendAdminNewStudentEmail($student, $parent, $parentId);
+ }
+ }
+
+ /**
+ * @param array $student
+ * @param array $parent
+ */
+ private function sendAdminNewStudentEmail(array $student, array $parent, int $parentId): void
+ {
+ $studentId = (int) ($student['id'] ?? 0);
+ $studentFullName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? ''))
+ ?: 'Student ID ' . $studentId;
+
+ $payload = $student;
+ $payload['parents'] = [
+ 'user_id' => $parentId,
+ 'firstname' => (string) ($parent['firstname'] ?? ''),
+ 'lastname' => (string) ($parent['lastname'] ?? ''),
+ 'email' => (string) ($parent['email'] ?? ''),
+ ];
+
+ $adminMessage = view('emails/admin_student_registered', ['student' => $payload], ['saveData' => true]);
+
+ try {
+ $sent = $this->emailService->send(
+ $this->adminEmail,
+ 'New Student Registered: ' . $studentFullName,
+ $adminMessage,
+ 'notifications'
+ );
+
+ if (! $sent) {
+ log_message('error', 'Admin student registration email failed for student ID ' . $studentId);
+ }
+ } catch (Throwable $e) {
+ log_message('error', 'Admin student registration email failed for student ID ' . $studentId . ': ' . $e->getMessage());
+ }
+ }
+}
diff --git a/tests/app/Services/ParentRegistrationNotificationServiceTest.php b/tests/app/Services/ParentRegistrationNotificationServiceTest.php
new file mode 100644
index 0000000..ad39815
--- /dev/null
+++ b/tests/app/Services/ParentRegistrationNotificationServiceTest.php
@@ -0,0 +1,65 @@
+createMock(UserModel::class);
+ $userModel->expects($this->once())
+ ->method('find')
+ ->with(12)
+ ->willReturn([
+ 'firstname' => 'Parent',
+ 'lastname' => 'One',
+ 'email' => 'parent@example.test',
+ ]);
+
+ $studentModel = $this->createMock(StudentModel::class);
+ $studentModel->expects($this->once())
+ ->method('find')
+ ->with(34)
+ ->willReturn([
+ 'id' => 34,
+ 'firstname' => 'Student',
+ 'lastname' => 'One',
+ ]);
+
+ $emailService = $this->createMock(EmailService::class);
+ $emailService->expects($this->once())
+ ->method('send')
+ ->with(
+ 'registration@example.test',
+ 'New Student Registered: Student One',
+ $this->stringContains('emails/admin_student_registered'),
+ 'notifications'
+ )
+ ->willReturn(true);
+
+ $service = new ParentRegistrationNotificationService(
+ $userModel,
+ $studentModel,
+ $emailService,
+ 'registration@example.test'
+ );
+
+ $service->sendAdminNewStudentEmails([34], 12);
+ }
+ }
+}