fix installment for carry over balance parent account
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 48s
Tests / PHPUnit (push) Successful in 1m19s

This commit is contained in:
root
2026-08-29 18:33:28 -04:00
parent 611a5c8e4b
commit 6cf3a607a4
13 changed files with 477 additions and 57 deletions
@@ -161,6 +161,7 @@ if (!function_exists('enrollment_admin_rule_label')) {
'SIBLING_LAST_NAME_MISMATCH' => 'Sibling last names do not match',
'OUTSTANDING_BALANCE_BLOCKED' => 'Previous-year balance must be paid',
'FINANCE_APPROVAL_REQUIRED' => 'Finance approval required',
'CURRENT_YEAR_INSTALLMENT_OVERRIDE' => 'Allow installments for new-year balance only',
'AGE_RULE_BLOCKED' => 'Age rule not met',
'REGISTRATION_CLOSED' => 'Registration is closed',
'REGISTRATION_NOT_OPEN' => 'Registration is not open yet',
@@ -427,6 +428,14 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<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>
<?php if ($flagTypeValue === 'FINANCIAL_REVIEW_REQUIRED'): ?>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="allow_current_year_installments" id="allow_current_year_installments_<?= $flagId ?>" value="1">
<label class="form-check-label small" for="allow_current_year_installments_<?= $flagId ?>">
<?= esc(enrollment_admin_rule_label('CURRENT_YEAR_INSTALLMENT_OVERRIDE')) ?>
</label>
</div>
<?php endif; ?>
<input class="form-control form-control-sm mb-2" name="reason" placeholder="Approval reason" required>
<button class="btn btn-sm btn-warning" type="submit">Approve exception</button>
</form>
@@ -516,17 +525,13 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
$warningCodes = array_values(array_filter(array_map('strval', $evaluation['warning_rule_codes'] ?? [])));
$failedCodes = array_values(array_unique(array_merge($blockingCodes, $reviewCodes)));
$overridableCodes = array_values(array_diff($failedCodes, $nonOverridableCodes));
$hasGrantableStudent = $hasGrantableStudent || $overridableCodes !== [];
$hasGrantableStudent = true;
$decision = (string) ($evaluation['decision'] ?? '');
$canEnroll = !empty($evaluation['can_enroll']);
?>
<tr>
<td>
<?php if ($overridableCodes !== []): ?>
<input class="form-check-input" type="checkbox" name="student_ids[]" value="<?= $studentId ?>">
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
<input class="form-check-input" type="checkbox" name="student_ids[]" value="<?= $studentId ?>">
</td>
<td>
<?= esc($studentPreview['student_name'] ?? '') ?>
@@ -550,9 +555,13 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
</label>
</div>
<?php endforeach; ?>
<?php else: ?>
<span class="text-muted small">None</span>
<?php endif; ?>
<div class="form-check mt-1">
<input class="form-check-input" type="checkbox" name="bypassed_rule_codes_by_student[<?= $studentId ?>][]" id="bypass_<?= $studentId ?>_current_year_installments" value="CURRENT_YEAR_INSTALLMENT_OVERRIDE">
<label class="form-check-label small" for="bypass_<?= $studentId ?>_current_year_installments">
<?= esc(enrollment_admin_rule_label('CURRENT_YEAR_INSTALLMENT_OVERRIDE')) ?>
</label>
</div>
</td>
<td>
<?php if ($warningCodes !== []): ?>
+1 -1
View File
@@ -156,7 +156,7 @@
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="5" class="text-center">No students found.</td>
<td colspan="8" class="text-center">No students found.</td>
</tr>
<?php endif; ?>
</tbody>
+16 -7
View File
@@ -423,6 +423,7 @@
data-display-total="<?= esc($invoice['display_total'] ?? $invoice['total_amount'] ?? '') ?>"
data-balance-due-cents="<?= (int)($invoice['balance_due_cents'] ?? 0) ?>"
data-customer-credit-cents="<?= (int)($invoice['customer_credit_cents'] ?? 0) ?>"
data-carry-forward-invoice="<?= !empty($invoice['is_carry_forward_invoice']) ? '1' : '0' ?>"
data-next-installment="<?= (int)($invoice['next_installment'] ?? 1) ?>">
<?php
$uiPaid = (float)($invoice['paid_amount'] ?? 0);
@@ -634,6 +635,11 @@
return isFinite(v) ? v : 1;
}
function selectedInvoiceRequiresCarryForwardFull() {
const opt = currentOpt();
return carryForwardFullRequired || (opt && opt.getAttribute('data-carry-forward-invoice') === '1');
}
function monthsUntil(endYmd) {
if (!endYmd) return 0;
const today = new Date();
@@ -788,12 +794,15 @@
if (m === 'card') {
forceCardRules();
} else if (carryForwardFullRequired) {
} else if (selectedInvoiceRequiresCarryForwardFull()) {
forceCarryForwardFullRules();
} else {
// enable full/installment select
$type().removeAttribute('disabled');
$type().value = 'full';
const type = $type();
type.removeAttribute('disabled');
const installmentOption = type.querySelector('option[value="installment"]');
if (installmentOption) installmentOption.disabled = false;
type.value = 'full';
$instSec().style.display = 'none';
$instSeqRow().style.display = 'none';
$amount().removeAttribute('readonly');
@@ -810,7 +819,7 @@
forceCardRules();
return;
}
if (carryForwardFullRequired) {
if (selectedInvoiceRequiresCarryForwardFull()) {
forceCarryForwardFullRules();
return;
}
@@ -834,7 +843,7 @@
if (($method().value || '').toLowerCase() === 'card') {
forceCardRules();
} else if (carryForwardFullRequired) {
} else if (selectedInvoiceRequiresCarryForwardFull()) {
forceCarryForwardFullRules();
} else {
updateAmountHint();
@@ -908,7 +917,7 @@
alert('Please enter a valid amount > 0.');
return;
}
if (carryForwardFullRequired && type === 'installment') {
if (selectedInvoiceRequiresCarryForwardFull() && type === 'installment') {
alert(carryForwardFullMessage);
return;
}
@@ -924,7 +933,7 @@
const newBal = (isFinite(balance) && isFinite(amount)) ? (balance - amount) : NaN;
const overpay = (isFinite(newBal) && newBal < -0.005);
if (carryForwardFullRequired && isFinite(balance) && Math.abs(amount - balance) > 0.005) {
if (selectedInvoiceRequiresCarryForwardFull() && isFinite(balance) && Math.abs(amount - balance) > 0.005) {
alert(carryForwardFullMessage);
$amount().value = Math.max(0, balance).toFixed(2);
return;
+54 -20
View File
@@ -60,21 +60,47 @@
<th>Second Parent</th>
<th>Phone</th>
<!--th>Email</th-->
<th>
In Group (Second)
<div class="form-check d-inline-block ms-2">
<input class="form-check-input" type="checkbox" id="selectAllSecond<?= $index ?>" title="Set all second parents: checked=Yes, unchecked=No">
<label for="selectAllSecond<?= $index ?>" class="form-check-label small">All</label>
</div>
</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($classSection['parents'] as $rIdx => $p): ?>
<?php $formId = 'mform-' . $index . '-' . $rIdx; ?>
<tr>
<td><?= esc($p['primary_name']) ?></td>
<th>
In Group (Second)
<div class="form-check d-inline-block ms-2">
<input class="form-check-input" type="checkbox" id="selectAllSecond<?= $index ?>" title="Set all second parents: checked=Yes, unchecked=No">
<label for="selectAllSecond<?= $index ?>" class="form-check-label small">All</label>
</div>
</th>
<th>Delivery</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($classSection['parents'] as $rIdx => $p): ?>
<?php $formId = 'mform-' . $index . '-' . $rIdx; ?>
<?php
$deliveryStatus = strtolower((string)($p['delivery_status'] ?? 'not_sent'));
$deliveryLabel = 'Not sent';
$deliveryClass = 'bg-secondary';
if ($deliveryStatus === 'sent') {
$deliveryLabel = 'Sent';
$deliveryClass = 'bg-success';
} elseif ($deliveryStatus === 'failed') {
$deliveryLabel = 'Failed';
$deliveryClass = 'bg-danger';
}
$deliveryTitleParts = [];
if (!empty($p['delivery_sent_at'])) {
$deliveryTitleParts[] = 'Last attempt: ' . $p['delivery_sent_at'];
}
if (!empty($p['delivery_error'])) {
$deliveryTitleParts[] = 'Error: ' . $p['delivery_error'];
}
if (!empty($p['delivery_class_specific']) && $deliveryStatus !== 'not_sent') {
$deliveryTitleParts[] = 'Class-specific log';
} elseif ($deliveryStatus !== 'not_sent') {
$deliveryTitleParts[] = 'Parent-level log';
}
$deliveryTitle = implode(' | ', $deliveryTitleParts);
?>
<tr>
<td><?= esc($p['primary_name']) ?></td>
<td>
<?php if (!empty($p['primary_phone'])): ?>
<a href="tel:<?= esc(preg_replace('/\D+/', '', $p['primary_phone'])) ?>">
@@ -131,11 +157,19 @@
</select>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</td>
<td class="text-center">
<form id="<?= $formId ?>" method="post" action="<?= site_url('whatsapp/update-membership') ?>" class="d-inline wa-membership-form">
<?= csrf_field() ?>
<?php endif; ?>
</td>
<td class="text-center">
<span class="badge <?= esc($deliveryClass) ?>" title="<?= esc($deliveryTitle) ?>">
<?= esc($deliveryLabel) ?>
</span>
<?php if (!empty($p['delivery_sent_at'])): ?>
<div class="small text-muted"><?= esc($p['delivery_sent_at']) ?></div>
<?php endif; ?>
</td>
<td class="text-center">
<form id="<?= $formId ?>" method="post" action="<?= site_url('whatsapp/update-membership') ?>" class="d-inline wa-membership-form">
<?= csrf_field() ?>
<input type="hidden" name="class_section_id" value="<?= (int)($p['class_section_id'] ?? $classSection['class_section_id']) ?>">
<input type="hidden" name="school_year" value="<?= esc($classSection['school_year'] ?? '') ?>">
<input type="hidden" name="semester" value="<?= esc($classSection['semester'] ?? '') ?>">