Files
alrahma_sunday_school/app/Services/Parents/ParentRegistrationService.php
T
root 140be9922d
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 49s
Tests / PHPUnit (push) Successful in 1m32s
fix registration issue and split parent controller into services
2026-08-30 20:40:37 -04:00

725 lines
28 KiB
PHP

<?php
namespace App\Services\Parents;
use App\Models\EmergencyContactModel;
use App\Models\EnrollmentModel;
use App\Models\StudentAllergyModel;
use App\Models\StudentMedicalConditionModel;
use App\Models\StudentModel;
use App\Models\UserModel;
use App\Services\PhoneFormatterService;
use App\Services\SchoolIdService;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;
use Throwable;
class ParentRegistrationService
{
public function __construct(
private readonly BaseConnection $db,
private readonly UserModel $userModel,
private readonly StudentModel $studentModel,
private readonly EnrollmentModel $enrollmentModel,
private readonly EmergencyContactModel $emergencyContactModel,
private readonly StudentMedicalConditionModel $medicalConditionModel,
private readonly StudentAllergyModel $allergyModel,
) {
}
public function registrationData(
int $parentId,
string $selectedSchoolYear,
bool $isEditable,
int $maxChilds,
int $maxEmergency
): array {
$enrollments = $this->getEnrollmentsByParent($parentId, $selectedSchoolYear);
$enrollmentMap = [];
foreach ($enrollments as $enroll) {
$enrollmentMap[$enroll['student_id']] = $enroll;
}
$user = $this->userModel->find($parentId);
if (! $user || ($user['user_type'] ?? '') !== 'primary') {
throw new \RuntimeException('Only primary parents are allowed to register children.');
}
$kids = $this->studentModel->where('parent_id', $parentId)->findAll();
foreach ($kids as &$kid) {
$studentId = (int) ($kid['id'] ?? 0);
$kid['allergies'] = $this->allergyModel->where('student_id', $studentId)->findColumn('allergy') ?? [];
$kid['medical_conditions'] = $this->medicalConditionModel->where('student_id', $studentId)->findColumn('condition_name') ?? [];
$kid['enrollment'] = isset($enrollmentMap[$studentId]['id']) && ! empty($enrollmentMap[$studentId]['id']) ? 1 : 0;
}
unset($kid);
$this->ensureStudentYearStatusRows($kids, $selectedSchoolYear);
service('studentYearStatus')->attachToStudents($kids, $selectedSchoolYear);
foreach ($kids as &$kid) {
$kid['can_delete'] = $this->canParentDeleteStudent($kid, $parentId);
}
unset($kid);
return [
'existingKids' => $kids,
'emergencies' => $this->emergencyContactModel->where('parent_id', $parentId)->findAll(),
'parent' => $user,
'maxChilds' => $maxChilds,
'maxEmergency' => $maxEmergency,
'enrollments' => $enrollments,
'selectedYear' => $selectedSchoolYear,
'isEditable' => $isEditable,
];
}
public function validateRegistrationSubmission(array $post, array $registrationData): array
{
$existingKids = $registrationData['existingKids'] ?? [];
$existingECs = $registrationData['emergencies'] ?? [];
$maxChilds = (int) ($registrationData['maxChilds'] ?? 0);
$maxEmergency = (int) ($registrationData['maxEmergency'] ?? 0);
$incomingFirstNames = (array) ($post['studentFirstName'] ?? []);
$incomingLastNames = (array) ($post['studentLastName'] ?? []);
$incomingDOBs = (array) ($post['dob'] ?? []);
$newStudentCount = count(array_filter($incomingFirstNames));
foreach ($incomingFirstNames as $i => $firstName) {
$lastName = trim($incomingLastNames[$i] ?? '');
$dob = trim($incomingDOBs[$i] ?? '');
if (empty($firstName) || empty($lastName) || empty($dob)) {
continue;
}
foreach ($existingKids as $kid) {
if (
strtolower($kid['firstname']) === strtolower($firstName)
&& strtolower($kid['lastname']) === strtolower($lastName)
&& $kid['dob'] === $dob
) {
return ['ok' => false, 'error' => "Duplicate student detected: {$firstName} {$lastName} with DOB {$dob} already exists."];
}
}
}
$seenStudents = [];
foreach ($incomingFirstNames as $i => $firstName) {
$lastName = trim($incomingLastNames[$i] ?? '');
$dob = trim($incomingDOBs[$i] ?? '');
if (empty($firstName) || empty($lastName) || empty($dob)) {
continue;
}
$key = strtolower($firstName . '|' . $lastName . '|' . $dob);
if (isset($seenStudents[$key])) {
return ['ok' => false, 'error' => "Duplicate student entry in the form: {$firstName} {$lastName} with DOB {$dob}."];
}
$seenStudents[$key] = true;
}
$incomingECFirst = (array) ($post['emergency_firstname'] ?? []);
$incomingECLast = (array) ($post['emergency_lastname'] ?? []);
$incomingECPhones = (array) ($post['emergency_phone'] ?? []);
$incomingECEmails = (array) ($post['emergency_email'] ?? []);
$newECCount = count(array_filter($incomingECFirst));
foreach ($incomingECFirst as $i => $first) {
$last = trim($incomingECLast[$i] ?? '');
$phone = preg_replace('/\D/', '', $incomingECPhones[$i] ?? '');
$email = strtolower(trim($incomingECEmails[$i] ?? ''));
if (empty($first) || empty($last)) {
continue;
}
foreach ($existingECs as $contact) {
$existingPhone = preg_replace('/\D/', '', $contact['cellphone']);
$existingEmail = strtolower($contact['email']);
if (
strtolower($contact['emergency_contact_name']) === strtolower(trim($first . ' ' . $last))
|| ($phone && $phone === $existingPhone)
|| ($email && $email === $existingEmail)
) {
return ['ok' => false, 'error' => "Duplicate emergency contact: {$first} {$last} already exists."];
}
}
}
$seenContacts = [];
foreach ($incomingECFirst as $i => $first) {
$last = trim($incomingECLast[$i] ?? '');
$phone = preg_replace('/\D/', '', $incomingECPhones[$i] ?? '');
$email = strtolower(trim($incomingECEmails[$i] ?? ''));
if (empty($first) || empty($last)) {
continue;
}
$key = strtolower($first . '|' . $last . '|' . $phone . '|' . $email);
if (isset($seenContacts[$key])) {
return ['ok' => false, 'error' => "Duplicate emergency contact entry in the form: {$first} {$last}."];
}
$seenContacts[$key] = true;
}
$existingKidsCount = count($existingKids);
$existingECCount = count($existingECs);
if (($existingKidsCount + $newStudentCount) > $maxChilds) {
return ['ok' => false, 'error' => "Student limit exceeded. You have $existingKidsCount and tried to add $newStudentCount (limit: $maxChilds)."];
}
if (($existingECCount + $newECCount) > $maxEmergency) {
return ['ok' => false, 'error' => "Emergency contact limit exceeded. You have $existingECCount and tried to add $newECCount (limit: $maxEmergency)."];
}
return ['ok' => true];
}
public function saveStudentAtIndex(
int $idx,
array $post,
int $parentId,
string $schoolYear,
SchoolIdService $schoolIdService,
?bool $isNew = null,
?int $studentId = null,
?string $schoolStartDate = null,
?string $ageDateReference = null
): array {
$firstName = $post['studentFirstName'][$idx] ?? null;
$lastName = $post['studentLastName'][$idx] ?? null;
$dob = $post['dob'][$idx] ?? null;
$gender = $post['gender'][$idx] ?? null;
$grade = $post['registration_grade'][$idx] ?? null;
$conditions = $post['medical_conditions'][$idx] ?? [];
$allergies = $post['allergies'][$idx] ?? [];
$photoRaw = $post['photo_consent'][$idx] ?? '';
if (! $firstName || ! $lastName || ! $dob || ! $gender || ! $grade) {
return ['ok' => false, 'empty' => true];
}
$firstName = $this->normalizeStudentName((string) $firstName);
$lastName = $this->normalizeStudentName((string) $lastName);
$this->validateNames($firstName);
$this->validateNames($lastName);
$dobObj = new DateTime((string) $dob);
$schoolYearAgeDeadline = $this->schoolYearAgeDeadline($schoolYear, $schoolStartDate);
$age = $this->calculateAgeAsOfSchoolYearStartYear((string) $dob, $schoolYear);
$validation = $this->validateDobAge(
(string) $dob,
$this->registrationMinimumAgeDeadline($schoolYear, $ageDateReference),
5,
18,
$schoolYearAgeDeadline
);
if (! $validation['isValid']) {
$displayDeadline = (new DateTime($schoolYearAgeDeadline))->format('m-d-Y');
return [
'ok' => false,
'error' => "Student '{$firstName} {$lastName}' {$validation['message']}. General age is calculated as of {$displayDeadline}.",
];
}
$studentData = [
'firstname' => $firstName,
'lastname' => $lastName,
'age' => $age,
'dob' => $dobObj->format('Y-m-d'),
'gender' => $gender,
'registration_grade' => $grade,
'photo_consent' => strtolower((string) $photoRaw) === 'yes' ? 1 : 0,
'parent_id' => $parentId,
'year_of_registration' => date('Y'),
];
if ($this->db->fieldExists('school_year', 'students')) {
$studentData['school_year'] = $schoolYear;
}
if ($isNew !== null) {
$studentData['is_new'] = $isNew ? 1 : 0;
}
$existingBuilder = $this->studentModel
->where('parent_id', $parentId)
->where('dob', $dobObj->format('Y-m-d'))
->where('firstname', $firstName)
->where('lastname', $lastName);
if ($this->db->fieldExists('school_year', 'students')) {
$existingBuilder->where('school_year', $schoolYear);
}
if (! $studentId && $existingBuilder->first()) {
return ['ok' => false, 'error' => "Student '{$firstName} {$lastName}' with the same birthdate is already registered for $schoolYear."];
}
if ($studentId) {
$existing = $this->studentModel->find($studentId);
if (! is_array($existing) || (int) ($existing['parent_id'] ?? 0) !== $parentId) {
return ['ok' => false, 'error' => 'Student record was not found for this parent account.'];
}
$this->auditParentStudentFieldChanges($existing, $studentData, $parentId, 'parent_student_edit');
$this->studentModel->update($studentId, $studentData);
if ($this->parentEditAffectsEligibility($existing, $studentData)) {
$this->recheckEligibilityAfterParentEdit($studentId, $parentId, $schoolYear);
}
} else {
$studentData['registration_date'] = utc_now();
$studentData['tuition_paid'] = 0;
$studentData['school_id'] = $schoolIdService->generateStudentSchoolId();
try {
$studentId = (int) $this->studentModel->insert($studentData, true);
} catch (DatabaseException $e) {
if (strpos($e->getMessage(), '1062') !== false) {
return ['ok' => false, 'error' => "Student '{$firstName} {$lastName}' with the same birthdate is already registered for $schoolYear."];
}
throw $e;
}
}
if ($isNew !== null && $studentId > 0) {
$studentYearStatus = service('studentYearStatus');
$statusSaved = $studentYearStatus->upsert($studentId, $schoolYear, $isNew);
if (! $statusSaved || ! $studentYearStatus->hasStatus($studentId, $schoolYear)) {
throw new \RuntimeException('Student year status could not be saved for student ID ' . $studentId . ' and school year ' . $schoolYear . '.');
}
}
$this->medicalConditionModel->where('student_id', $studentId)->delete();
foreach ((array) $conditions as $condition) {
$condition = trim((string) $condition);
if ($condition !== '') {
$this->medicalConditionModel->insert(['student_id' => $studentId, 'condition_name' => $condition]);
}
}
$this->allergyModel->where('student_id', $studentId)->delete();
foreach ((array) $allergies as $allergy) {
$allergy = trim((string) $allergy);
if ($allergy !== '') {
$this->allergyModel->insert(['student_id' => $studentId, 'allergy' => $allergy]);
}
}
return ['ok' => true, 'student_id' => $studentId];
}
public function saveEmergencyContact(int $parentId, array $post, ?array $single = null, ?int $id = null): array
{
$phoneFormatter = new PhoneFormatterService();
if ($single !== null) {
$firstName = $this->formatName($single['first_name'] ?? '');
$lastName = $this->formatName($single['last_name'] ?? '');
$relation = trim($single['relation'] ?? '');
$phone = $phoneFormatter->formatPhoneNumber($single['cellphone'] ?? '');
$email = strtolower(trim($single['email'] ?? ''));
if ($firstName === '' && $lastName === '' && $phone === '(000)-000-0000' && $email === '' && $relation === '') {
return ['ok' => true, 'empty' => true];
}
$this->validateNames($firstName);
$this->validateNames($lastName);
if ($email && ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \Exception('Invalid email format for emergency contact.');
}
$data = [
'parent_id' => $parentId,
'emergency_contact_name' => $firstName . ' ' . $lastName,
'cellphone' => $phone,
'email' => $email,
'relation' => $relation,
'updated_at' => utc_now(),
];
$duplicateBuilder = $this->emergencyContactModel
->where('parent_id', $parentId)
->where('emergency_contact_name', $data['emergency_contact_name'])
->where('cellphone', $phone)
->where('email', $email)
->where('relation', $relation);
if ($id !== null) {
$duplicateBuilder->where('id !=', $id);
}
if ($duplicateBuilder->first()) {
return ['ok' => false, 'error' => $id !== null
? 'Another emergency contact with the same information already exists.'
: 'This emergency contact is already registered.'];
}
if ($id !== null) {
$this->emergencyContactModel->update($id, $data);
} else {
$this->emergencyContactModel->insert($data);
}
return ['ok' => true];
}
$firstNames = (array) ($post['emergency_firstname'] ?? []);
$lastNames = (array) ($post['emergency_lastname'] ?? []);
$relations = (array) ($post['emergency_relation'] ?? []);
$phones = (array) ($post['emergency_phone'] ?? []);
$emails = (array) ($post['emergency_email'] ?? []);
foreach ($firstNames as $idx => $first) {
$firstName = $this->formatName($first ?? '');
$lastName = $this->formatName($lastNames[$idx] ?? '');
$relation = trim($relations[$idx] ?? '');
$phone = $phoneFormatter->formatPhoneNumber($phones[$idx] ?? '');
$email = strtolower(trim($emails[$idx] ?? ''));
if ($firstName === '' && $lastName === '' && $phone === '(000)-000-0000' && $email === '' && $relation === '') {
continue;
}
if ($phone === '(000)-000-0000') {
throw new \Exception('Invalid phone number.');
}
if ($email && ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \Exception('Invalid email format for emergency contact.');
}
$fullName = $firstName . ' ' . $lastName;
$exists = $this->emergencyContactModel->where([
'parent_id' => $parentId,
'emergency_contact_name' => $fullName,
'cellphone' => $phone,
'email' => $email,
'relation' => $relation,
])->first();
if (! $exists) {
$this->emergencyContactModel->insert([
'parent_id' => $parentId,
'emergency_contact_name' => $fullName,
'cellphone' => $phone,
'email' => $email,
'relation' => $relation,
]);
}
}
return ['ok' => true];
}
public function canParentDeleteStudent(array $student, int $parentId): bool
{
$studentId = (int) ($student['id'] ?? 0);
if ($studentId <= 0) {
return false;
}
$statusYear = trim((string) ($student['school_year'] ?? ''));
if ($statusYear === '') {
$statusYear = (string) (service('studentYearStatus')->activeSchoolYear() ?? '');
}
$isNew = $statusYear !== ''
? service('studentYearStatus')->isNew($studentId, $statusYear)
: ((string) ($student['is_new'] ?? '1') === '1');
return $isNew
&& ! $this->studentHasEnrollmentHistory($studentId, $parentId)
&& ! $this->studentHasClassAssignmentHistory($studentId);
}
public function validateDobAge(
string $dob,
string $registrationAgeDeadline,
int $minAge = 5,
int $maxAge = 18,
?string $schoolYearAgeDeadline = null
): array {
$response = ['isValid' => false, 'message' => '', 'age' => null];
$tz = new DateTimeZone('UTC');
$dob = trim($dob);
if ($dob === '') {
$response['message'] = 'Date of birth is required';
return $response;
}
$birthDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $tz);
$errs = DateTimeImmutable::getLastErrors();
if ($birthDate === false || (is_array($errs) && (($errs['warning_count'] ?? 0) > 0 || ($errs['error_count'] ?? 0) > 0))) {
$response['message'] = 'Invalid date format (Use YYYY-MM-DD)';
return $response;
}
try {
$minimumAgeDeadline = new DateTimeImmutable($registrationAgeDeadline, $tz);
} catch (Throwable $e) {
$minimumAgeDeadline = new DateTimeImmutable('now', $tz);
}
try {
$ageDeadline = new DateTimeImmutable($schoolYearAgeDeadline ?: $registrationAgeDeadline, $tz);
} catch (Throwable $e) {
$ageDeadline = $minimumAgeDeadline;
}
$minimumAgeDeadline = $minimumAgeDeadline->setTime(23, 59, 59);
$ageDeadline = $ageDeadline->setTime(23, 59, 59);
$ageAtDeadline = $birthDate->diff($ageDeadline)->y;
$ageAtMinimumAgeDeadline = $birthDate->diff($minimumAgeDeadline)->y;
$response['age'] = $ageAtDeadline;
$minBirthDate = $ageDeadline->modify('-' . ($maxAge + 1) . ' years')->modify('+1 day')->setTime(0, 0, 0);
$maxBirthDate = $minimumAgeDeadline->modify("-{$minAge} years")->setTime(23, 59, 59);
$response['isValid'] = ($birthDate >= $minBirthDate) && ($birthDate <= $maxBirthDate);
if (! $response['isValid']) {
$response['message'] = sprintf(
'Must be at least %d years old by %s and no older than %d by %s. Current registration age would be: %d',
$minAge,
$minimumAgeDeadline->format('m-d-Y'),
$maxAge,
$ageDeadline->format('m-d-Y'),
$ageAtMinimumAgeDeadline
);
}
return $response;
}
public function schoolYearAgeDeadline(string $schoolYear, ?string $schoolStartDate = null): string
{
if (preg_match('/^(\d{4})/', trim($schoolYear), $matches)) {
return $matches[1] . '-09-01';
}
if (! empty($schoolStartDate) && strtotime($schoolStartDate)) {
return (new DateTimeImmutable($schoolStartDate))->format('Y-m-d');
}
return date('Y') . '-09-01';
}
public function registrationMinimumAgeDeadline(string $schoolYear, ?string $ageDateReference = null): string
{
$configured = trim((string) $ageDateReference);
if ($configured !== '' && strtotime($configured)) {
return (new DateTimeImmutable($configured))->format('Y-m-d');
}
if (preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $matches)) {
return $matches[2] . '-12-31';
}
return date('Y') . '-12-31';
}
public function formatName(string $name): string
{
$name = trim($name);
$name = strtolower($name);
$name = ucwords($name, ' ');
return implode('-', array_map('ucfirst', explode('-', $name)));
}
public function validateNames(string $name): void
{
if (! preg_match('/^[A-Za-z\s\-]{2,30}$/', $name)) {
throw new InvalidArgumentException('Invalid name format: Only letters, spaces, or dashes (2-30 chars) allowed.');
}
}
private function getEnrollmentsByParent(int $parentId, string $schoolYear): array
{
return $this->enrollmentModel
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->orderBy('enrollment_date', 'DESC')
->findAll();
}
private function ensureStudentYearStatusRows(array $students, string $schoolYear): void
{
$schoolYear = trim($schoolYear);
if ($students === [] || ! preg_match('/^\d{4}-\d{4}$/', $schoolYear)) {
return;
}
$studentYearStatus = service('studentYearStatus');
foreach ($students as $student) {
$studentId = (int) ($student['id'] ?? $student['student_id'] ?? 0);
if ($studentId <= 0 || $studentYearStatus->hasStatus($studentId, $schoolYear)) {
continue;
}
$isNew = (int) ($student['is_new'] ?? 1) === 1;
if (! $studentYearStatus->upsert($studentId, $schoolYear, $isNew)) {
log_message('error', 'Unable to repair student_year_status for student_id={studentId}, school_year={schoolYear}', [
'studentId' => $studentId,
'schoolYear' => $schoolYear,
]);
}
}
}
private function calculateAgeAsOfSchoolYearStartYear(?string $dob, string $schoolYear): ?int
{
$dob = trim((string) $dob);
$schoolYear = trim($schoolYear);
if ($dob === '' || ! preg_match('/^(\d{4})/', $schoolYear, $matches)) {
return null;
}
try {
$timezone = new DateTimeZone((string) (config('School')->attendance['timezone'] ?? user_timezone()));
$birthDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dob, $timezone);
$errors = DateTimeImmutable::getLastErrors();
$hasParseErrors = is_array($errors)
&& (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0);
if ($birthDate === false || $hasParseErrors) {
return null;
}
$schoolYearStartYearCutoff = new DateTimeImmutable($matches[1] . '-09-01', $timezone);
if ($birthDate > $schoolYearStartYearCutoff) {
return null;
}
return $birthDate->diff($schoolYearStartYearCutoff)->y;
} catch (Throwable $e) {
log_message('warning', 'Unable to calculate school-year age from DOB: {message}', [
'message' => $e->getMessage(),
]);
return null;
}
}
private function normalizeStudentName(string $name): string
{
$name = trim(preg_replace('/\s+/', ' ', $name) ?? '');
return mb_convert_case($name, MB_CASE_TITLE, 'UTF-8');
}
private function auditParentStudentFieldChanges(array $original, array $updated, int $parentId, string $source): void
{
if (! $this->db->tableExists('enrollment_transition_audits')) {
return;
}
$studentId = (int) ($original['id'] ?? 0);
if ($studentId <= 0) {
return;
}
$changes = [];
foreach (['firstname', 'lastname', 'dob'] as $field) {
$oldValue = trim((string) ($original[$field] ?? ''));
$newValue = trim((string) ($updated[$field] ?? ''));
if ($oldValue !== $newValue) {
$changes[$field] = [
'old_value' => $oldValue,
'new_value' => $newValue,
'changed' => true,
'changed_by' => $parentId,
'changed_at' => date('Y-m-d H:i:s'),
'source' => $source,
];
}
}
if ($changes === []) {
return;
}
$this->db->table('enrollment_transition_audits')->insert([
'student_id' => $studentId,
'school_year' => (string) ($updated['school_year'] ?? $original['school_year'] ?? date('Y')),
'source_school_year' => null,
'action' => 'parent_student_field_edit',
'performed_by' => $parentId,
'original_values_json' => json_encode($original, JSON_UNESCAPED_SLASHES),
'new_values_json' => json_encode(['changes' => $changes, 'updated' => $updated], JSON_UNESCAPED_SLASHES),
'reason' => $source,
'created_at' => date('Y-m-d H:i:s'),
]);
}
private function parentEditAffectsEligibility(array $original, array $updated): bool
{
return trim((string) ($original['dob'] ?? '')) !== trim((string) ($updated['dob'] ?? ''))
|| trim((string) ($original['lastname'] ?? '')) !== trim((string) ($updated['lastname'] ?? ''));
}
private function recheckEligibilityAfterParentEdit(int $studentId, int $parentId, string $targetSchoolYear): void
{
$previousSchoolYear = $this->previousSchoolYearName($targetSchoolYear);
if ($previousSchoolYear === null) {
return;
}
$evaluation = service('enrollmentTransition')->evaluateForParent(
$parentId,
$studentId,
$previousSchoolYear,
$targetSchoolYear,
'parent'
);
if (($evaluation['can_enroll'] ?? false) === true || ($evaluation['parent_enrollment_allowed'] ?? false) === true) {
return;
}
$message = (string) ($evaluation['primary_parent_message'] ?? 'Updated student information affects enrollment eligibility.');
service('enrollmentTransition')->logEnrollmentBlock($evaluation, 'parent_student_edit', $parentId, $parentId);
session()->setFlashdata('warning', $message);
}
private function previousSchoolYearName(string $schoolYear): ?string
{
if (! preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $matches)) {
return null;
}
return ((int) $matches[1] - 1) . '-' . ((int) $matches[2] - 1);
}
private function studentHasEnrollmentHistory(int $studentId, int $parentId): bool
{
if (! $this->db->tableExists('enrollments')) {
return false;
}
return $this->db->table('enrollments')
->where('student_id', $studentId)
->where('parent_id', $parentId)
->countAllResults() > 0;
}
private function studentHasClassAssignmentHistory(int $studentId): bool
{
if (! $this->db->tableExists('student_class')) {
return false;
}
return $this->db->table('student_class')
->where('student_id', $studentId)
->countAllResults() > 0;
}
}