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
@@ -48,6 +48,18 @@
white-space: normal;
text-align: left;
}
.enrollment-admin .issue-badge.adult-student,
.enrollment-admin .adult-student-name {
background-color: #b02a37;
color: #fff;
}
.enrollment-admin .adult-student-name {
display: inline-block;
border-radius: .25rem;
font-weight: 600;
padding: .15rem .35rem;
margin: .1rem .15rem .1rem 0;
}
</style>
<?= $this->endSection() ?>
@@ -62,12 +74,11 @@ if (!function_exists('enrollment_admin_flag_label')) {
'AGE_EXCEPTION_REQUIRED' => 'Age exception',
'LATE_REGISTRATION_EXCEPTION' => 'Late registration',
'FINANCIAL_REVIEW_REQUIRED' => 'Finance review',
'CLASS_CAPACITY_EXCEPTION_REQUIRED' => 'Class capacity',
'DEFERRED_DELIBERATION' => 'Decision review',
'RESTRICTED_ADMINISTRATIVE_REVIEW' => 'Restricted review',
'WITHDRAWAL_REVIEW_REQUIRED' => 'Withdrawal review',
'COMPLETION_OR_EXIT_PROCESS_REQUIRED' => 'Exit / completion',
'ADULT_STUDENT_ACTION_REQUIRED' => 'Adult student',
'ADULT_STUDENT_ACTION_REQUIRED', 'ADULT_STUDENT_PARENT_BLOCKED' => 'Adult student',
'SIBLING_LAST_NAME_MISMATCH' => 'Sibling last names',
'NO_FINAL_DECISION' => 'No final decision',
'UNRECOGNIZED_DECISION' => 'Unrecognized decision',
@@ -110,8 +121,9 @@ if (!function_exists('enrollment_admin_issue_badge_class')) {
return match (strtoupper($code)) {
'NO_FINAL_DECISION', 'UNRECOGNIZED_DECISION', 'EXPELLED', 'DENIED', 'RESTRICTED_ADMINISTRATIVE_REVIEW' => 'bg-danger',
'DEFERRED_DECISION', 'DEFERRED_DELIBERATION', 'FINANCIAL_REVIEW_REQUIRED', 'OUTSTANDING_BALANCE_BLOCKED', 'FINANCE_APPROVAL_REQUIRED' => 'bg-warning text-dark',
'PENDING_MAKE_UP_EXAM_PROMOTION', 'MAKE_UP_EXAM', 'AGE_EXCEPTION_REQUIRED', 'ADULT_STUDENT_ACTION_REQUIRED' => 'bg-info text-dark',
'CLASS_REASSIGNMENT_REQUIRED', 'CLASS_CAPACITY_EXCEPTION_REQUIRED' => 'bg-primary',
'ADULT_STUDENT_ACTION_REQUIRED', 'ADULT_STUDENT_PARENT_BLOCKED' => 'adult-student',
'PENDING_MAKE_UP_EXAM_PROMOTION', 'MAKE_UP_EXAM', 'AGE_EXCEPTION_REQUIRED' => 'bg-info text-dark',
'CLASS_REASSIGNMENT_REQUIRED' => 'bg-primary',
'WITHDRAWN', 'WITHDRAWAL_REVIEW_REQUIRED', 'COMPLETION_OR_EXIT_PROCESS_REQUIRED' => 'bg-dark',
'SIBLING_LAST_NAME_MISMATCH', 'LATE_REGISTRATION_EXCEPTION' => 'bg-secondary',
default => 'bg-secondary',
@@ -147,7 +159,7 @@ if (!function_exists('enrollment_admin_rule_label')) {
'AGE_RULE_BLOCKED' => 'Age rule not met',
'REGISTRATION_CLOSED' => 'Registration is closed',
'REGISTRATION_NOT_OPEN' => 'Registration is not open yet',
'ADULT_STUDENT_PARENT_BLOCKED' => 'Adult student cannot be enrolled by a parent',
'ADULT_STUDENT_PARENT_BLOCKED' => 'Adult students cannot enroll or re-enroll at the school',
'EXIT_REQUIRED' => 'Exit / completion required',
'EXPELLED' => 'Expelled — administrative review',
'WITHDRAWN' => 'Withdrawn — review required',
@@ -405,7 +417,7 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<input class="form-control form-control-sm mb-2" name="resolution_notes" placeholder="Resolution notes" required>
<button class="btn btn-sm btn-primary" type="submit">Save exam result</button>
</form>
<?php elseif (in_array($flagTypeValue, ['AGE_EXCEPTION_REQUIRED', 'LATE_REGISTRATION_EXCEPTION', 'FINANCIAL_REVIEW_REQUIRED', 'CLASS_CAPACITY_EXCEPTION_REQUIRED', 'SIBLING_LAST_NAME_MISMATCH', 'DEFERRED_DELIBERATION', 'WITHDRAWAL_REVIEW_REQUIRED', 'RESTRICTED_ADMINISTRATIVE_REVIEW'], true) && !empty($canManageEnrollmentExceptions)): ?>
<?php elseif (in_array($flagTypeValue, ['AGE_EXCEPTION_REQUIRED', 'LATE_REGISTRATION_EXCEPTION', 'FINANCIAL_REVIEW_REQUIRED', 'SIBLING_LAST_NAME_MISMATCH', 'DEFERRED_DELIBERATION', 'WITHDRAWAL_REVIEW_REQUIRED', 'RESTRICTED_ADMINISTRATIVE_REVIEW'], true) && !empty($canManageEnrollmentExceptions)): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/approve-exception') ?>" class="mb-2">
<?= csrf_field() ?>
<div class="small fw-semibold mb-1">Approve an exception for this student</div>
@@ -528,7 +540,7 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<div class="form-check">
<input class="form-check-input" type="checkbox" name="bypassed_rule_codes_by_student[<?= $studentId ?>][]" id="bypass_<?= $studentId ?>_<?= esc($code) ?>" value="<?= esc($code) ?>" <?= $isBypassable ? 'checked' : 'disabled' ?>>
<label class="form-check-label small" for="bypass_<?= $studentId ?>_<?= esc($code) ?>">
<?= esc(enrollment_admin_rule_label($code)) ?><?= $isBypassable ? '' : ' (cannot bypass)' ?>
<?= esc(enrollment_admin_rule_label($code)) ?><?= $isBypassable ? '' : ' ' ?>
</label>
</div>
<?php endforeach; ?>
@@ -562,9 +574,8 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<?php endforeach; ?>
</select>
</div>
<div class="col-md-7">
<label class="form-label" for="expires_at">Expires</label>
<input class="form-control form-control-sm" id="expires_at" name="expires_at" type="datetime-local">
<div class="col-md-7 d-flex align-items-end">
<div class="form-text mb-2">Applies for the selected school year (<?= esc($schoolYear ?? '') ?>).</div>
</div>
<div class="col-12">
<label class="form-label" for="reason_note">Approval note</label>
@@ -601,7 +612,7 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<th>Reason</th>
<th>Bypassed rules</th>
<th>Created by</th>
<th>Expires</th>
<th>School year</th>
<th class="no-sort">Action</th>
</tr>
</thead>
@@ -633,7 +644,7 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
</td>
<td><?= esc($bypassedLabels !== [] ? implode(', ', $bypassedLabels) : 'None') ?></td>
<td><?= esc($exception['created_by_name'] ?? '') ?></td>
<td><?= esc(!empty($exception['expires_at']) ? local_datetime($exception['expires_at'], 'm-d-Y H:i') : 'No expiry') ?></td>
<td><?= esc($exception['school_year'] ?? ($schoolYear ?? '')) ?></td>
<td>
<?php if ($exceptionStatus === 'active' && !empty($canManageEnrollmentExceptions)): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/exceptions/' . $exceptionId . '/revoke') ?>">
@@ -751,7 +762,20 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<tr>
<td><?= esc($example['parent_name'] ?? '') ?></td>
<td><?= esc(implode(', ', $example['recipients'] ?? [])) ?></td>
<td><?= esc(implode(', ', $example['student_names'] ?? [])) ?></td>
<td>
<?php if (!empty($example['students']) && is_array($example['students'])): ?>
<?php foreach ($example['students'] as $studentIndex => $student): ?>
<?= $studentIndex > 0 ? ', ' : '' ?>
<?php if (!empty($student['adult_student'])): ?>
<span class="adult-student-name"><?= esc($student['name'] ?? '') ?></span>
<?php else: ?>
<span><?= esc($student['name'] ?? '') ?></span>
<?php endif; ?>
<?php endforeach; ?>
<?php else: ?>
<?= esc(implode(', ', $example['student_names'] ?? [])) ?>
<?php endif; ?>
</td>
<td><?= esc($example['subject'] ?? '') ?></td>
<td>
<span class="badge <?= esc($badgeClass) ?>"><?= esc($deliveryLabel) ?></span>
+180 -16
View File
@@ -9,22 +9,51 @@
<?php endif; ?>
<div class="border rounded p-3 mb-3">
<div><strong>Parent:</strong> <?= esc(trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? ''))) ?> <?= esc($parent['email'] ?? '') ?></div>
<div><strong>School year:</strong> <?= esc($requestRow['school_year'] ?? '') ?></div>
<div><strong>Status:</strong> <?= esc($requestRow['status'] ?? '') ?></div>
<div><strong>Household size:</strong> <?= esc((string) ($requestRow['household_size'] ?? 'Not provided')) ?></div>
<div><strong>Requested amount:</strong> <?= $requestRow['requested_amount'] !== null && $requestRow['requested_amount'] !== '' ? '$' . number_format((float) $requestRow['requested_amount'], 2) : 'Not specified' ?></div>
<div class="mt-2"><strong>Need statement</strong></div>
<p><?= nl2br(esc($requestRow['need_statement'] ?? '')) ?></p>
<div><strong>Students</strong></div>
<ul>
<?php foreach (($students ?? []) as $student): ?>
<li>
<?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?>
<?= student_enrollment_status_button($student, $schoolYear ?? null) ?>
</li>
<?php endforeach; ?>
</ul>
<div class="row g-4">
<div class="col-md-6">
<div><strong>Parent:</strong> <?= esc(trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? ''))) ?></div>
<div><strong>Household size:</strong> <?= esc((string) ($requestRow['household_size'] ?? 'Not provided')) ?></div>
<div><strong>Requested amount:</strong> <?= $requestRow['requested_amount'] !== null && $requestRow['requested_amount'] !== '' ? '$' . number_format((float) $requestRow['requested_amount'], 2) : 'Not specified' ?></div>
<div class="mt-2"><strong>Parent statement</strong></div>
<p class="mb-2"><?= nl2br(esc($requestRow['need_statement'] ?? '')) ?></p>
<div><strong>Students</strong></div>
<ul class="mb-0">
<?php foreach (($students ?? []) as $student): ?>
<li>
<?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?>
<?= student_enrollment_status_button($student, $schoolYear ?? null) ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<div class="col-md-6">
<h5 class="mb-3">Aid estimate calculator</h5>
<div class="mb-3">
<label class="form-label" for="calc_household_income">Household income</label>
<input class="form-control" type="number" min="0" step="0.01" id="calc_household_income"
value="<?= esc((string) ($requestRow['household_income'] ?? '0')) ?>">
</div>
<div class="mb-3">
<label class="form-label" for="calc_household_size">Household size</label>
<input class="form-control" type="number" min="1" id="calc_household_size"
value="<?= esc((string) max(1, (int) ($requestRow['household_size'] ?? 1))) ?>">
</div>
<div class="mb-3">
<label class="form-label" for="calc_invoice_balance">Invoice balance</label>
<input class="form-control" type="number" min="0" step="0.01" id="calc_invoice_balance"
value="<?= esc(number_format((float) ($invoiceBalance ?? 0), 2, '.', '')) ?>" readonly>
</div>
<button type="button" class="btn btn-outline-primary mb-3" id="calcFinancialAidBtn">Calculate / Recalculate</button>
<div id="calcFinancialAidError" class="alert alert-danger d-none py-2"></div>
<div id="calcFinancialAidSuccess" class="alert alert-success d-none py-2">Financial aid calculation completed.</div>
<div class="border rounded p-3 bg-light">
<div class="small text-muted">Estimated aid</div>
<div class="fs-4 fw-semibold" id="calcEstimatedAid">$<?= number_format((float) ($estimatedAid ?? 0), 2) ?></div>
<div class="small text-muted mt-2">Capped by request and balance</div>
<div class="fw-semibold" id="calcFinalRebate">$<?= number_format((float) ($finalRebate ?? 0), 2) ?></div>
</div>
</div>
</div>
</div>
<?php if (in_array((string) ($requestRow['status'] ?? ''), ['submitted', 'under_review'], true)): ?>
@@ -53,3 +82,138 @@
<?php endif; ?>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
(function () {
const btn = document.getElementById('calcFinancialAidBtn');
const errorBox = document.getElementById('calcFinancialAidError');
const successBox = document.getElementById('calcFinancialAidSuccess');
const estimatedEl = document.getElementById('calcEstimatedAid');
const finalEl = document.getElementById('calcFinalRebate');
const csrfName = <?= json_encode(csrf_token()) ?>;
const csrfHeaderName = <?= json_encode(csrf_header()) ?>;
let csrfHash = <?= json_encode(csrf_hash()) ?>;
const csrfCookieNames = <?= json_encode(array_values(array_unique(array_filter([
config('Security')->cookieName ?? null,
config('Security')->csrfCookieName ?? null,
'csrf_cookie_name',
])))) ?>;
function fmtUSD(value) {
return '$' + Number(value || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function getCookie(name) {
return document.cookie.split(';')
.map((value) => value.trim())
.find((value) => value.startsWith(name + '='))?.split('=')[1] || '';
}
function refreshCsrfFromCookie() {
for (const cookieName of csrfCookieNames) {
const value = decodeURIComponent(getCookie(cookieName) || '');
if (value) {
csrfHash = value;
syncCsrfFields();
return;
}
}
}
function syncCsrfFields() {
document.querySelectorAll('input[name="' + csrfName + '"]').forEach((input) => {
input.value = csrfHash;
});
}
function captureCsrf(resp, data) {
const headerValue = resp.headers.get(csrfHeaderName) || resp.headers.get('X-CSRF-HASH') || '';
if (headerValue) {
csrfHash = headerValue;
syncCsrfFields();
return;
}
if (data?.csrf_hash) {
csrfHash = data.csrf_hash;
syncCsrfFields();
return;
}
if (data?.csrfHash) {
csrfHash = data.csrfHash;
syncCsrfFields();
return;
}
refreshCsrfFromCookie();
}
document.querySelectorAll(
'form[action$="/approve"], form[action$="/deny"]'
).forEach((form) => {
form.addEventListener('submit', () => {
refreshCsrfFromCookie();
syncCsrfFields();
});
});
btn?.addEventListener('click', async function () {
if (errorBox) {
errorBox.classList.add('d-none');
errorBox.textContent = '';
}
if (successBox) {
successBox.classList.add('d-none');
}
refreshCsrfFromCookie();
const body = new URLSearchParams();
body.set('household_income', document.getElementById('calc_household_income')?.value ?? '');
body.set('household_size', document.getElementById('calc_household_size')?.value ?? '');
body.set(csrfName, csrfHash);
try {
const resp = await fetch(<?= json_encode(site_url('administrator/financial-aid/' . (int) ($requestRow['id'] ?? 0) . '/estimate')) ?>, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
[csrfHeaderName]: csrfHash,
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
body: body.toString(),
});
const text = await resp.text();
let data = {};
try {
data = JSON.parse(text);
} catch (parseErr) {
throw new Error('Unable to calculate estimated aid.');
}
captureCsrf(resp, data);
if (!resp.ok) {
throw new Error(data.error || data.message || 'Unable to calculate estimated aid.');
}
if (estimatedEl) estimatedEl.textContent = fmtUSD(data.estimated_aid);
if (finalEl) finalEl.textContent = fmtUSD(data.final_rebate);
const balanceInput = document.getElementById('calc_invoice_balance');
if (balanceInput && data.invoice_balance !== undefined) {
balanceInput.value = Number(data.invoice_balance).toFixed(2);
}
if (successBox) {
successBox.textContent = 'Financial aid calculation completed.';
successBox.classList.remove('d-none');
}
} catch (err) {
if (errorBox) {
errorBox.textContent = err.message || 'Unable to calculate estimated aid.';
errorBox.classList.remove('d-none');
}
}
});
})();
</script>
<?= $this->endSection() ?>