fix logo circle, start email body
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 48s
Tests / PHPUnit (push) Failing after 1m17s

This commit is contained in:
root
2026-08-21 13:28:30 -04:00
parent d3da699e55
commit 23d1cbb64c
40 changed files with 303 additions and 157 deletions
+2 -1
View File
@@ -311,7 +311,8 @@ class Services extends BaseService
return new \App\Services\EnrollmentRegistrationEmailService(
\Config\Database::connect(),
static::enrollmentTransition(),
static::emailService()
static::emailService(),
model(\App\Models\ConfigurationModel::class)
);
}
@@ -46,7 +46,6 @@ class EnrollmentAdminController extends BaseController
'schoolYears' => $this->schoolYears(),
'classSections' => $this->classSections($schoolYear),
'admins' => $this->adminUsers(),
'enrollmentFollowups' => $this->enrollmentFollowups($schoolYear, $openFlags),
'auditRows' => $this->auditRows($schoolYear),
'activeExceptions' => $canManageExceptions ? $this->enrollmentExceptions($schoolYear) : [],
'exceptionNeeded' => $this->exceptionNeededFromFlags($openFlags),
+6 -6
View File
@@ -1115,7 +1115,7 @@ class StudentController extends BaseController
$builder->where('is_active', 1);
}
$this->applyDistributionAgeFilter($builder, $year);
$this->applyDistributionAgeFilter($builder, $year, 'dob');
$rows = array_merge($rows, $builder->get()->getResultArray());
}
@@ -1326,7 +1326,7 @@ class StudentController extends BaseController
$builder->where('is_active', 1);
}
$this->applyDistributionAgeFilter($builder, $year);
$this->applyDistributionAgeFilter($builder, $year, 'dob');
$rows = $builder
->orderBy('lastname', 'ASC')
->orderBy('firstname', 'ASC')
@@ -2138,14 +2138,14 @@ class StudentController extends BaseController
return $out;
}
private function applyDistributionAgeFilter($builder, string $schoolYear): void
private function applyDistributionAgeFilter($builder, string $schoolYear, string $dobColumn = 'students.dob'): void
{
[$earliestDob, $latestDob] = $this->distributionAgeBirthDateWindow($schoolYear);
$builder
->where('students.dob IS NOT NULL', null, false)
->where('students.dob >=', $earliestDob)
->where('students.dob <=', $latestDob);
->where($dobColumn . ' IS NOT NULL', null, false)
->where($dobColumn . ' >=', $earliestDob)
->where($dobColumn . ' <=', $latestDob);
}
private function distributionAgeBirthDateWindow(string $schoolYear): array
@@ -2,18 +2,20 @@
namespace App\Services;
use App\Models\ConfigurationModel;
use App\Support\Enrollment\DeliberationDecision;
use CodeIgniter\Database\BaseConnection;
use DateTimeInterface;
final class EnrollmentRegistrationEmailService
{
public const TEMPLATE_VERSION = 'phase5_consolidated_v2';
public const TEMPLATE_VERSION = 'phase5_consolidated_v3';
public function __construct(
private readonly BaseConnection $db,
private readonly EnrollmentTransitionService $transitionService,
private readonly EmailService $emailService,
private readonly ?ConfigurationModel $configurationModel = null,
) {
}
@@ -151,6 +153,7 @@ final class EnrollmentRegistrationEmailService
$opens = $this->dateText($schoolYear['registration_opens_at'] ?? $schoolYear['registration_starts_on'] ?? null);
$studentRows = [];
$studentIds = [];
$hasReEnrollmentEligibleStudent = false;
foreach ($family['students'] as $student) {
$studentId = (int) ($student['id'] ?? 0);
@@ -161,18 +164,16 @@ final class EnrollmentRegistrationEmailService
$evaluation = $this->transitionService->evaluate($studentId, $previousYear, $schoolYearName, 'parent');
$studentRows[] = $this->studentRow($student, $evaluation, $opens, $deadline);
$studentIds[] = $studentId;
$hasReEnrollmentEligibleStudent = $hasReEnrollmentEligibleStudent || $this->canCompleteParentReEnrollment($evaluation);
}
$financial = $this->financialSection((int) $family['parent_user_id'], $schoolYear, $previousYear);
$subject = 'Registration for ' . $schoolYearName . ' Is Now Open';
$bodyHtml = '<p>Dear ' . esc($family['name']) . ',</p>'
. '<p>Registration for the ' . esc($schoolYearName) . ' school year is now open. Please review the student summary below and complete the portal steps before ' . esc($deadline) . '.</p>'
. '<p>Registration for the ' . esc($schoolYearName) . ' school year is now open. Please review the student(s) summary below and complete the portal steps before ' . esc($deadline) . '.</p>'
. $this->studentsTable($studentRows)
. $financial
. '<h3>Registration Steps</h3>'
. '<p>Re-enrollment opens on ' . esc($opens) . ' and closes on ' . esc($deadline) . '. Complete re-enrollment for each eligible student before the deadline to secure their enrollment for the upcoming school year.</p>'
. '<ol><li>Sign in to the parent portal.</li><li>Review student information and upload any required documents.</li><li>Acknowledge school policies.</li><li>Review tuition, fees, and any carry-over balance.</li><li>Submit Enrollment.</li></ol>'
. '<p>Registration portal: <a href="' . esc(site_url('parent/enroll_classes')) . '">' . esc(site_url('parent/enroll_classes')) . '</a></p>'
. $this->registrationStepsSection($opens, $deadline, $hasReEnrollmentEligibleStudent)
. '<p>For assistance, please contact the school administration.</p>'
. '<p>Sincerely,<br>Al Rahma Sunday School<br>School Administration</p>';
@@ -189,16 +190,32 @@ final class EnrollmentRegistrationEmailService
];
}
private function canCompleteParentReEnrollment(array $evaluation): bool
{
return (bool) ($evaluation['parent_enrollment_allowed'] ?? false);
}
private function registrationStepsSection(string $opens, string $deadline, bool $include): string
{
if (! $include) {
return '';
}
return '<h3>Re-enrollment Steps</h3>'
. '<p>Re-enrollment opens on ' . esc($opens) . ' and closes on ' . esc($deadline) . '. Complete re-enrollment for each eligible student before the deadline to secure their enrollment for the upcoming school year.</p>'
. '<ol><li>Sign in to the parent portal.</li><li>Review/update student and contact information.</li><li>Acknowledge school policies.</li><li>Review tuition, fees and any carry-over balance.</li><li>Submit Enrollment.</li></ol>'
. '<p>Registration portal: <a href="' . esc(site_url('parent/enroll_classes')) . '">' . esc(site_url('parent/enroll_classes')) . '</a></p>';
}
private function studentsTable(array $studentRows): string
{
if ($studentRows === []) {
return '<p>No eligible linked students were found for this email.</p>';
}
return '<table style="width:100%; border-collapse:collapse; margin:12px 0 18px; font-size:14px;">'
. '<tbody>'
return '<div style="margin:12px 0 18px;">'
. implode('', $studentRows)
. '</tbody></table>';
. '</div>';
}
private function studentRow(array $student, array $evaluation, string $opens, string $deadline): string
@@ -207,17 +224,52 @@ final class EnrollmentRegistrationEmailService
$decision = DeliberationDecision::display((string) ($evaluation['deliberation_decision'] ?? ''));
$status = $this->registrationStatus($evaluation);
$placement = $this->placementText($evaluation);
$requiredAction = $this->requiredAction($evaluation, $deadline);
$requiredAction = $this->requiredAction($evaluation, $deadline, $name);
$message = $this->decisionMessage($name, $evaluation, $opens, $deadline);
$nextStepHtml = $this->decisionMessageHtml($message, (string) ($evaluation['deliberation_decision'] ?? ''));
if ($requiredAction !== '') {
$nextStepHtml .= '<br><strong>' . esc($requiredAction) . '</strong>';
}
return '<table role="presentation" cellpadding="0" cellspacing="0" style="width:100%; border:1px solid #d9e2ec; border-collapse:collapse; margin:0 0 12px; font-size:14px; line-height:1.45;">'
. '<tbody>'
. '<tr>'
. '<td style="background:#f4f8fb; border-bottom:1px solid #d9e2ec; padding:10px 12px; vertical-align:top; word-break:break-word;">'
. '<strong style="font-size:16px; color:#1f2937;">' . esc($name) . '</strong>'
. '</td>'
. '</tr>'
. $this->studentDetailRow('Decision', esc($decision ?: 'Pending'))
. $this->studentDetailRow('Registration Status', '<strong>' . esc($status) . '</strong><br>' . esc($placement))
. $this->studentDetailRow('Next Step', $nextStepHtml)
. '</tbody></table>';
}
private function studentDetailRow(string $label, string $valueHtml): string
{
return '<tr>'
. '<td style="border:1px solid #ddd; padding:8px; vertical-align:top;">' . esc($name) . '</td>'
. '<td style="border:1px solid #ddd; padding:8px; vertical-align:top;">' . esc($decision ?: 'Pending') . '</td>'
. '<td style="border:1px solid #ddd; padding:8px; vertical-align:top;"><strong>' . esc($status) . '</strong><br>' . esc($placement) . '</td>'
. '<td style="border:1px solid #ddd; padding:8px; vertical-align:top;">' . esc($message) . '<br><strong>' . esc($requiredAction) . '</strong></td>'
. '<td style="border-bottom:1px solid #edf2f7; padding:10px 12px; vertical-align:top; word-break:break-word;">'
. '<div style="font-size:12px; line-height:1.3; color:#64748b; text-transform:uppercase; font-weight:bold; margin:0 0 4px;">' . esc($label) . '</div>'
. '<div style="color:#1f2937;">' . $valueHtml . '</div>'
. '</td>'
. '</tr>';
}
private function decisionMessageHtml(string $message, string $decision): string
{
if ($decision !== DeliberationDecision::MAKE_UP_EXAM) {
return nl2br(esc($message), false);
}
$lines = explode("\n", $message);
$lastIndex = count($lines) - 1;
foreach ($lines as $index => $line) {
$escaped = esc($line);
$lines[$index] = $index === $lastIndex ? '<strong>' . $escaped . '</strong>' : $escaped;
}
return implode('<br>', $lines);
}
private function decisionMessage(string $name, array $evaluation, string $opens, string $deadline): string
{
if ((string) ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::PASSED) {
@@ -231,12 +283,29 @@ final class EnrollmentRegistrationEmailService
}
return match ((string) ($evaluation['deliberation_decision'] ?? '')) {
DeliberationDecision::REPEAT_CLASS => 'The deliberation decision for ' . $name . ' is to repeat the current grade. After registration is completed, the student will remain in the same grade and class when available.',
DeliberationDecision::MAKE_UP_EXAM => 'The deliberation decision for ' . $name . ' is pending the result of a make-up exam. Registration may be completed now. The student will initially remain in the same grade.',
DeliberationDecision::REPEAT_CLASS => 'The deliberation decision for ' . $name . ' is to repeat the current grade. After re-enrollment is completed, the student will remain in the same grade.',
DeliberationDecision::MAKE_UP_EXAM => 'The final academic decision for ' . $name . ' is currently pending the result of a make-up exam. This exam is scheduled for ' . $this->makeupExamDateText() . ' from 9:30 AM to 11:00 AM at ISGL.'
. "\n" . 'The exam result will determine whether the student advances to the next grade or repeats the current class. Failure to attend the make-up exam will automatically result in the student repeating the class, as no further retake opportunities will be available.'
. "\n" . 'You must re-enroll ' . $name . ' before the make-up exam can be taken.',
default => 'Please review the registration portal for the current enrollment status.',
};
}
private function makeupExamDateText(): string
{
$raw = trim((string) (($this->configurationModel ?? new ConfigurationModel())->getConfig('make-up-exam') ?? ''));
if ($raw === '') {
return 'a date to be announced';
}
$timestamp = strtotime($raw);
if ($timestamp !== false) {
return date('m-d-Y', $timestamp);
}
return $raw;
}
private function financialSection(int $parentId, array $schoolYear, ?string $previousYear): string
{
$carry = $previousYear !== null ? $this->invoiceBalance($parentId, $previousYear) : 0.0;
@@ -250,14 +319,11 @@ final class EnrollmentRegistrationEmailService
$message = trim((string) ($schoolYear['financial_policy_message'] ?? ''));
if ($message === '') {
$message = 'The balance is shown for information and does not currently block registration.';
$message = 'The balance needs to be settled with the school before the re-enrollment process can be started.';
}
return '<h3>Family Account Information</h3>'
. '<p><strong>Carry-over balance:</strong> $' . number_format($carry, 2) . '<br>'
. '<strong>Registration fee:</strong> $' . number_format($registrationFee, 2) . '<br>'
. '<strong>New-year tuition due now:</strong> $' . number_format($tuition, 2) . '<br>'
. '<strong>Mandatory fees:</strong> $' . number_format($mandatory, 2) . '<br>'
. '<strong>Total currently due:</strong> $' . number_format($total, 2) . '</p>'
. '<p>' . esc($message) . '</p>';
}
@@ -428,13 +494,13 @@ final class EnrollmentRegistrationEmailService
'automatic_distribution_pending' => $this->assignedGradeText($evaluation),
'same_class_assigned' => 'Same grade and class',
'temporary_same_grade' => 'Same grade initially',
'manual_class_required' => 'Same grade - administrative class assignment required',
'manual_class_required' => 'Same grade',
'exit_required' => 'Completion or exit process required',
default => 'Pending',
};
}
private function requiredAction(array $evaluation, string $deadline): string
private function requiredAction(array $evaluation, string $deadline, string $name = 'The student'): string
{
if ((string) ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::PASSED) {
return 'Complete re-enrollment before ' . $deadline . '.';
@@ -444,7 +510,7 @@ final class EnrollmentRegistrationEmailService
return ($evaluation['adult_student'] ?? false) ? 'Student must complete the authorized adult-student process or contact administration.' : 'Contact the school administration.';
}
return ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::MAKE_UP_EXAM
? 'Complete re-enrollment and follow the school instructions regarding the make-up exam.'
? ''
: 'Complete re-enrollment before ' . $deadline . '.';
}
+1 -1
View File
@@ -44,7 +44,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
@@ -222,7 +222,6 @@ $nonOverridableCodes = ['STUDENT_NOT_LINKED', 'SOURCE_YEAR_NOT_FOUND', 'TARGET_Y
$previewStudents = is_array($preview['students'] ?? null) ? $preview['students'] : [];
$hasGrantableStudent = false;
$openFlagCount = (int) ($openFlagCount ?? 0);
$followupCount = count($enrollmentFollowups ?? []);
$activeExceptionCount = 0;
foreach (($activeExceptions ?? []) as $exceptionRow) {
if (($exceptionRow['status'] ?? '') === 'active') {
@@ -273,15 +272,9 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<span class="badge rounded-pill <?= $openFlagCount > 0 ? 'bg-primary' : 'bg-secondary' ?> tab-count"><?= $openFlagCount ?></span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="followup-tab" data-bs-toggle="tab" data-bs-target="#followup-pane" type="button" role="tab">
2. Follow-up
<span class="badge rounded-pill bg-secondary tab-count"><?= $followupCount ?></span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="emails-tab" data-bs-toggle="tab" data-bs-target="#emails-pane" type="button" role="tab">
3. Parent emails
2. Parent emails
<span class="badge rounded-pill <?= $launchApproved ? 'bg-success' : ($launchReady ? 'bg-info text-dark' : 'bg-danger') ?> tab-count"><?= esc($emailStepLabel) ?></span>
</button>
</li>
@@ -348,8 +341,7 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<th>School ID</th>
<th>Issue</th>
<th>Details</th>
<th>Assigned</th>
<th>Created</th>
<th>Update by</th>
<th class="no-sort" style="min-width: 280px;">What to do</th>
</tr>
</thead>
@@ -380,7 +372,6 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<?php endif; ?>
</td>
<td><?= esc($flag['assignee_name'] ?: '') ?></td>
<td><?= esc(!empty($flag['created_at']) ? local_datetime($flag['created_at'], 'm-d-Y H:i') : '') ?></td>
<td class="action-stack">
<?php if (($flag['status'] ?? '') === 'open'): ?>
<?php if ($flagTypeValue === 'CLASS_REASSIGNMENT_REQUIRED'): ?>
@@ -435,67 +426,6 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
</div>
</div>
<div class="tab-pane fade" id="followup-pane" role="tabpanel">
<h3 class="h5 mb-1">Enrollments still waiting on staff</h3>
<p class="text-muted">These students already have an enrollment record. If a class or exception is needed, handle it in Work queue.</p>
<div class="table-responsive mb-4">
<table id="enrollmentFollowupTable" class="table table-bordered table-striped align-middle enrollment-admin-datatable no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th>Student</th>
<th>School ID</th>
<th>Enrollment status</th>
<th>Decision</th>
<th>Placement</th>
<th>Class</th>
<th>Exception / reason</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
<?php if (!empty($enrollmentFollowups)): ?>
<?php foreach ($enrollmentFollowups as $row): ?>
<?php
$statusValue = (string) ($row['enrollment_status'] ?? '');
$placementValue = (string) ($row['placement_status'] ?? '');
$decisionValue = (string) ($row['deliberation_decision'] ?? '');
$statusClass = match (strtolower($statusValue)) {
'review & decision' => 'bg-warning text-dark',
'waitlist' => 'bg-info text-dark',
'denied' => 'bg-danger',
default => 'bg-secondary',
};
?>
<tr>
<td>
<?= esc($row['student_name'] ?? '') ?>
<?= student_enrollment_status_button($row, $schoolYear ?? null) ?>
</td>
<td><?= esc($row['school_id'] ?? '') ?></td>
<td><span class="badge <?= esc($statusClass) ?>"><?= esc($statusValue) ?></span></td>
<td><?= esc($decisionValue !== '' ? ucwords(str_replace('_', ' ', $decisionValue)) : 'Pending') ?></td>
<td><?= esc(enrollment_admin_placement_label($placementValue)) ?></td>
<td><?= esc($row['class_section_name'] ?? '') ?></td>
<td>
<?php if (!empty($row['exception_required'])): ?>
<span class="badge bg-warning text-dark">Exception</span>
<?php endif; ?>
<?php
$exceptionReason = (string) ($row['exception_reason'] ?? '');
$reasonParts = array_values(array_filter(array_map('trim', explode(',', $exceptionReason))));
$looksLikeCodes = $reasonParts !== [] && count(array_filter($reasonParts, static fn (string $part): bool => (bool) preg_match('/^[A-Z0-9_]+$/', $part))) === count($reasonParts);
?>
<div class="small"><?= esc($looksLikeCodes ? implode(', ', array_map('enrollment_admin_flag_label', $reasonParts)) : $exceptionReason) ?></div>
</td>
<td><?= esc(!empty($row['updated_at']) ? local_datetime($row['updated_at'], 'm-d-Y H:i') : '') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="exceptions-pane" role="tabpanel">
<h3 class="h5 mb-1">Family exception tools</h3>
<p class="text-muted">Use the lookup to grant a family exception after reviewing the matching work queue item. Saved exceptions stay here for audit and revocation.</p>
@@ -904,7 +834,6 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
var tableMessages = {
enrollmentEmailExamplesTable: 'No parent decision email examples found for this school year.',
enrollmentFollowupTable: 'No enrollment records need follow-up for this school year.',
enrollmentFlagsTable: 'No students with enrollment blockages were found for these filters.',
enrollmentFamilyEligibilityTable: 'No linked students found for this parent.',
enrollmentExceptionsTable: 'No enrollment exceptions found for this school year.',
@@ -945,11 +874,27 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
});
}
function activateTabFromHash() {
if (!window.bootstrap || !window.bootstrap.Tab || !window.location.hash) {
return;
}
var target = window.location.hash;
var trigger = document.querySelector('[data-bs-toggle="tab"][data-bs-target="' + target + '"]');
if (trigger) {
window.bootstrap.Tab.getOrCreateInstance(trigger).show();
}
}
mergeExceptionPaneIntoWorkQueue();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initEnrollmentAdminTables);
document.addEventListener('DOMContentLoaded', function () {
activateTabFromHash();
initEnrollmentAdminTables();
});
} else {
activateTabFromHash();
initEnrollmentAdminTables();
}
})();
@@ -3,8 +3,8 @@
<style>
.enrollment-email-preview-frame .email-container {
max-width: 1280px;
width: min(1280px, 100%);
max-width: 640px;
width: min(640px, 100%);
}
</style>
@@ -14,7 +14,7 @@
<h2 class="mb-1">Registration Email Preview</h2>
<div class="text-muted"><?= esc($schoolYear ?? '') ?> · Parent #<?= esc((string) ($parentId ?? '')) ?></div>
</div>
<a class="btn btn-secondary" href="<?= site_url('administrator/enrollment-admin?school_year=' . rawurlencode((string) ($schoolYear ?? ''))) ?>">Back</a>
<a class="btn btn-secondary" href="<?= site_url('administrator/enrollment-admin?school_year=' . rawurlencode((string) ($schoolYear ?? ''))) ?>#emails-pane">Back</a>
</div>
<div class="mb-3">
+1 -1
View File
@@ -48,7 +48,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -14,7 +14,7 @@
<?= csrf_field() ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
</div>
+1 -1
View File
@@ -48,7 +48,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -11,7 +11,7 @@
.verify-card { max-width: 520px; margin: 60px auto; }
.badge-valid { background: #198754; }
.badge-invalid { background: #dc3545; }
.cert-logo { width: 70px; height: 70px; border-radius: 50%; object-fit: cover; }
.cert-logo { width: 70px; height: 70px; border-radius: 50%; object-fit: contain; background-color: #fff; }
.field-label { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: #6c757d; }
.field-value { font-size: 1.05rem; font-weight: 500; }
</style>
+1 -1
View File
@@ -45,7 +45,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -44,7 +44,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="modal-content rounded-4 shadow border-0" style="max-width: 600px; margin: auto;">
<div class="modal-body text-center p-5">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="Logo"
style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;" class="mb-4">
style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;" class="mb-4">
<h5 class="modal-title text-danger mb-3" id="blockedLabel">Access Blocked</h5>
<p class="lead mb-3">Too many password reset attempts have been made.</p>
<p>Please try again after 24 hours or contact support if you need urgent assistance.</p>
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="bg-white p-5 rounded-5 shadow registration-form container" style="max-width: 600px; width: 100%;">
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
</div>
+1 -1
View File
@@ -44,7 +44,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -413,7 +413,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 60px; width: 60px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 60px; width: 60px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title"></h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -57,7 +57,7 @@
<body>
<div class="email-container">
<div class="email-nopicture">
<img src="https://alrahmaisgl.org/assets/images/alrahma_logo.png" alt="" style="width: 110px; height: 110px; border-radius: 50%; object-fit: cover;">
<img src="https://alrahmaisgl.org/assets/images/alrahma_logo.png" alt="" style="width: 110px; height: 110px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</div>
<div class="email-body">
+1 -1
View File
@@ -175,7 +175,7 @@
#navbarManagement[data-mgmt-menu-mode="dark"] .navbar-toggler-icon { filter: invert(1) brightness(2); }
.logout-btn { border-color: var(--mgmt-primary) !important; color: var(--mgmt-primary) !important; }
.logout-btn:hover { background-color: var(--mgmt-primary) !important; color: #fff !important; }
.school-logo-circle { border-radius: 50%; object-fit: cover; }
.school-logo-circle { border-radius: 50%; object-fit: contain; background-color: #fff; }
/* Sidebar (hover to reveal) */
#navbarManagement.mgmt-sidebar {
+1 -1
View File
@@ -1,7 +1,7 @@
<!-- navbar.php -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" class="school-logo-circle" style="height: 40px; width: 40px; object-fit: cover; border-radius: 50%;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" class="school-logo-circle" style="height: 40px; width: 40px; object-fit: contain; border-radius: 50%; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -56,7 +56,7 @@
<div class="d-flex align-items-center w-100">
<!-- Brand (left) -->
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-1" href="/administrator/administratordashboard">
<img src="<?= base_url('assets/images/logo.png') ?>" alt="School Icon" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover; margin-right: 8px;">
<img src="<?= base_url('assets/images/logo.png') ?>" alt="School Icon" style="width: 40px; height: 40px; border-radius: 50%; object-fit: contain; background-color: #fff; margin-right: 8px;">
<strong>School Management Dashboard</strong>
</a>
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="bg-white p-5 rounded-5 shadow registration-form container" style="max-width: 600px; width: 100%;">
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
</div>
+1 -1
View File
@@ -43,7 +43,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -48,7 +48,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -48,7 +48,7 @@
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top px-4 px-lg-5 py-lg-0">
<a href="<?= base_url('/') ?>" class="navbar-brand d-flex align-items-center">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('images/logo.png') ?>" alt="Al Rahma Sunday School" style="height: 40px; width: 40px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h1 class="m-0 ms-2 green-title">Al Rahma Sunday School</h1>
</a>
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
+1 -1
View File
@@ -14,7 +14,7 @@
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt=""
style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
</div>
+1 -1
View File
@@ -8,7 +8,7 @@
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt=""
style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
</div>
@@ -4,7 +4,7 @@
<div class="modal-content rounded-4 shadow border-0" style="max-width: 600px; margin: auto;">
<div class="modal-body text-center">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="Logo" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="Logo" style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
<h5 class="modal-title text-success" id="emailConfirmLabel">Check Your Email</h5>
<p class="lead mt-3">
A link to reset the password has been sent to this email:<br>
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="modal fade" id="successModal" tabindex="-1" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-success shadow">
+1 -1
View File
@@ -20,7 +20,7 @@
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt=""
style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
</div>
+1 -1
View File
@@ -11,7 +11,7 @@
<?= csrf_field() ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;"></a>
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;"></a>
</div>
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Reset Your Password</h3>
<br>
@@ -6,7 +6,7 @@
<?= csrf_field(); ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
</div>
+1 -1
View File
@@ -8,7 +8,7 @@
<?= csrf_field(); ?>
<div class="text-center mb-4">
<a href="<?= base_url('/') ?>">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover;">
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;">
</a>
</div>
+4 -2
View File
@@ -323,14 +323,16 @@ color: #080808;
.navbar-logo {
height: 50px !important;
width: 50px !important;
object-fit: cover;
object-fit: contain;
border-radius: 50%;
background-color: #fff;
margin-right: 1rem;
}
.school-logo-circle {
border-radius: 50%;
object-fit: cover;
object-fit: contain;
background-color: #fff;
}
+2 -1
View File
@@ -24,7 +24,8 @@ html {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
object-fit: contain;
background-color: #fff;
margin-bottom: 20px;
}
@@ -25,7 +25,8 @@ body {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
object-fit: contain;
background-color: #fff;
margin-bottom: 10px;
}
+2 -1
View File
@@ -128,7 +128,8 @@ body.centered {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
object-fit: contain;
background-color: #fff;
margin-bottom: 20px;
}
+2 -1
View File
@@ -34,7 +34,8 @@ body {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
object-fit: contain;
background-color: #fff;
margin-top: 10px;
}
+2 -1
View File
@@ -2,7 +2,8 @@
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
object-fit: contain;
background-color: #fff;
}
/* Responsive adjustments for smaller screens */
@@ -6,6 +6,7 @@ use App\Services\EmailService;
use App\Services\EnrollmentRegistrationEmailService;
use App\Services\EnrollmentTransitionService;
use App\Support\Enrollment\DeliberationDecision;
use App\Models\ConfigurationModel;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\BaseConnection;
use PHPUnit\Framework\TestCase;
@@ -55,18 +56,75 @@ final class EnrollmentRegistrationEmailServiceTest extends TestCase
$this->assertSame('', $html);
}
public function testMakeupExamDecisionMessageUsesConfiguredDate(): void
{
$configModel = $this->createMock(ConfigurationModel::class);
$configModel->expects($this->once())
->method('getConfig')
->with('make-up-exam')
->willReturn('2026-09-13');
$service = $this->service(configModel: $configModel);
$message = $this->invoke($service, 'decisionMessage', ['Student Name', [
'deliberation_decision' => DeliberationDecision::MAKE_UP_EXAM,
'blockers' => [],
], 'August 1, 2026', 'August 31, 2026']);
$this->assertStringContainsString('The final academic decision for Student Name is currently pending the result of a make-up exam.', $message);
$this->assertStringContainsString('This exam is scheduled for 09-13-2026 from 9:30 AM to 11:00 AM at ISGL.', $message);
$this->assertStringContainsString("\n" . 'The exam result will determine whether the student advances to the next grade or repeats the current class. Failure to attend the make-up exam will automatically result in the student repeating the class, as no further retake opportunities will be available.', $message);
$this->assertStringContainsString("\n" . 'You must re-enroll Student Name before the make-up exam can be taken.', $message);
$this->assertStringNotContainsString('on date at ISGL', $message);
$action = $this->invoke($service, 'requiredAction', [[
'deliberation_decision' => DeliberationDecision::MAKE_UP_EXAM,
'blockers' => [],
], 'August 31, 2026', 'Student Name']);
$this->assertSame('', $action);
}
public function testMakeupExamStudentRowRendersMessageLineBreaks(): void
{
$configModel = $this->createMock(ConfigurationModel::class);
$configModel->method('getConfig')
->with('make-up-exam')
->willReturn('2026-09-13');
$service = $this->service(configModel: $configModel);
$html = $this->invoke($service, 'studentRow', [[
'firstname' => 'Student',
'lastname' => 'Name',
], [
'deliberation_decision' => DeliberationDecision::MAKE_UP_EXAM,
'placement_status' => 'temporary_same_grade',
'blockers' => [],
], 'August 1, 2026', 'August 31, 2026']);
$this->assertStringContainsString('from 9:30 AM to 11:00 AM at ISGL.<br>', $html);
$this->assertStringContainsString('available.<br>', $html);
$this->assertStringContainsString('<strong>You must re-enroll Student Name before the make-up exam can be taken.</strong>', $html);
$this->assertStringNotContainsString('before they are permitted', $html);
}
public function testFinancialSectionShowsWhenAnyAmountIsDue(): void
{
$service = $this->service();
$html = $this->invoke($service, 'financialSection', [10, [
'registration_fee' => 25,
'tuition_due_at_registration' => 0,
'mandatory_fees' => 0,
'tuition_due_at_registration' => 10,
'mandatory_fees' => 5,
], null]);
$this->assertStringContainsString('Family Account Information', $html);
$this->assertStringContainsString('Total currently due:</strong> $25.00', $html);
$this->assertStringContainsString('Carry-over balance:</strong> $0.00', $html);
$this->assertStringContainsString('Total currently due:</strong> $40.00', $html);
$this->assertStringNotContainsString('Registration fee:', $html);
$this->assertStringNotContainsString('New-year tuition due now:', $html);
$this->assertStringNotContainsString('Mandatory fees:', $html);
}
public function testAutomaticDistributionPlacementShowsOnlyGrade(): void
@@ -81,6 +139,75 @@ final class EnrollmentRegistrationEmailServiceTest extends TestCase
$this->assertSame('Grade 9', $placement);
}
public function testStudentSummaryUsesStackedEmailRows(): void
{
$service = $this->service();
$html = $this->invoke($service, 'studentRow', [[
'firstname' => 'Test',
'lastname' => 'Student',
], [
'deliberation_decision' => DeliberationDecision::PASSED,
'placement_status' => 'automatic_distribution_pending',
'source_grade_name' => '4',
'assigned_grade_name' => '5',
'blockers' => [],
], 'August 1, 2026', 'August 31, 2026']);
$this->assertStringContainsString('role="presentation"', $html);
$this->assertStringContainsString('Decision', $html);
$this->assertStringContainsString('Registration Status', $html);
$this->assertStringContainsString('Next Step', $html);
$this->assertStringContainsString('word-break:break-word', $html);
$this->assertStringNotContainsString('</td><td', $html);
}
public function testStudentsTableWrapsStackedStudentBlocks(): void
{
$service = $this->service();
$html = $this->invoke($service, 'studentsTable', [['<table role="presentation"></table>']]);
$this->assertStringStartsWith('<div style="margin:12px 0 18px;">', $html);
$this->assertStringContainsString('<table role="presentation"></table>', $html);
$this->assertStringEndsWith('</div>', $html);
}
public function testRegistrationStepsSectionIsHiddenWhenNoStudentCanReEnroll(): void
{
$service = $this->service();
$html = $this->invoke($service, 'registrationStepsSection', ['August 1, 2026', 'August 31, 2026', false]);
$this->assertSame('', $html);
}
public function testRegistrationStepsSectionShowsWhenAnyStudentCanReEnroll(): void
{
$service = $this->service();
$html = $this->invoke($service, 'registrationStepsSection', ['August 1, 2026', 'August 31, 2026', true]);
$this->assertStringContainsString('Re-enrollment Steps', $html);
$this->assertStringContainsString('Sign in to the parent portal.', $html);
$this->assertStringContainsString('parent/enroll_classes', $html);
}
public function testParentReEnrollmentEligibilityUsesTransitionAllowedFlag(): void
{
$service = $this->service();
$this->assertTrue($this->invoke($service, 'canCompleteParentReEnrollment', [[
'parent_enrollment_allowed' => true,
'blockers' => [],
]]));
$this->assertFalse($this->invoke($service, 'canCompleteParentReEnrollment', [[
'parent_enrollment_allowed' => false,
'blockers' => ['Contact administration.'],
]]));
}
public function testForceCannotSendWithoutAdminLaunchApproval(): void
{
$emailService = $this->createMock(EmailService::class);
@@ -108,7 +235,7 @@ final class EnrollmentRegistrationEmailServiceTest extends TestCase
return $reflection->invokeArgs($service, $args);
}
private function service(?BaseConnection $db = null, ?EmailService $emailService = null): EnrollmentRegistrationEmailService
private function service(?BaseConnection $db = null, ?EmailService $emailService = null, ?ConfigurationModel $configModel = null): EnrollmentRegistrationEmailService
{
$db ??= $this->createMock(BaseConnection::class);
@@ -116,6 +243,7 @@ final class EnrollmentRegistrationEmailServiceTest extends TestCase
$db,
new EnrollmentTransitionService($db),
$emailService ?? $this->createMock(EmailService::class),
$configModel,
);
}