fix school year for all tables
Tests / PHPUnit (push) Failing after 1m20s

This commit is contained in:
root
2026-07-18 14:45:38 -04:00
parent 4db8334a3c
commit 716cc8b8d3
125 changed files with 2315 additions and 81 deletions
@@ -14,11 +14,13 @@ class SchoolYearClosingController extends BaseController
$targetId = $this->normalizeInt($this->request->getGet('target_school_year_id'));
$preview = service('schoolYearClosing')->preview($id, $targetId);
$promotionTable = $this->promotionTablePayload($preview['promotion']['rows'] ?? []);
$latestBatch = service('schoolYearClosing')->latestBatch($id);
return view('school_years/closing_preview', [
'preview' => $preview,
'promotionTable' => $promotionTable,
'latestBatch' => service('schoolYearClosing')->latestBatch($id),
'latestBatch' => $latestBatch,
'missingCarryForwardInvoices' => $this->missingCarryForwardInvoiceCount($latestBatch),
'schoolYears' => (new SchoolYearModel())->orderBy('name', 'DESC')->findAll(),
]);
} catch (Throwable $e) {
@@ -26,6 +28,23 @@ class SchoolYearClosingController extends BaseController
}
}
public function previewByName(string $name)
{
try {
$year = (new SchoolYearModel())
->where('name', rawurldecode($name))
->first();
if ($year === null) {
return redirect()->to('/administrator/school-years')->with('error', 'School year was not found.');
}
return $this->preview((int) $year['id']);
} catch (Throwable $e) {
return redirect()->to('/administrator/school-years')->with('error', $e->getMessage());
}
}
public function start(int $id)
{
try {
@@ -76,6 +95,26 @@ class SchoolYearClosingController extends BaseController
return is_numeric($value) && (int) $value > 0 ? (int) $value : null;
}
private function missingCarryForwardInvoiceCount(?array $batch): int
{
if ($batch === null || ! in_array((string) ($batch['status'] ?? ''), ['executed', 'completed'], true)) {
return 0;
}
$db = \Config\Database::connect();
if (! $db->tableExists('school_year_closing_items')) {
return 0;
}
return $db->table('school_year_closing_items')
->where('closing_batch_id', (int) $batch['id'])
->groupStart()
->where('target_invoice_id', null)
->orWhere('target_invoice_id', 0)
->groupEnd()
->countAllResults();
}
private function promotionTablePayload(array $rows): array
{
$allowedSorts = ['student', 'school_id', 'class', 'year_score', 'decision', 'source', 'queue', 'target', 'status'];
@@ -860,8 +860,13 @@ class LandingPageController extends BaseController
->get()
->getResultArray();
// Fetch latest invoice balance
$paymentBalance = $this->invoiceModel->getLatestInvoiceTotalAmount($parentId);
$paymentRow = $this->db->table('invoices')
->select('COALESCE(SUM(balance), 0) AS account_balance')
->where('parent_id', $parentId)
->where('school_year', $this->schoolYear)
->get()
->getRowArray();
$paymentBalance = (float) ($paymentRow['account_balance'] ?? 0);
// Pass data to the view, including the deadlines
return view('/landing_page/parent_dashboard', [
@@ -297,6 +297,8 @@ class PaymentController extends ResourceController
$payments = [];
$invoices = [];
$pager = null;
$carryForwardPaymentRequired = false;
$carryForwardPaymentMessage = '';
// Read the search term (email or phone)
$searchTerm = trim((string) $this->request->getGet('search_term'));
@@ -351,6 +353,10 @@ class PaymentController extends ResourceController
if ($parent && !empty($parent['id'])) {
$parentData = $parent;
$parentId = (int) $parent['id'];
$carryForwardPaymentRequired = $this->parentHasActiveCarryForwardBalance($parentId, $this->schoolYear);
if ($carryForwardPaymentRequired) {
$carryForwardPaymentMessage = 'This parent has a balance carried over from a previous school year. Manual payments must be paid in full; installments are not allowed.';
}
// Students
$students = $this->studentModel
@@ -461,6 +467,8 @@ class PaymentController extends ResourceController
'searchTermUsedInSearch' => $searchTerm,
'todayYmd' => utc_now(),
'installmentEndYmd' => $installmentEndYmd, // SELECT will use this in data-end-date
'carryForwardPaymentRequired' => $carryForwardPaymentRequired,
'carryForwardPaymentMessage' => $carryForwardPaymentMessage,
]);
}
@@ -952,6 +960,15 @@ class PaymentController extends ResourceController
// Recompute invoice totals from tuition + events + additional charges
$currentBalance = (float) $this->invoiceLedgerService->recalculateInvoice($invoiceId)['balance'];
$carryForwardPaymentRequired = $this->parentHasActiveCarryForwardBalance($parentId, $invYear);
if ($carryForwardPaymentRequired && $paymentType === 'installment') {
$this->db->transRollback();
return redirect()->back()->withInput()->with(
'error',
'This parent has a balance carried over from a previous school year. Installments are not allowed; payment must be made in full.'
);
}
if ($amount > $currentBalance + 0.00001) {
$this->db->transRollback();
return redirect()->back()->withInput()->with(
@@ -960,6 +977,14 @@ class PaymentController extends ResourceController
);
}
if ($carryForwardPaymentRequired && (float)round($amount, 2) !== (float)round($currentBalance, 2)) {
$this->db->transRollback();
return redirect()->back()->withInput()->with(
'error',
'This parent has a balance carried over from a previous school year. Payment must equal the full remaining balance (' . number_format($currentBalance, 2) . ').'
);
}
if ($paymentMethod === 'card' && (float)round($amount, 2) !== (float)round($currentBalance, 2)) {
$this->db->transRollback();
return redirect()->back()->withInput()->with(
@@ -1158,6 +1183,36 @@ class PaymentController extends ResourceController
}
}
private function parentHasActiveCarryForwardBalance(int $parentId, ?string $schoolYear = null): bool
{
if ($parentId <= 0 || ! $this->db->tableExists('invoices')) {
return false;
}
$builder = $this->db->table('invoices')
->where('parent_id', $parentId)
->where('balance >', 0);
if ($schoolYear !== null && $schoolYear !== '') {
$builder->where('school_year', $schoolYear);
}
$builder->groupStart()
->like('invoice_number', 'CF-', 'after')
->orWhere('semester', 'Opening Balance');
if ($this->db->fieldExists('description', 'invoices')) {
$builder
->orLike('description', 'carried over')
->orLike('description', 'carry-forward')
->orLike('description', 'previous school year');
}
$builder->groupEnd();
return $builder->countAllResults() > 0;
}
/**
* 🔄 Helper: Recalculate invoice totals and status based on all payments for current school year