fix enrollment, invoice, payment and financila aid
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user