fix enrollment, invoice, payment and financila aid
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Failing after 1m19s

This commit is contained in:
root
2026-08-24 21:20:58 -04:00
parent 576da3bd78
commit 608aca79b8
30 changed files with 3327 additions and 774 deletions
@@ -116,15 +116,31 @@ final class EnrollmentRegistrationEmailService
foreach ($this->recipientFamilies($schoolYear, null) as $family) {
$message = $this->buildMessage($schoolYear, $family);
$latest = $this->latestEmailRecord($schoolYearName, (int) $family['parent_user_id']);
$previousYear = $this->previousSchoolYearName($schoolYearName);
$studentSummaries = [];
foreach ($family['students'] as $student) {
$studentId = (int) ($student['id'] ?? 0);
$evaluation = $studentId > 0 && $previousYear !== null
? $this->transitionService->evaluateForParent(
(int) $family['parent_user_id'],
$studentId,
$previousYear,
$schoolYearName
)
: [];
$studentSummaries[] = [
'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student #' . $studentId,
'adult_student' => ! empty($evaluation['adult_student']),
];
}
$examples[] = [
'parent_user_id' => (int) $family['parent_user_id'],
'parent_name' => (string) $family['name'],
'recipients' => $message['recipients'],
'student_names' => array_map(
static fn (array $student): string => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student #' . (int) ($student['id'] ?? 0),
$family['students']
),
'students' => $studentSummaries,
'student_names' => array_column($studentSummaries, 'name'),
'subject' => (string) $message['subject'],
'delivery_status' => (string) ($latest['delivery_status'] ?? 'not sent'),
'sent_at' => $latest['sent_at'] ?? null,
@@ -161,10 +177,15 @@ final class EnrollmentRegistrationEmailService
continue;
}
$evaluation = $this->transitionService->evaluate($studentId, $previousYear, $schoolYearName, 'parent');
$evaluation = $this->transitionService->evaluateForParent(
(int) $family['parent_user_id'],
$studentId,
$previousYear,
$schoolYearName
);
$studentRows[] = $this->studentRow($student, $evaluation, $opens, $deadline, $schoolYear);
$studentIds[] = $studentId;
$hasReEnrollmentEligibleStudent = $hasReEnrollmentEligibleStudent || $this->canCompleteParentReEnrollment($evaluation);
$hasReEnrollmentEligibleStudent = $hasReEnrollmentEligibleStudent || (bool) ($evaluation['can_enroll'] ?? false);
}
$financial = $this->financialSection((int) $family['parent_user_id'], $schoolYear, $previousYear);
@@ -192,7 +213,7 @@ final class EnrollmentRegistrationEmailService
private function canCompleteParentReEnrollment(array $evaluation): bool
{
return (bool) ($evaluation['parent_enrollment_allowed'] ?? false);
return (bool) ($evaluation['can_enroll'] ?? false);
}
private function registrationStepsSection(string $opens, string $deadline, bool $include): string
@@ -227,7 +248,7 @@ final class EnrollmentRegistrationEmailService
$requiredAction = $this->requiredAction($evaluation, $deadline, $name);
$message = $this->decisionMessage($name, $evaluation, $opens, $deadline, $schoolYear);
$nextStepHtml = $this->decisionMessageHtml($message, (string) ($evaluation['deliberation_decision'] ?? ''));
if ($requiredAction !== '') {
if ($this->shouldAppendRequiredAction($message, $requiredAction)) {
$nextStepHtml .= '<br><strong>' . esc($requiredAction) . '</strong>';
}
@@ -239,7 +260,7 @@ final class EnrollmentRegistrationEmailService
. '</td>'
. '</tr>'
. $this->studentDetailRow('Decision', esc($decision ?: 'Pending'))
. $this->studentDetailRow('Registration Status', '<strong>' . esc($status) . '</strong><br>' . esc($placement))
. $this->studentDetailRow('Registration Status', '<strong>' . esc($status) . '</strong>' . ($placement !== '' ? '<br>' . esc($placement) : ''))
. $this->studentDetailRow('Next Step', $nextStepHtml)
. '</tbody></table>';
}
@@ -272,14 +293,15 @@ final class EnrollmentRegistrationEmailService
private function decisionMessage(string $name, array $evaluation, string $opens, string $deadline, array $schoolYear = []): string
{
if ((string) ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::PASSED) {
$grade = $this->currentGradeText($evaluation);
return $name . ' has successfully passed ' . $grade . '.';
if (($evaluation['can_enroll'] ?? false) === false && ! empty($evaluation['primary_parent_message'])) {
return (string) $evaluation['primary_parent_message'];
}
if (($evaluation['blockers'] ?? []) !== []) {
return implode(' ', array_map('strval', $evaluation['blockers']));
return implode(' ', array_values(array_unique(array_filter(array_map(
static fn ($blocker): string => trim((string) $blocker),
$evaluation['blockers']
)))));
}
return match ((string) ($evaluation['deliberation_decision'] ?? '')) {
@@ -318,11 +340,13 @@ final class EnrollmentRegistrationEmailService
private function financialSection(int $parentId, array $schoolYear, ?string $previousYear): string
{
$carry = $previousYear !== null ? $this->invoiceBalance($parentId, $previousYear) : 0.0;
$registrationFee = (float) ($schoolYear['registration_fee'] ?? 0);
$tuition = (float) ($schoolYear['tuition_due_at_registration'] ?? 0);
$mandatory = (float) ($schoolYear['mandatory_fees'] ?? 0);
$total = max(0, $carry) + $registrationFee + $tuition + $mandatory;
$schoolYearName = (string) ($schoolYear['name'] ?? '');
$summary = $this->transitionService->getEnrollmentFinancialSummary(
$parentId,
$previousYear ?? '',
$schoolYearName
);
$total = (float) ($summary['total_enrollment_due'] ?? $summary['amount_due'] ?? 0);
if ($total <= 0.0) {
return '';
}
@@ -333,7 +357,7 @@ final class EnrollmentRegistrationEmailService
}
return '<h3>Family Account Information</h3>'
. '<p><strong>Carry-over balance:</strong> $' . number_format($carry, 2) . '<br>'
. '<p><strong>Carry-over balance:</strong> $' . number_format((float) ($summary['carry_forward_balance'] ?? 0), 2) . '<br>'
. '<strong>Total currently due:</strong> $' . number_format($total, 2) . '</p>'
. '<p>' . esc($message) . '</p>';
}
@@ -488,40 +512,73 @@ final class EnrollmentRegistrationEmailService
private function registrationStatus(array $evaluation): string
{
if ((string) ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::PASSED) {
return 'Eligible';
if (($evaluation['can_enroll'] ?? false) === true) {
return ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::MAKE_UP_EXAM
? 'Eligible with pending placement'
: 'Eligible';
}
if (! empty($evaluation['adult_student'])) {
return 'Not Eligible';
}
if (($evaluation['blockers'] ?? []) !== []) {
return ($evaluation['adult_student'] ?? false) ? 'Student Action Required' : 'Not Eligible';
return 'Not Eligible';
}
return ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::MAKE_UP_EXAM ? 'Eligible with pending placement' : 'Eligible';
return 'Not Eligible';
}
private function placementText(array $evaluation): string
{
return match ((string) ($evaluation['placement_status'] ?? '')) {
$placement = match ((string) ($evaluation['placement_status'] ?? '')) {
'automatic_distribution_pending' => $this->assignedGradeText($evaluation),
'same_class_assigned' => 'Same grade and class',
'temporary_same_grade' => 'Same grade initially',
'manual_class_required' => 'Same grade',
'exit_required' => 'Completion or exit process required',
default => 'Pending',
default => '',
};
if ($placement !== '') {
return $placement;
}
return ($evaluation['can_enroll'] ?? false) === true ? 'Pending' : '';
}
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 . '.';
if (($evaluation['can_enroll'] ?? false) === true) {
return ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::MAKE_UP_EXAM
? ''
: 'Complete re-enrollment before ' . $deadline . '.';
}
if (($evaluation['blockers'] ?? []) !== []) {
return ($evaluation['adult_student'] ?? false) ? 'Student must complete the authorized adult-student process or contact administration.' : 'Contact the school administration.';
if (! empty($evaluation['primary_parent_message'])) {
return (string) $evaluation['primary_parent_message'];
}
return ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::MAKE_UP_EXAM
? ''
: 'Complete re-enrollment before ' . $deadline . '.';
return 'Contact the school administration.';
}
private function shouldAppendRequiredAction(string $message, string $requiredAction): bool
{
$message = trim($message);
$requiredAction = trim($requiredAction);
if ($requiredAction === '') {
return false;
}
if ($message === '') {
return true;
}
$normalizedMessage = preg_replace('/\s+/', ' ', strtolower($message)) ?? $message;
$normalizedAction = preg_replace('/\s+/', ' ', strtolower($requiredAction)) ?? $requiredAction;
return $normalizedMessage !== $normalizedAction
&& ! str_contains($normalizedMessage, $normalizedAction)
&& ! str_contains($normalizedAction, $normalizedMessage);
}
private function previousSchoolYearName(string $schoolYear): ?string
File diff suppressed because it is too large Load Diff
@@ -214,6 +214,8 @@ public function updateStatuses(?array $enrollmentStatuses, string $schoolYear, s
$refundAmountByParent = []; // parent_id => preview amount for notification context
$validStatuses = EnrollmentStatusService::VALID_STATUSES;
$transitionService = service('enrollmentTransition');
$advanceStatuses = $transitionService->statusesRequiringEnrollmentEligibility();
foreach ($enrollmentStatuses as $studentId => $newEnrollmentStatus) {
if (!in_array($newEnrollmentStatus, $validStatuses, true)) {
@@ -246,6 +248,16 @@ public function updateStatuses(?array $enrollmentStatuses, string $schoolYear, s
continue;
}
if (in_array($newEnrollmentStatus, $advanceStatuses, true)) {
$advance = $transitionService->evaluateEnrollmentAdvance($parentId, (int) $studentId, $this->schoolYear, 'admin');
if (! $transitionService->adminMayAdvanceEnrollment($advance)) {
$transitionService->logEnrollmentBlock($advance, 'admin_updateStatuses_create', $parentId, $performedBy);
$errors[] = "Student ID $studentId cannot be advanced to '$newEnrollmentStatus': "
. (string) ($advance['primary_parent_message'] ?? 'Enrollment eligibility check failed.');
continue;
}
}
$isWithdrawn = in_array($newEnrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review'], true) ? 1 : 0;
$result = $enrollmentStatusService->upsertStatus([
@@ -323,6 +335,16 @@ public function updateStatuses(?array $enrollmentStatuses, string $schoolYear, s
continue;
}
if ($oldStatus !== $newEnrollmentStatus && in_array($newEnrollmentStatus, $advanceStatuses, true)) {
$advance = $transitionService->evaluateEnrollmentAdvance((int) $parentId, (int) $studentId, $this->schoolYear, 'admin');
if (! $transitionService->adminMayAdvanceEnrollment($advance)) {
$transitionService->logEnrollmentBlock($advance, 'admin_updateStatuses', (int) $parentId, $performedBy);
$errors[] = "Student ID $studentId cannot be advanced to '$newEnrollmentStatus': "
. (string) ($advance['primary_parent_message'] ?? 'Enrollment eligibility check failed.');
continue;
}
}
$result = $enrollmentStatusService->upsertStatus([
'id' => (int) $enrollmentRow['id'],
'student_id' => (int) $studentId,
@@ -4,6 +4,9 @@ namespace App\Services;
use CodeIgniter\Database\BaseConnection;
/**
* @deprecated Use EnrollmentRegistrationEmailService instead. This legacy sender is retained for reference only.
*/
class RegistrationOpeningEmailService
{
public const TEMPLATE_KEY = 'registration_opening';
+14 -3
View File
@@ -753,8 +753,9 @@ final class SchoolYearClosingService
];
if ($this->db->fieldExists('description', 'invoices')) {
$label = $amount > 0 ? 'Balance carried over' : 'Credit carried over';
$payload['description'] = "{$label} from previous school year {$sourceYear}.";
$payload['description'] = $amount >= 0
? "Carry over balance from last year {$sourceYear}"
: "Credit carry over from last year {$sourceYear}";
}
$invoiceModel = new InvoiceModel();
@@ -763,7 +764,17 @@ final class SchoolYearClosingService
throw new RuntimeException('Unable to create carry-forward invoice: ' . json_encode($invoiceModel->errors()));
}
return (int) $invoiceId;
$invoiceId = (int) $invoiceId;
$description = (string) ($payload['description'] ?? "Carry over balance from last year {$sourceYear}");
try {
$ledgerService = new \App\Libraries\InvoiceLedgerService();
$ledgerService->issueCarryForwardInvoiceLine($invoiceId, $amount, $description);
$ledgerService->recalculateInvoice($invoiceId);
} catch (\Throwable $e) {
log_message('error', 'Carry-forward invoice line issuance failed for invoice ' . $invoiceId . ': ' . $e->getMessage());
}
return $invoiceId;
}
private function carryForwardInvoiceNumber(int $itemId, int $parentId, string $sourceYear, string $targetYear): string