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
+438 -230
View File
@@ -105,104 +105,11 @@ class InvoiceController extends ResourceController
log_message('info', "Selected school year for invoice retrieval: $schoolYear");
$invoiceData = [];
$parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $schoolYear);
$parents = $this->invoiceManagementParents($schoolYear);
foreach ($parents as $parent) {
$students = $this->studentModel->where('parent_id', $parent['id'])->findAll();
$parentData = [
'parent_name' => $parent['firstname'] . ' ' . $parent['lastname'],
'parent_id' => $parent['id'],
'enrolledKids' => [],
'withdrawnKids' => [],
'invoice_amount' => 0,
'refund_amount' => 0, // default
'last_updated' => null,
'invoice_date' => null
];
// Fetch most recent invoice
$invoices = $this->invoiceModel->getInvoicesByParentId($parent['id'], $schoolYear);
foreach ($invoices as $invoice) {
if ($invoice) {
$parentData['invoice_amount'] = $invoice['total_amount'];
$parentData['last_updated'] = $invoice['updated_at'];
// Prefer issue_date (UTC) and render in configured/user local time for display
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$parentData['invoice_date'] = !empty($invoice['issue_date'])
? (new \DateTimeImmutable($invoice['issue_date'], new \DateTimeZone('UTC')))
->setTimezone(new \DateTimeZone($tzName))
->format('Y-m-d H:i:s')
: ($invoice['updated_at'] ?? null);
$parentData['invoice_id'] = $invoice['id'];
$refundSummary = $this->paidRefundSummaryForParentYear((int) $parent['id'], (string) $schoolYear);
$parentData['refund_amount'] = $refundSummary['amount'];
$parentData['refund_details'] = $refundSummary['details'];
log_message('info', "Latest invoice for parent {$parent['firstname']} {$parent['lastname']} in school year $schoolYear: Amount = {$invoice['total_amount']}, Updated at = {$invoice['updated_at']}");
} else {
log_message('error', "No invoice found for parent {$parent['firstname']} {$parent['lastname']} in school year $schoolYear.");
}
}
foreach ($students as $student) {
$studentClass = $this->db->table('student_class')
->where('student_id', $student['id'])
->where('school_year', $schoolYear)
->get()->getRowArray();
$grade = 'N/A';
if ($studentClass && isset($studentClass['class_section_id'])) {
$classSection = $this->db->table('classSection')
->where('class_section_id', $studentClass['class_section_id'])
->get()->getRowArray();
if ($classSection && isset($classSection['class_section_name'])) {
$grade = $classSection['class_section_name'];
}
}
$enrollments = $this->enrollmentModel
->where('student_id', $student['id'])
->where('school_year', $schoolYear)
->findAll();
foreach ($enrollments as $enrollment) {
$kidData = [
'name' => $student['firstname'] . ' ' . $student['lastname'],
'grade' => $grade,
'tuition_fee' => $enrollment['tuition_fee'] ?? 0
];
switch ($enrollment['enrollment_status']) {
case 'payment pending':
case 'enrolled':
$parentData['enrolledKids'][] = $kidData;
break;
case 'withdraw under review':
case 'withdrawn':
case 'refund pending':
$parentData['withdrawnKids'][] = $kidData;
break;
case 'admission under review':
log_message('info', "Student ID {$student['id']} is under admission review and not included in the invoice.");
break;
case 'waitlist':
log_message('info', "Student ID {$student['id']} is in waitlist and not included in the invoice.");
break;
case 'denied':
log_message('info', "Student ID {$student['id']} is denied and not included in the invoice.");
break;
default:
log_message('error', "Unexpected enrollment status '{$enrollment['enrollment_status']}' for student ID {$student['id']}.");
break;
}
}
}
if (!empty($parentData['enrolledKids']) || !empty($parentData['withdrawnKids'])) {
$invoiceData[] = $parentData;
foreach ($this->invoiceManagementRowsForParent($parent, $schoolYear) as $row) {
$invoiceData[] = $row;
}
}
@@ -370,94 +277,11 @@ class InvoiceController extends ResourceController
$schoolYears = [$this->schoolYear];
}
$parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $schoolYear);
$parents = $this->invoiceManagementParents($schoolYear);
foreach ($parents as $parent) {
$students = $this->studentModel->where('parent_id', $parent['id'])->findAll();
$parentData = [
'parent_name' => trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? '')),
'parent_id' => (int)$parent['id'],
'enrolledKids' => [],
'withdrawnKids' => [],
'invoice_amount'=> 0,
'refund_amount' => 0,
'last_updated' => null,
'invoice_date' => null,
'invoice_id' => null,
];
// Latest invoice
$invoices = $this->invoiceModel->getInvoicesByParentId($parent['id'], $schoolYear);
foreach ($invoices as $invoice) {
if ($invoice) {
$parentData['invoice_amount'] = (float)($invoice['total_amount'] ?? 0);
$parentData['last_updated'] = $invoice['updated_at'] ?? null;
// Prefer issue_date (UTC) -> local; fall back to updated_at/created_at
if (!empty($invoice['issue_date'])) {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$parentData['invoice_date'] = (new \DateTimeImmutable($invoice['issue_date'], new \DateTimeZone('UTC')))
->setTimezone(new \DateTimeZone($tzName))
->format('Y-m-d H:i:s');
} else {
$parentData['invoice_date'] = date('Y-m-d H:i:s', strtotime($invoice['updated_at'] ?? $invoice['created_at'] ?? 'now'));
}
$parentData['invoice_id'] = $invoice['id'] ?? null;
$refundSummary = $this->paidRefundSummaryForParentYear((int) $parent['id'], (string) $schoolYear);
$parentData['refund_amount'] = $refundSummary['amount'];
$parentData['refund_details'] = $refundSummary['details'];
break; // only most recent as before
}
}
// Build kids lists based on enrollment statuses
foreach ($students as $student) {
$studentClass = $this->db->table('student_class')
->where('student_id', $student['id'])
->where('school_year', $schoolYear)
->get()->getRowArray();
$grade = 'N/A';
if ($studentClass && isset($studentClass['class_section_id'])) {
$classSection = $this->db->table('classSection')
->where('class_section_id', $studentClass['class_section_id'])
->get()->getRowArray();
if ($classSection && isset($classSection['class_section_name'])) {
$grade = $classSection['class_section_name'];
}
}
$enrollments = $this->enrollmentModel
->where('student_id', $student['id'])
->where('school_year', $schoolYear)
->findAll();
foreach ($enrollments as $enrollment) {
$kid = [
'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
'grade' => $grade,
'tuition_fee' => (float)($enrollment['tuition_fee'] ?? 0),
];
switch ($enrollment['enrollment_status']) {
case 'payment pending':
case 'enrolled':
$parentData['enrolledKids'][] = $kid;
break;
case 'withdraw under review':
case 'withdrawn':
case 'refund pending':
$parentData['withdrawnKids'][] = $kid;
break;
default:
// ignore others for invoice summary
break;
}
}
}
if (!empty($parentData['enrolledKids']) || !empty($parentData['withdrawnKids'])) {
$invoiceData[] = $parentData;
foreach ($this->invoiceManagementRowsForParent($parent, $schoolYear) as $row) {
$invoiceData[] = $row;
}
}
@@ -610,7 +434,9 @@ class InvoiceController extends ResourceController
$updated = false;
$updatedIds = [];
if (!empty($invoice) && isset($invoice['id'])) {
$ledger = $this->invoiceLedgerService->recalculate((int) $invoice['id']);
$invoiceId = (int) $invoice['id'];
$this->invoiceLedgerService->syncTuitionLines($invoiceId);
$ledger = $this->invoiceLedgerService->recalculate($invoiceId);
$updatedIds[] = (int) $ledger['invoice_id'];
log_message('info', "Updated invoice ID {$invoice['id']} for parent ID {$parentId}.");
$updated = true;
@@ -767,39 +593,273 @@ class InvoiceController extends ResourceController
return null;
}
// Prefer invoices flagged as having discounts.
$invoice = $this->invoiceModel
$invoices = $this->invoiceModel
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->where('has_discount', 1)
->orderBy('id', 'DESC')
->first();
if (!empty($invoice)) {
return $invoice;
->findAll();
$tuitionInvoices = [];
foreach ($invoices as $invoice) {
if ($this->invoiceLedgerService->invoiceIsCarryForward($invoice)) {
continue;
}
$tuitionInvoices[] = $invoice;
}
if ($tuitionInvoices === []) {
return null;
}
foreach ($tuitionInvoices as $invoice) {
if ((int) ($invoice['has_discount'] ?? 0) === 1) {
return $invoice;
}
}
// Fallback: prefer invoices with discount_usages rows.
try {
$row = $this->db->table('invoices i')
->select('i.*')
->join('discount_usages du', 'du.invoice_id = i.id', 'inner')
->where('i.parent_id', $parentId)
->where('i.school_year', $schoolYear)
->orderBy('i.id', 'DESC')
->get()
->getRowArray();
if (!empty($row)) {
return $row;
$tuitionInvoiceIds = array_values(array_filter(array_map(
static fn (array $row): int => (int) ($row['id'] ?? 0),
$tuitionInvoices
)));
if ($tuitionInvoiceIds !== []) {
$row = $this->db->table('invoices i')
->select('i.*')
->join('discount_usages du', 'du.invoice_id = i.id', 'inner')
->whereIn('i.id', $tuitionInvoiceIds)
->orderBy('i.id', 'DESC')
->get()
->getRowArray();
if (! empty($row)) {
return $row;
}
}
} catch (\Throwable $e) {
}
// Final fallback: latest invoice for parent/year.
return $this->invoiceModel
->where('parent_id', $parentId)
return $tuitionInvoices[0];
}
/**
* @return list<array<string, mixed>>
*/
private function invoiceManagementParents(string $schoolYear): array
{
$byId = [];
foreach ($this->userModel->getUsersByRoleAndSchoolYear('parent', $schoolYear) as $parent) {
$byId[(int) ($parent['id'] ?? 0)] = $parent;
}
$invoiceParentRows = $this->invoiceModel
->select('parent_id')
->distinct()
->where('school_year', $schoolYear)
->orderBy('id', 'DESC')
->first();
->findAll();
foreach ($invoiceParentRows as $row) {
$parentId = (int) ($row['parent_id'] ?? 0);
if ($parentId <= 0 || isset($byId[$parentId])) {
continue;
}
$parent = $this->userModel->find($parentId);
if ($parent !== null) {
$byId[$parentId] = $parent;
}
}
return array_values($byId);
}
/**
* @return list<array<string, mixed>>
*/
private function invoiceManagementRowsForParent(array $parent, string $schoolYear): array
{
$parentId = (int) ($parent['id'] ?? 0);
if ($parentId <= 0 || $schoolYear === '') {
return [];
}
$kids = $this->invoiceManagementKidsForParent($parentId, $schoolYear);
$enrolledKids = $kids['enrolledKids'];
$withdrawnKids = $kids['withdrawnKids'];
$rows = [];
foreach ($this->carryForwardInvoicesForParentYear($parentId, $schoolYear) as $carryForwardInvoice) {
$rows[] = $this->buildInvoiceManagementRow(
$parent,
$carryForwardInvoice,
true,
[],
[],
$this->paidRefundSummaryForInvoice((int) ($carryForwardInvoice['id'] ?? 0))
);
}
$tuitionInvoice = $this->selectActiveInvoiceForParentYear($parentId, $schoolYear);
if ($enrolledKids !== [] || $withdrawnKids !== [] || $tuitionInvoice !== null) {
$rows[] = $this->buildInvoiceManagementRow(
$parent,
$tuitionInvoice,
false,
$enrolledKids,
$withdrawnKids,
$this->paidRefundSummaryForParentYear($parentId, $schoolYear)
);
}
return $rows;
}
/**
* @return array{enrolledKids: list<array<string, mixed>>, withdrawnKids: list<array<string, mixed>>}
*/
private function invoiceManagementKidsForParent(int $parentId, string $schoolYear): array
{
$enrolledKids = [];
$withdrawnKids = [];
$students = $this->studentModel->where('parent_id', $parentId)->findAll();
foreach ($students as $student) {
$studentClass = $this->db->table('student_class')
->where('student_id', $student['id'])
->where('school_year', $schoolYear)
->get()
->getRowArray();
$grade = 'N/A';
if ($studentClass && isset($studentClass['class_section_id'])) {
$classSection = $this->db->table('classSection')
->where('class_section_id', $studentClass['class_section_id'])
->get()
->getRowArray();
if ($classSection && isset($classSection['class_section_name'])) {
$grade = $classSection['class_section_name'];
}
}
$enrollments = $this->enrollmentModel
->where('student_id', $student['id'])
->where('school_year', $schoolYear)
->findAll();
foreach ($enrollments as $enrollment) {
$kid = [
'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')),
'grade' => $grade,
'tuition_fee' => (float) ($enrollment['tuition_fee'] ?? 0),
];
switch ($enrollment['enrollment_status']) {
case 'payment pending':
case 'enrolled':
$enrolledKids[] = $kid;
break;
case 'withdraw under review':
case 'withdrawn':
case 'refund pending':
$withdrawnKids[] = $kid;
break;
}
}
}
return [
'enrolledKids' => $enrolledKids,
'withdrawnKids' => $withdrawnKids,
];
}
/**
* @return list<array<string, mixed>>
*/
private function carryForwardInvoicesForParentYear(int $parentId, string $schoolYear): array
{
if ($parentId <= 0 || $schoolYear === '') {
return [];
}
return array_values(array_filter(
$this->invoiceModel
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->orderBy('id', 'ASC')
->findAll(),
fn (array $invoice): bool => $this->invoiceLedgerService->invoiceIsCarryForward($invoice)
));
}
/**
* @param list<array<string, mixed>> $enrolledKids
* @param list<array<string, mixed>> $withdrawnKids
* @param array{amount: float, details: list<array<string, mixed>>} $refundSummary
* @return array<string, mixed>
*/
private function buildInvoiceManagementRow(
array $parent,
?array $invoice,
bool $isCarryForward,
array $enrolledKids,
array $withdrawnKids,
array $refundSummary
): array {
$parentId = (int) ($parent['id'] ?? 0);
$invoiceId = $invoice !== null ? (int) ($invoice['id'] ?? 0) : null;
$invoiceDate = null;
if ($invoice !== null) {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$invoiceDate = ! empty($invoice['issue_date'])
? (new \DateTimeImmutable($invoice['issue_date'], new \DateTimeZone('UTC')))
->setTimezone(new \DateTimeZone($tzName))
->format('Y-m-d H:i:s')
: ($invoice['updated_at'] ?? null);
}
$description = '';
if ($isCarryForward && $invoice !== null) {
$description = $this->invoiceLedgerService->carryForwardDisplayDescription($invoice);
}
return [
'parent_name' => trim((string) ($parent['firstname'] ?? '') . ' ' . (string) ($parent['lastname'] ?? '')),
'parent_id' => $parentId,
'enrolledKids' => $enrolledKids,
'withdrawnKids' => $withdrawnKids,
'invoice_amount' => $invoice !== null ? (float) ($invoice['total_amount'] ?? 0) : 0.0,
'invoice_balance' => $invoice !== null ? (float) ($invoice['balance'] ?? 0) : 0.0,
'refund_amount' => (float) ($refundSummary['amount'] ?? 0.0),
'refund_details' => $refundSummary['details'] ?? [],
'last_updated' => $invoice['updated_at'] ?? null,
'invoice_date' => $invoiceDate,
'invoice_id' => $invoiceId,
'invoice_number' => $invoice !== null ? (string) ($invoice['invoice_number'] ?? '') : '',
'invoice_description' => $description,
'invoice_status' => $invoice !== null ? (string) ($invoice['status'] ?? '') : '',
'is_carry_forward' => $isCarryForward,
];
}
/**
* @return array{amount: float, details: list<array<string, mixed>>}
*/
private function paidRefundSummaryForInvoice(int $invoiceId): array
{
if ($invoiceId <= 0) {
return ['amount' => 0.0, 'details' => []];
}
$details = $this->paidRefundDetailsForInvoice($invoiceId);
$amount = 0.0;
foreach ($details as $detail) {
$amount += (float) ($detail['amount'] ?? 0.0);
}
return [
'amount' => round($amount, 2),
'details' => $details,
];
}
private function recalculateAndUpdateDiscount(
@@ -854,11 +914,15 @@ class InvoiceController extends ResourceController
private function calculateTotalTuitionFee(array $students): float
{
$schoolYear = (string) ($students[0]['school_year'] ?? $this->schoolYear);
// 1) Normalize each student's grade name (once)
foreach ($students as &$student) {
$gradeName = $this->classSectionModel
->getClassSectionNameBySectionId($student['class_section_id']);
$student['grade'] = strtoupper(trim($gradeName));
$student['grade'] = $this->resolveStudentGradeName(
(int) ($student['student_id'] ?? 0),
$schoolYear,
$student['class_section_id'] ?? null
);
}
unset($student); // break reference
@@ -876,6 +940,29 @@ class InvoiceController extends ResourceController
return $total;
}
private function resolveStudentGradeName(int $studentId, string $schoolYear, $classSectionId = null): string
{
$sectionId = $classSectionId;
if (empty($sectionId) && $studentId > 0 && $schoolYear !== '') {
$row = $this->studentClassModel
->select('class_section_id')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('is_event_only', 0)
->orderBy('updated_at', 'DESC')
->first();
$sectionId = $row['class_section_id'] ?? null;
}
if (empty($sectionId)) {
return 'N/A';
}
$gradeName = $this->classSectionModel->getClassSectionNameBySectionId($sectionId);
return strtoupper(trim((string) $gradeName));
}
// Method to check and generate an invoice when enrollment status changes
public function checkAndGenerateInvoice($parentId, $status)
@@ -922,6 +1009,10 @@ class InvoiceController extends ResourceController
return ['error' => "No invoice was generated. Please contact the school administration."];
}
if ($this->invoiceLedgerService->invoiceIsCarryForward($invoice)) {
return $this->prepareCarryForwardInvoiceData($invoice);
}
$parentId = $invoice['parent_id'];
$schoolYear = $invoice['school_year'];
@@ -1141,6 +1232,8 @@ class InvoiceController extends ResourceController
return [
'invoice' => $invoice,
'parent' => $parent,
'isCarryForwardInvoice' => $this->invoiceLedgerService->invoiceIsCarryForward($invoice),
'carryForwardDescription'=> $this->invoiceLedgerService->carryForwardDisplayDescription($invoice),
'registeredKids' => $registeredKids,
'withdrawnKids' => $withdrawnKids,
'studentCharges' => $studentCharges,
@@ -1157,6 +1250,85 @@ class InvoiceController extends ResourceController
];
}
private function prepareCarryForwardInvoiceData(array $invoice): array
{
$invoiceId = (int) ($invoice['id'] ?? 0);
$parentId = (int) ($invoice['parent_id'] ?? 0);
$schoolYear = (string) ($invoice['school_year'] ?? '');
$parent = $this->userModel->find($parentId);
if (! $parent) {
return ['error' => 'Parent associated with the invoice was not found.'];
}
$ledger = $this->invoiceLedgerService->calculateInvoice($invoiceId);
$carryForwardDescription = $this->invoiceLedgerService->carryForwardDisplayDescription($invoice);
$invoice['description'] = $carryForwardDescription;
$carryForwardAmount = (float) ($ledger['total_amount'] ?? $invoice['total_amount'] ?? 0);
if (abs($carryForwardAmount) >= 0.01) {
try {
$this->invoiceLedgerService->issueCarryForwardInvoiceLine(
$invoiceId,
$carryForwardAmount,
$carryForwardDescription
);
$ledger = $this->invoiceLedgerService->calculateInvoice($invoiceId);
} catch (\Throwable $e) {
log_message('warning', 'Unable to normalize carry-forward invoice line for invoice ' . $invoiceId . ': ' . $e->getMessage());
}
}
$db = $this->db;
$table = $this->paymentModel->table;
$hasStatus = $db->fieldExists('status', $table);
$hasVoid = $db->fieldExists('is_void', $table);
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
$paymentQuery = $this->paymentModel
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->where('invoice_id', $invoiceId);
if ($hasStatus) {
$paymentQuery->groupStart()
->whereNotIn('status', $exclude)
->orWhere('status IS NULL', null, false)
->groupEnd();
}
if ($hasVoid) {
$paymentQuery->groupStart()
->where('is_void', 0)
->orWhere('is_void IS NULL', null, false)
->groupEnd();
}
$payments = $paymentQuery->findAll();
$refundsPaidTotal = (float) ($ledger['refund_paid_total'] ?? 0.0);
$refundDetails = $this->paidRefundDetailsForInvoice($invoiceId);
return [
'invoice' => $invoice,
'parent' => $parent,
'isCarryForwardInvoice' => true,
'carryForwardDescription' => $carryForwardDescription,
'registeredKids' => [],
'withdrawnKids' => [],
'studentCharges' => [],
'events' => [],
'students' => [],
'payments' => $payments,
'discounts' => [],
'additionalChargesTotal' => 0.0,
'additionalChargeLines' => [],
'invoiceLines' => [],
'refundsPaidTotal' => $refundsPaidTotal,
'refundDetails' => $refundDetails,
'ledger' => $ledger,
];
}
/**
* Treat common KG spellings as Kindergarten.
@@ -1339,25 +1511,30 @@ class InvoiceController extends ResourceController
}
$studentTuitionRows = [];
foreach (array_merge($registeredKids ?? [], $withdrawnKids ?? []) as $student) {
$sid = (int)($student['student_id'] ?? 0);
$charge = $studentCharges[$sid] ?? null;
$amount = (float)($charge['unit_fee'] ?? 0.0);
if ($sid <= 0 || abs($amount) < 0.00001) {
continue;
}
$isCarryForwardInvoice = (bool) ($data['isCarryForwardInvoice'] ?? $this->invoiceLedgerService->invoiceIsCarryForward($invoice));
$carryForwardDescription = (string) ($data['carryForwardDescription'] ?? $this->invoiceLedgerService->carryForwardDisplayDescription($invoice));
$name = trim((string)($student['student_firstname'] ?? '') . ' ' . (string)($student['student_lastname'] ?? ''));
$grade = trim((string)($student['grade'] ?? ''));
$desc = 'Tuition - ' . ($name !== '' ? $name : 'Student #' . $sid);
if ($grade !== '' && strtoupper($grade) !== 'N/A') {
$desc .= ' (' . $grade . ')';
}
if (! $isCarryForwardInvoice) {
foreach (array_merge($registeredKids ?? [], $withdrawnKids ?? []) as $student) {
$sid = (int)($student['student_id'] ?? 0);
$charge = $studentCharges[$sid] ?? null;
$amount = (float)($charge['unit_fee'] ?? 0.0);
if ($sid <= 0 || abs($amount) < 0.00001) {
continue;
}
$studentTuitionRows[] = [
'description' => $desc,
'amount' => $amount,
];
$name = trim((string)($student['student_firstname'] ?? '') . ' ' . (string)($student['student_lastname'] ?? ''));
$grade = trim((string)($student['grade'] ?? ''));
$desc = 'Tuition - ' . ($name !== '' ? $name : 'Student #' . $sid);
if ($grade !== '' && strtoupper($grade) !== 'N/A') {
$desc .= ' (' . $grade . ')';
}
$studentTuitionRows[] = [
'description' => $desc,
'amount' => $amount,
];
}
}
$eventRows = [];
@@ -1390,9 +1567,24 @@ class InvoiceController extends ResourceController
$dt = $toLocal($line['created_at'] ?? ($invoice['created_at'] ?? null), true);
$amount = ((int)($line['line_amount_cents'] ?? 0)) / 100;
$type = (string)($line['line_type'] ?? 'other');
$sourceType = (string)($line['source_type'] ?? '');
$category = str_contains($type, 'event') ? 'event'
: (str_contains($type, 'additional') ? 'additional' : 'registration');
if ($sourceType === 'carry_forward_invoice' || $sourceType === 'carry_forward_opening_balance') {
$lineDescription = trim((string)($line['description'] ?? ''));
if ($lineDescription === '') {
$lineDescription = $carryForwardDescription;
}
$push($dt, $lineDescription, $amount, 'additional');
continue;
}
if ($isCarryForwardInvoice) {
$push($dt, $carryForwardDescription, $amount, 'additional');
continue;
}
if (!str_contains($type, 'additional') && !str_contains($type, 'event') && !empty($studentTuitionRows)) {
$expandedTotal = 0.0;
foreach ($studentTuitionRows as $row) {
@@ -1426,11 +1618,18 @@ class InvoiceController extends ResourceController
if (empty($data['invoiceLines'] ?? [])) {
$fallbackDt = $toLocal($invoice['created_at'] ?? ($invoice['issue_date'] ?? null), true);
foreach ($studentTuitionRows as $row) {
$push($fallbackDt, $row['description'], (float)$row['amount'], 'registration');
}
foreach ($eventRows as $row) {
$push($fallbackDt, $row['description'], (float)$row['amount'], 'event');
if ($isCarryForwardInvoice) {
$carryForwardAmount = (float)($ledger['total_amount'] ?? $invoice['total_amount'] ?? 0);
if (abs($carryForwardAmount) >= 0.01) {
$push($fallbackDt, $carryForwardDescription, $carryForwardAmount, 'additional');
}
} else {
foreach ($studentTuitionRows as $row) {
$push($fallbackDt, $row['description'], (float)$row['amount'], 'registration');
}
foreach ($eventRows as $row) {
$push($fallbackDt, $row['description'], (float)$row['amount'], 'event');
}
}
}
@@ -1737,13 +1936,22 @@ private function getGradeLevel($grade): array
// Attach refund amount and last payment data to each invoice
foreach ($invoices as &$invoice) {
if ($this->invoiceLedgerService->invoiceIsCarryForward($invoice)) {
$invoice['description'] = $this->invoiceLedgerService->carryForwardDisplayDescription($invoice);
}
$invoice['refund_amount'] = $refunds[$invoice['id']] ?? 0.00;
$invoice['last_paid_amount'] = $lastPayments[$invoice['id']]['last_paid_amount'] ?? 0.00;
$invoice['last_payment_date'] = $lastPayments[$invoice['id']]['last_payment_date'] ?? null;
}
unset($invoice);
$invoiceEventCharges = [];
foreach ($invoices as $invoice) {
if ($this->invoiceLedgerService->invoiceIsCarryForward($invoice)) {
continue;
}
$year = $invoice['school_year'] ?? $this->schoolYear;
$sem = $invoice['semester'] ?? $this->semester;
$invoiceEventCharges[(int)$invoice['id']] = $this->chargesModel->getChargesWithEventInfo(