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