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