fix invoice and enrollment fees
This commit is contained in:
@@ -244,6 +244,32 @@ class InvoiceController extends ResourceController
|
||||
return $details;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
private function eventChargesForInvoice(array $invoice): array
|
||||
{
|
||||
if (! $this->db->tableExists('event_charges')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parentId = (int) ($invoice['parent_id'] ?? 0);
|
||||
$schoolYear = trim((string) ($invoice['school_year'] ?? ''));
|
||||
if ($parentId <= 0 || $schoolYear === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->db->table('event_charges ec')
|
||||
->select('ec.*, e.event_name, e.amount AS event_amount, e.description AS event_description')
|
||||
->join('events e', 'e.id = ec.event_id', 'left')
|
||||
->where('ec.parent_id', $parentId)
|
||||
->where('ec.school_year', $schoolYear)
|
||||
->orderBy('COALESCE(ec.created_at, ec.updated_at)', 'ASC', false)
|
||||
->orderBy('ec.id', 'ASC');
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
private function assertSchoolYearNameWritable(string $schoolYear): void
|
||||
{
|
||||
service('schoolYearWriteGuard')->assertWritable(
|
||||
@@ -378,7 +404,6 @@ class InvoiceController extends ResourceController
|
||||
'class_section_id' => $enrollment['class_section_id'],
|
||||
'enrollment_status' => $enrollment['enrollment_status'],
|
||||
'school_year' => $enrollment['school_year'],
|
||||
'semester' => $enrollment['semester'],
|
||||
'admission_status' => $enrollment['admission_status'],
|
||||
'is_withdrawn' => $enrollment['is_withdrawn']
|
||||
];
|
||||
@@ -408,6 +433,9 @@ class InvoiceController extends ResourceController
|
||||
// ✅ Use your helper to calculate tuition fee
|
||||
$fees = $this->calculateTuitionFee($registeredKids, $withdrawnKids);
|
||||
$tuitionFee = $fees['tuition_fee'];
|
||||
$tuitionFeeByStudentId = $this->studentTuitionFeeMap($registeredKids, $withdrawnKids);
|
||||
$registeredKids = $this->applyStudentTuitionFees($registeredKids, $tuitionFeeByStudentId);
|
||||
$withdrawnKids = $this->applyStudentTuitionFees($withdrawnKids, $tuitionFeeByStudentId);
|
||||
|
||||
// ✅ Fetch event charges
|
||||
$eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear);
|
||||
@@ -435,6 +463,11 @@ class InvoiceController extends ResourceController
|
||||
$updatedIds = [];
|
||||
if (!empty($invoice) && isset($invoice['id'])) {
|
||||
$invoiceId = (int) $invoice['id'];
|
||||
$this->syncInvoiceStudentSnapshot(
|
||||
$invoiceId,
|
||||
array_merge($registeredKids, $withdrawnKids),
|
||||
$schoolYear
|
||||
);
|
||||
$this->invoiceLedgerService->syncTuitionLines($invoiceId);
|
||||
$ledger = $this->invoiceLedgerService->recalculate($invoiceId);
|
||||
$updatedIds[] = (int) $ledger['invoice_id'];
|
||||
@@ -511,6 +544,12 @@ class InvoiceController extends ResourceController
|
||||
);
|
||||
}
|
||||
$updated = false;
|
||||
|
||||
$this->syncInvoiceStudentSnapshot(
|
||||
(int) $insertId,
|
||||
array_merge($registeredKids, $withdrawnKids),
|
||||
$schoolYear
|
||||
);
|
||||
}
|
||||
|
||||
$successPayload = [
|
||||
@@ -640,6 +679,165 @@ class InvoiceController extends ResourceController
|
||||
return $tuitionInvoices[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the invoice's student roster as an append-only snapshot.
|
||||
*
|
||||
* Existing rows are never removed or changed here. That keeps already-issued
|
||||
* invoices from losing a student's name after enrollment status changes.
|
||||
*
|
||||
* @param list<array<string, mixed>> $students
|
||||
*/
|
||||
private function syncInvoiceStudentSnapshot(int $invoiceId, array $students, string $schoolYear): void
|
||||
{
|
||||
if ($invoiceId <= 0 || $schoolYear === '' || $students === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hasTuitionFeeColumn = $this->db->fieldExists('tuition_fee', 'invoice_students_list');
|
||||
$existingRows = $this->invoicestudentModel
|
||||
->select($hasTuitionFeeColumn ? 'id, student_id, tuition_fee' : 'id, student_id')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->findAll();
|
||||
$existingByStudentId = [];
|
||||
foreach ($existingRows as $row) {
|
||||
$sid = (int) ($row['student_id'] ?? 0);
|
||||
if ($sid > 0) {
|
||||
$existingByStudentId[$sid] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$studentIds = array_values(array_unique(array_filter(
|
||||
array_map(static fn (array $student): int => (int) ($student['student_id'] ?? 0), $students),
|
||||
static fn (int $studentId): bool => $studentId > 0
|
||||
)));
|
||||
if ($studentIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->invoicestudentModel
|
||||
->where('invoice_id', $invoiceId)
|
||||
->whereNotIn('student_id', $studentIds)
|
||||
->delete();
|
||||
|
||||
$studentRows = $this->studentModel
|
||||
->select('id, firstname, lastname, school_id')
|
||||
->whereIn('id', $studentIds)
|
||||
->findAll();
|
||||
$studentsById = [];
|
||||
foreach ($studentRows as $row) {
|
||||
$studentsById[(int) ($row['id'] ?? 0)] = $row;
|
||||
}
|
||||
|
||||
$now = utc_now();
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int) ($student['student_id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$studentRow = $studentsById[$studentId] ?? [];
|
||||
$tuitionFee = round((float) ($student['tuition_fee'] ?? 0), 2);
|
||||
if (isset($existingByStudentId[$studentId])) {
|
||||
$existing = $existingByStudentId[$studentId];
|
||||
$payload = [
|
||||
'student_firstname' => (string) ($studentRow['firstname'] ?? $student['student_firstname'] ?? ''),
|
||||
'student_lastname' => (string) ($studentRow['lastname'] ?? $student['student_lastname'] ?? ''),
|
||||
'school_id' => (int) ($studentRow['school_id'] ?? $student['school_id'] ?? 0),
|
||||
'enrolled' => in_array((string) ($student['enrollment_status'] ?? ''), ['enrolled', 'payment pending'], true) ? 1 : 0,
|
||||
'school_year' => $schoolYear,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
if ($hasTuitionFeeColumn) {
|
||||
$payload['tuition_fee'] = number_format($tuitionFee, 2, '.', '');
|
||||
}
|
||||
|
||||
$this->invoicestudentModel->update((int) $existing['id'], $payload);
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'invoice_id' => $invoiceId,
|
||||
'student_id' => $studentId,
|
||||
'student_firstname' => (string) ($studentRow['firstname'] ?? $student['student_firstname'] ?? ''),
|
||||
'student_lastname' => (string) ($studentRow['lastname'] ?? $student['student_lastname'] ?? ''),
|
||||
'school_id' => (int) ($studentRow['school_id'] ?? $student['school_id'] ?? 0),
|
||||
'enrolled' => in_array((string) ($student['enrollment_status'] ?? ''), ['enrolled', 'payment pending'], true) ? 1 : 0,
|
||||
'school_year' => $schoolYear,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
if ($hasTuitionFeeColumn) {
|
||||
$payload['tuition_fee'] = number_format($tuitionFee, 2, '.', '');
|
||||
}
|
||||
|
||||
$this->invoicestudentModel->insert($payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function invoiceStudentSnapshotRows(int $invoiceId, string $schoolYear): array
|
||||
{
|
||||
if ($invoiceId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$hasTuitionFeeColumn = $this->db->fieldExists('tuition_fee', 'invoice_students_list');
|
||||
$select = 'id, invoice_id, student_id, student_firstname, student_lastname, school_id, enrolled, school_year';
|
||||
if ($hasTuitionFeeColumn) {
|
||||
$select .= ', tuition_fee';
|
||||
}
|
||||
|
||||
$rows = $this->invoicestudentModel
|
||||
->select($select)
|
||||
->where('invoice_id', $invoiceId)
|
||||
->orderBy('id', 'ASC')
|
||||
->findAll();
|
||||
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$studentIds = array_values(array_unique(array_filter(
|
||||
array_map(static fn (array $row): int => (int) ($row['student_id'] ?? 0), $rows),
|
||||
static fn (int $studentId): bool => $studentId > 0
|
||||
)));
|
||||
|
||||
$schoolIdMap = [];
|
||||
if ($studentIds !== []) {
|
||||
$studentRows = $this->studentModel
|
||||
->select('id, school_id')
|
||||
->whereIn('id', $studentIds)
|
||||
->findAll();
|
||||
foreach ($studentRows as $studentRow) {
|
||||
$schoolIdMap[(int) ($studentRow['id'] ?? 0)] = $studentRow['school_id'] ?? 'N/A';
|
||||
}
|
||||
}
|
||||
|
||||
$snapshot = [];
|
||||
foreach ($rows as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$grade = $this->resolveStudentGradeName($studentId, $schoolYear);
|
||||
$snapshot[] = [
|
||||
'student_id' => $studentId,
|
||||
'student_firstname' => (string) ($row['student_firstname'] ?? ''),
|
||||
'student_lastname' => (string) ($row['student_lastname'] ?? ''),
|
||||
'student_school_id' => $row['school_id'] ?? $schoolIdMap[$studentId] ?? 'N/A',
|
||||
'school_id' => $row['school_id'] ?? $schoolIdMap[$studentId] ?? 0,
|
||||
'grade' => $grade,
|
||||
'tuition_fee' => (float) ($row['tuition_fee'] ?? 0),
|
||||
'enrollment_status' => ((int) ($row['enrolled'] ?? 1) === 1) ? 'enrolled' : 'withdrawn',
|
||||
];
|
||||
}
|
||||
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
@@ -698,6 +896,27 @@ class InvoiceController extends ResourceController
|
||||
}
|
||||
|
||||
$tuitionInvoice = $this->selectActiveInvoiceForParentYear($parentId, $schoolYear);
|
||||
if ($tuitionInvoice !== null) {
|
||||
$snapshotKids = $this->invoiceStudentSnapshotRows((int) ($tuitionInvoice['id'] ?? 0), $schoolYear);
|
||||
if ($snapshotKids !== []) {
|
||||
$enrolledKids = [];
|
||||
$withdrawnKids = [];
|
||||
foreach ($snapshotKids as $snapshotKid) {
|
||||
$kid = [
|
||||
'name' => trim((string) ($snapshotKid['student_firstname'] ?? '') . ' ' . (string) ($snapshotKid['student_lastname'] ?? '')),
|
||||
'grade' => $snapshotKid['grade'] ?? 'N/A',
|
||||
'tuition_fee' => (float) ($snapshotKid['tuition_fee'] ?? 0),
|
||||
];
|
||||
|
||||
if (in_array((string) ($snapshotKid['enrollment_status'] ?? ''), ['enrolled', 'payment pending'], true)) {
|
||||
$enrolledKids[] = $kid;
|
||||
} else {
|
||||
$withdrawnKids[] = $kid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($enrolledKids !== [] || $withdrawnKids !== [] || $tuitionInvoice !== null) {
|
||||
$rows[] = $this->buildInvoiceManagementRow(
|
||||
$parent,
|
||||
@@ -755,7 +974,7 @@ class InvoiceController extends ResourceController
|
||||
$kid = [
|
||||
'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')),
|
||||
'grade' => $grade,
|
||||
'tuition_fee' => (float) ($enrollment['tuition_fee'] ?? 0),
|
||||
'tuition_fee' => 0.0,
|
||||
];
|
||||
|
||||
switch ($enrollment['enrollment_status']) {
|
||||
@@ -937,6 +1156,67 @@ class InvoiceController extends ResourceController
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string,mixed>> $registeredKids
|
||||
* @param list<array<string,mixed>> $withdrawnKids
|
||||
* @return array<int,float>
|
||||
*/
|
||||
private function studentTuitionFeeMap(array $registeredKids, array $withdrawnKids): array
|
||||
{
|
||||
try {
|
||||
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||||
$today = new \DateTimeImmutable('today', new \DateTimeZone($tzName));
|
||||
$deadline = new \DateTimeImmutable($this->refundDeadline, new \DateTimeZone($tzName));
|
||||
$refundOk = $today <= $deadline;
|
||||
} catch (\Throwable $e) {
|
||||
$refundOk = true;
|
||||
}
|
||||
|
||||
$billableStudents = $refundOk ? $registeredKids : array_merge($registeredKids, $withdrawnKids);
|
||||
$schoolYear = (string) ($billableStudents[0]['school_year'] ?? $this->schoolYear);
|
||||
|
||||
foreach ($billableStudents as &$student) {
|
||||
$student['grade'] = $this->resolveStudentGradeName(
|
||||
(int) ($student['student_id'] ?? 0),
|
||||
$schoolYear,
|
||||
$student['class_section_id'] ?? null
|
||||
);
|
||||
}
|
||||
unset($student);
|
||||
|
||||
usort($billableStudents, fn (array $left, array $right): int => GradeLevelParser::parse($left['grade'] ?? null) <=> GradeLevelParser::parse($right['grade'] ?? null));
|
||||
|
||||
$fees = [];
|
||||
$studentCount = 0;
|
||||
foreach ($billableStudents as $student) {
|
||||
$studentId = (int) ($student['student_id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fees[$studentId] = $studentCount === 0 ? $this->firstStudentFee : $this->secondStudentFee;
|
||||
$studentCount++;
|
||||
}
|
||||
|
||||
return $fees;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string,mixed>> $students
|
||||
* @param array<int,float> $tuitionFeeByStudentId
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
private function applyStudentTuitionFees(array $students, array $tuitionFeeByStudentId): array
|
||||
{
|
||||
foreach ($students as &$student) {
|
||||
$studentId = (int) ($student['student_id'] ?? 0);
|
||||
$student['tuition_fee'] = $tuitionFeeByStudentId[$studentId] ?? 0.0;
|
||||
}
|
||||
unset($student);
|
||||
|
||||
return $students;
|
||||
}
|
||||
|
||||
private function calculateTotalTuitionFee(array $students): float
|
||||
{
|
||||
$schoolYear = (string) ($students[0]['school_year'] ?? $this->schoolYear);
|
||||
@@ -1074,100 +1354,29 @@ class InvoiceController extends ResourceController
|
||||
return ['error' => "Parent associated with the invoice was not found."];
|
||||
}
|
||||
|
||||
$ledger = $this->invoiceLedgerService->calculateInvoice((int) $invoiceId);
|
||||
$invoiceLines = $this->db->table('invoice_lines')
|
||||
->select('description, quantity, unit_amount_cents, line_amount_cents, line_type, source_type, source_id, created_at, metadata_json')
|
||||
->where('invoice_id', (int)$invoiceId)
|
||||
->where('voided_at IS NULL', null, false)
|
||||
->orderBy('id', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$enrollments = $this->enrollmentModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
if (empty($enrollments)) {
|
||||
return ['error' => 'No enrollments found for this invoice.'];
|
||||
}
|
||||
$ledger = $this->invoiceLedgerService->storedInvoiceLedger((int) $invoiceId);
|
||||
$invoiceLines = [];
|
||||
|
||||
$registeredKids = [];
|
||||
$withdrawnKids = [];
|
||||
$snapshotKids = $this->invoiceStudentSnapshotRows((int) $invoiceId, (string) $schoolYear);
|
||||
|
||||
foreach ($enrollments as $enrollment) {
|
||||
$student = $this->studentModel->find($enrollment['student_id']);
|
||||
if (!$student) {
|
||||
log_message('error', "Student not found for ID: {$enrollment['student_id']}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$grade = $this->studentClassModel->getStudentGrade($student['id']);
|
||||
|
||||
$studentData = [
|
||||
'student_id' => $student['id'],
|
||||
'student_firstname' => $student['firstname'],
|
||||
'student_lastname' => $student['lastname'],
|
||||
'grade' => $grade,
|
||||
'tuition_fee' => $enrollment['tuition_fee'] ?? 0,
|
||||
'enrollment_status' => $enrollment['enrollment_status'],
|
||||
];
|
||||
|
||||
if (in_array($enrollment['enrollment_status'], ['enrolled', 'payment pending'], true)) {
|
||||
$registeredKids[] = $studentData;
|
||||
} elseif (in_array($enrollment['enrollment_status'], ['withdrawn', 'withdraw under review', 'refund pending'], true)) {
|
||||
$withdrawnKids[] = $studentData;
|
||||
} else {
|
||||
log_message('info', "Skipping student ID {$student['id']} with status {$enrollment['enrollment_status']}");
|
||||
if ($snapshotKids !== []) {
|
||||
foreach ($snapshotKids as $studentData) {
|
||||
if (in_array((string) ($studentData['enrollment_status'] ?? ''), ['enrolled', 'payment pending'], true)) {
|
||||
$registeredKids[] = $studentData;
|
||||
} else {
|
||||
$withdrawnKids[] = $studentData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$registeredKids = array_values(array_filter($registeredKids, function ($student) use ($schoolYear) {
|
||||
$sid = (int)($student['student_id'] ?? 0);
|
||||
return $sid > 0 && $this->studentClassModel->hasNonEventAssignment($sid, $schoolYear);
|
||||
}));
|
||||
|
||||
$withdrawnKids = array_values(array_filter($withdrawnKids, function ($student) use ($schoolYear) {
|
||||
$sid = (int)($student['student_id'] ?? 0);
|
||||
return $sid > 0 && $this->studentClassModel->hasNonEventAssignment($sid, $schoolYear);
|
||||
}));
|
||||
|
||||
usort($registeredKids, fn($a, $b) => $this->compareGrades($a['grade'], $b['grade']));
|
||||
usort($withdrawnKids, fn($a, $b) => $this->compareGrades($a['grade'], $b['grade']));
|
||||
|
||||
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||||
$tz = new \DateTimeZone($tzName);
|
||||
$currentDate = new \DateTimeImmutable('today', $tz);
|
||||
$deadline = new \DateTimeImmutable($this->refundDeadline, $tz);
|
||||
$refundAllowed = $currentDate <= $deadline;
|
||||
|
||||
$studentCharges = [];
|
||||
$studentCount = 0;
|
||||
|
||||
/**
|
||||
* First student pays the base fee; every additional student pays base minus $100.
|
||||
*/
|
||||
$computeCharge = function (array $student) use (&$studentCount) {
|
||||
$fee = ($studentCount === 0) ? $this->firstStudentFee : $this->secondStudentFee;
|
||||
$studentCount++;
|
||||
return $fee;
|
||||
};
|
||||
|
||||
// Registered kids -> pay unit fee
|
||||
foreach ($registeredKids as $student) {
|
||||
$unitFee = $computeCharge($student);
|
||||
$studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0];
|
||||
}
|
||||
|
||||
// Refund NOT allowed -> withdrawn students still owe unit fee
|
||||
if (!$refundAllowed) {
|
||||
foreach ($withdrawnKids as $student) {
|
||||
$unitFee = $computeCharge($student);
|
||||
$studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
$eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear, $invoice['semester'] ?? null);
|
||||
$eventsList = $this->eventChargesForInvoice($invoice);
|
||||
|
||||
// Attach SCHOOL IDs
|
||||
$allKids = array_merge($registeredKids, $withdrawnKids);
|
||||
@@ -1261,7 +1470,7 @@ class InvoiceController extends ResourceController
|
||||
'carryForwardDescription'=> $this->invoiceLedgerService->carryForwardDisplayDescription($invoice),
|
||||
'registeredKids' => $registeredKids,
|
||||
'withdrawnKids' => $withdrawnKids,
|
||||
'studentCharges' => $studentCharges,
|
||||
'studentCharges' => [],
|
||||
'events' => $eventsList,
|
||||
'students' => $students,
|
||||
'payments' => $payments,
|
||||
@@ -1286,23 +1495,11 @@ class InvoiceController extends ResourceController
|
||||
return ['error' => 'Parent associated with the invoice was not found.'];
|
||||
}
|
||||
|
||||
$ledger = $this->invoiceLedgerService->calculateInvoice($invoiceId);
|
||||
$ledger = $this->invoiceLedgerService->storedInvoiceLedger($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;
|
||||
@@ -1542,8 +1739,7 @@ class InvoiceController extends ResourceController
|
||||
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);
|
||||
$amount = (float)($student['tuition_fee'] ?? 0.0);
|
||||
if ($sid <= 0 || abs($amount) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
@@ -1562,6 +1758,41 @@ class InvoiceController extends ResourceController
|
||||
}
|
||||
}
|
||||
|
||||
$snapshotTuitionRows = function (float $total) use ($registeredKids, $withdrawnKids): array {
|
||||
$students = array_values(array_filter(
|
||||
array_merge($registeredKids ?? [], $withdrawnKids ?? []),
|
||||
static fn (array $student): bool => (int)($student['student_id'] ?? 0) > 0
|
||||
));
|
||||
if ($students === [] || abs($total) < 0.01) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$count = count($students);
|
||||
$baseAmount = floor(($total / $count) * 100) / 100;
|
||||
$allocated = 0.0;
|
||||
$rows = [];
|
||||
|
||||
foreach ($students as $index => $student) {
|
||||
$sid = (int)($student['student_id'] ?? 0);
|
||||
$amount = $index === $count - 1 ? round($total - $allocated, 2) : $baseAmount;
|
||||
$allocated += $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 . ')';
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'description' => $desc,
|
||||
'amount' => $amount,
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
};
|
||||
|
||||
$eventRows = [];
|
||||
foreach (($events ?? []) as $event) {
|
||||
$amount = (float)($event['charged'] ?? 0.0);
|
||||
@@ -1624,6 +1855,16 @@ class InvoiceController extends ResourceController
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!str_contains($type, 'additional') && !str_contains($type, 'event') && $studentTuitionRows === []) {
|
||||
$fallbackStudentRows = $snapshotTuitionRows($amount);
|
||||
if ($fallbackStudentRows !== []) {
|
||||
foreach ($fallbackStudentRows as $row) {
|
||||
$push($dt, $row['description'], (float)$row['amount'], 'registration');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (str_contains($type, 'event') && !empty($eventRows)) {
|
||||
$expandedTotal = 0.0;
|
||||
foreach ($eventRows as $row) {
|
||||
@@ -1649,8 +1890,23 @@ class InvoiceController extends ResourceController
|
||||
$push($fallbackDt, $carryForwardDescription, $carryForwardAmount, 'additional');
|
||||
}
|
||||
} else {
|
||||
foreach ($studentTuitionRows as $row) {
|
||||
$push($fallbackDt, $row['description'], (float)$row['amount'], 'registration');
|
||||
if ($studentTuitionRows !== []) {
|
||||
foreach ($studentTuitionRows as $row) {
|
||||
$push($fallbackDt, $row['description'], (float)$row['amount'], 'registration');
|
||||
}
|
||||
} else {
|
||||
$eventTotal = array_reduce($eventRows, static fn (float $sum, array $row): float => $sum + (float) ($row['amount'] ?? 0), 0.0);
|
||||
$savedTuitionTotal = round((float)($ledger['total_amount'] ?? $invoice['total_amount'] ?? 0) - $eventTotal - $additionalChargesTotal, 2);
|
||||
if (abs($savedTuitionTotal) >= 0.01) {
|
||||
$fallbackStudentRows = $snapshotTuitionRows($savedTuitionTotal);
|
||||
if ($fallbackStudentRows !== []) {
|
||||
foreach ($fallbackStudentRows as $row) {
|
||||
$push($fallbackDt, $row['description'], (float)$row['amount'], 'registration');
|
||||
}
|
||||
} else {
|
||||
$push($fallbackDt, 'Tuition charges', $savedTuitionTotal, 'registration');
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($eventRows as $row) {
|
||||
$push($fallbackDt, $row['description'], (float)$row['amount'], 'event');
|
||||
@@ -1977,13 +2233,7 @@ private function getGradeLevel($grade): array
|
||||
continue;
|
||||
}
|
||||
|
||||
$year = $invoice['school_year'] ?? $this->schoolYear;
|
||||
$sem = $invoice['semester'] ?? $this->semester;
|
||||
$invoiceEventCharges[(int)$invoice['id']] = $this->chargesModel->getChargesWithEventInfo(
|
||||
$invoice['parent_id'],
|
||||
$year,
|
||||
$sem
|
||||
);
|
||||
$invoiceEventCharges[(int)$invoice['id']] = $this->eventChargesForInvoice($invoice);
|
||||
}
|
||||
|
||||
return view('/parent/invoice_payment', [
|
||||
|
||||
Reference in New Issue
Block a user