prepareInvoiceData($invoiceId); if (isset($data['error'])) { return $this->renderErrorPdf($data['error']); } return $this->renderPdfInvoice($data); } private function prepareInvoiceData(int $invoiceId): array { $invoice = Invoice::query()->find($invoiceId); if (!$invoice) { return ['error' => 'No invoice was generated. Please contact the school administration.']; } $parentId = (int) ($invoice->parent_id ?? 0); $schoolYear = (string) ($invoice->school_year ?? $this->config->getSchoolYear()); $paymentsQuery = Payment::query() ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->where('invoice_id', $invoiceId); if (Schema::hasColumn('payments', 'status')) { $exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled']; $paymentsQuery->where(function ($q) use ($exclude) { $q->whereNotIn('status', $exclude)->orWhereNull('status'); }); } if (Schema::hasColumn('payments', 'is_void')) { $paymentsQuery->where(function ($q) { $q->where('is_void', 0)->orWhereNull('is_void'); }); } $payments = $paymentsQuery->get()->map(fn ($r) => $r->toArray())->all(); $parent = User::query()->find($parentId); if (!$parent) { return ['error' => 'Parent associated with the invoice was not found.']; } $enrollments = Enrollment::query() ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->get() ->map(fn ($r) => $r->toArray()) ->all(); if (empty($enrollments)) { return ['error' => 'No enrollments found for this invoice.']; } $registeredKids = []; $withdrawnKids = []; foreach ($enrollments as $enrollment) { $student = Student::query()->find($enrollment['student_id']); if (!$student) { continue; } $grade = StudentClass::getStudentGrade((int) $student->id); $studentData = [ 'student_id' => (int) $student->id, 'student_firstname' => (string) ($student->firstname ?? ''), 'student_lastname' => (string) ($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; } } $registeredKids = array_values(array_filter($registeredKids, function ($student) use ($schoolYear) { $sid = (int) ($student['student_id'] ?? 0); return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear); })); $withdrawnKids = array_values(array_filter($withdrawnKids, function ($student) use ($schoolYear) { $sid = (int) ($student['student_id'] ?? 0); return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear); })); usort($registeredKids, fn ($a, $b) => $this->grades->compareGrades((string) $a['grade'], (string) $b['grade'])); usort($withdrawnKids, fn ($a, $b) => $this->grades->compareGrades((string) $a['grade'], (string) $b['grade'])); $refundAllowed = true; $refundDeadline = $this->config->getRefundDeadline(); if ($refundDeadline) { try { $tz = new \DateTimeZone($this->config->getTimezone()); $today = new \DateTimeImmutable('today', $tz); $deadline = new \DateTimeImmutable($refundDeadline, $tz); $refundAllowed = $today <= $deadline; } catch (\Throwable $e) { $refundAllowed = true; } } $studentCharges = []; $regularCount = 0; $gradeFee = $this->config->getGradeFee(); $firstFee = $this->config->getFirstStudentFee(); $secondFee = $this->config->getSecondStudentFee(); $youthFee = $this->config->getYouthFee(); $computeCharge = function (int $gradeLevel, string $gradeName) use (&$regularCount, $gradeFee, $firstFee, $secondFee, $youthFee) { $isRegular = $this->grades->isKindergarten($gradeName) || ($gradeLevel > 0 && $gradeLevel <= $gradeFee); if ($isRegular) { $fee = $regularCount === 0 ? $firstFee : $secondFee; $regularCount++; return $fee; } return $youthFee; }; foreach ($registeredKids as $student) { $gradeName = (string) ($student['grade'] ?? ''); $gradeLevel = $this->grades->gradeLevelInt($gradeName); $unitFee = $computeCharge($gradeLevel, $gradeName); $studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0]; } if (!$refundAllowed) { foreach ($withdrawnKids as $student) { $gradeName = (string) ($student['grade'] ?? ''); $gradeLevel = $this->grades->gradeLevelInt($gradeName); $unitFee = $computeCharge($gradeLevel, $gradeName); $studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0]; } } $eventsList = EventCharges::getChargesWithEventInfo($parentId, $schoolYear); $studentIds = array_values(array_unique(array_map(static fn ($r) => (int) ($r['student_id'] ?? 0), array_merge($registeredKids, $withdrawnKids)))); $schoolIdMap = []; if (!empty($studentIds)) { $rows = Student::query()->whereIn('id', $studentIds)->get(['id', 'school_id'])->all(); foreach ($rows as $row) { $schoolIdMap[(int) $row->id] = $row->school_id ?? 'N/A'; } } $students = array_merge($registeredKids, $withdrawnKids); foreach ($students as $i => $s) { $sid = (int) ($s['student_id'] ?? 0); $students[$i]['student_school_id'] = $schoolIdMap[$sid] ?? 'N/A'; } $discounts = DiscountUsage::query() ->where('invoice_id', $invoiceId) ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->get() ->map(fn ($r) => $r->toArray()) ->all(); $refundsPaidTotal = (float) DB::table('refunds') ->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS tot') ->where('invoice_id', $invoiceId) ->where('school_year', $schoolYear) ->whereIn('status', ['Partial', 'Paid']) ->value('tot'); $acRows = AdditionalCharge::query() ->select('id', 'charge_type', 'title', 'description', 'amount', 'due_date', 'status', 'created_at') ->where('invoice_id', $invoiceId) ->where('status', '!=', 'void') ->orderBy('created_at') ->orderBy('id') ->get() ->map(fn ($r) => $r->toArray()) ->all(); $additionalChargeLines = []; $additionalChargesTotal = 0.0; foreach ($acRows as $ac) { $signed = (float) ($ac['amount'] ?? 0); $ctype = strtolower((string) ($ac['charge_type'] ?? '')); if ($ctype === 'deduct' && $signed > 0) { $signed = -$signed; } elseif ($ctype === 'add' && $signed < 0) { $signed = abs($signed); } $lineDate = !empty($ac['created_at']) ? date('Y-m-d', strtotime($ac['created_at'])) : (!empty($invoice->created_at) ? local_date($invoice->created_at, 'Y-m-d') : local_date(utc_now(), 'Y-m-d')); $desc = $ac['description'] ?? ''; if ($desc === '') { $desc = $ctype === 'deduct' ? 'Deduct' : 'Add'; } $additionalChargesTotal += $signed; $additionalChargeLines[] = [ 'date' => $lineDate, 'description' => $desc, 'amount' => $signed, ]; } return [ 'invoice' => $invoice->toArray(), 'parent' => $parent->toArray(), 'registeredKids' => $registeredKids, 'withdrawnKids' => $withdrawnKids, 'studentCharges' => $studentCharges, 'events' => $eventsList, 'students' => $students, 'payments' => $payments, 'discounts' => $discounts, 'additionalChargeLines' => $additionalChargeLines, 'additionalChargesTotal' => round($additionalChargesTotal, 2), 'refundsPaidTotal' => $refundsPaidTotal, ]; } private function renderPdfInvoice(array $data): string { $invoice = $data['invoice']; $parent = $data['parent']; $registeredKids = $data['registeredKids'] ?? []; $withdrawnKids = $data['withdrawnKids'] ?? []; $studentCharges = $data['studentCharges'] ?? []; $events = $data['events'] ?? []; $students = $data['students'] ?? []; $payments = $data['payments'] ?? []; $discounts = $data['discounts'] ?? []; $additionalChargeLines = $data['additionalChargeLines'] ?? []; $additionalChargesTotal = (float) ($data['additionalChargesTotal'] ?? 0); $refundsPaidTotal = (float) ($data['refundsPaidTotal'] ?? 0); $pdf = new \FPDF(); $pdf->AddPage(); $pdf->SetFont('Arial', 'B', 16); $pdf->Cell(0, 10, 'Invoice', 0, 1, 'C'); $pdf->Ln(4); $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(40, 6, 'Parent Name:', 0, 0, 'L'); $pdf->SetFont('Arial', '', 12); $pdf->Cell(0, 6, ($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? ''), 0, 1, 'L'); $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(40, 6, 'Invoice Number:', 0, 0, 'L'); $pdf->SetFont('Arial', '', 12); $pdf->Cell(0, 6, (string) ($invoice['invoice_number'] ?? ''), 0, 1, 'L'); $pdf->Ln(8); $pdf->SetFont('Arial', 'B', 11); $pdf->Cell(30, 7, 'Date', 1); $pdf->Cell(130, 7, 'Description', 1); $pdf->Cell(30, 7, 'Amount', 1, 1, 'R'); $transactions = []; $seq = 0; $push = function (\DateTimeImmutable $dt, string $desc, float $amount, string $cat) use (&$transactions, &$seq) { $transactions[] = [ 'dt' => $dt, 'description' => $desc, 'amount' => $amount, 'cat' => $cat, 'seq' => $seq++, ]; }; $tz = new \DateTimeZone($this->config->getTimezone()); $toLocal = function (?string $raw) use ($tz): \DateTimeImmutable { if (!$raw) { return new \DateTimeImmutable('now', $tz); } try { return new \DateTimeImmutable($raw, $tz); } catch (\Throwable $e) { return new \DateTimeImmutable('now', $tz); } }; foreach ($registeredKids as $student) { $id = $student['student_id']; $unit = (float) ($studentCharges[$id]['unit_fee'] ?? 0.0); $name = $student['student_firstname'] . ' ' . $student['student_lastname']; $gradeName = ClassSection::getClassSectionNameByClassId($student['grade']) ?? $student['grade']; $push($toLocal($invoice['created_at'] ?? null), 'Registration of "' . $name . '" in ' . $gradeName, $unit, 'registration'); } foreach ($events as $event) { $studentName = 'N/A'; if (!empty($event['student_id'])) { foreach ($students as $st) { if (($st['student_id'] ?? null) == $event['student_id']) { $studentName = $st['student_firstname'] . ' ' . $st['student_lastname']; break; } } } $amount = (float) ($event['charged'] ?? 0.0); $eventName = $event['event_name'] ?? 'Event'; $push($toLocal($event['created_at'] ?? null), 'Event ' . $eventName . ' for "' . $studentName . '"', $amount, 'event'); } foreach ($additionalChargeLines as $line) { $push($toLocal($line['date'] ?? null), (string) ($line['description'] ?? 'Additional Charge'), (float) ($line['amount'] ?? 0), 'additional'); } $totalPaid = 0.0; foreach ($payments as $payment) { $amount = (float) ($payment['paid_amount'] ?? 0.0); $totalPaid += $amount; $push($toLocal($payment['payment_date'] ?? null), 'Payment (' . ($payment['payment_method'] ?? 'Payment') . ')', -$amount, 'payment'); } $totalDiscount = 0.0; foreach ($discounts as $discount) { $amount = (float) ($discount['discount_amount'] ?? 0.0); $totalDiscount += $amount; $push($toLocal($discount['used_at'] ?? null), 'Discount applied', -$amount, 'discount'); } usort($transactions, function ($a, $b) { $dayA = $a['dt']->format('Y-m-d'); $dayB = $b['dt']->format('Y-m-d'); if ($dayA !== $dayB) { return $a['dt'] <=> $b['dt']; } $pri = [ 'registration' => 10, 'event' => 20, 'additional' => 25, 'discount' => 30, 'payment' => 90, 'other' => 50, ]; $pa = $pri[$a['cat']] ?? 50; $pb = $pri[$b['cat']] ?? 50; if ($pa !== $pb) { return $pa <=> $pb; } $cmp = $a['dt'] <=> $b['dt']; return $cmp !== 0 ? $cmp : ($a['seq'] <=> $b['seq']); }); $pdf->SetFont('Arial', '', 10); foreach ($transactions as $t) { $pdf->Cell(30, 7, $t['dt']->format('m-d-Y'), 1); $pdf->Cell(130, 7, $t['description'], 1); $pdf->Cell(30, 7, ($t['amount'] < 0 ? '-$' : '$') . number_format(abs($t['amount']), 2), 1, 1, 'R'); } $tuitionSubtotal = 0.0; foreach ($studentCharges as $sc) { $tuitionSubtotal += (float) ($sc['unit_fee'] ?? 0.0); } $eventSubtotal = 0.0; foreach ($events as $ev) { $eventSubtotal += (float) ($ev['charged'] ?? 0.0); } $totalAmount = round($tuitionSubtotal + $eventSubtotal + $additionalChargesTotal, 2); $balance = $totalAmount - $totalPaid - $totalDiscount - $refundsPaidTotal; if ($balance < 0) { $balance = 0.0; } $pdf->Ln(5); $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Total Charges:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); $pdf->Cell(30, 7, '$' . number_format($totalAmount, 2), 0, 1, 'R'); $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Total Paid:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); $pdf->Cell(30, 7, '$' . number_format($totalPaid, 2), 0, 1, 'R'); if ($totalDiscount > 0) { $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Total Discount:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); $pdf->Cell(30, 7, '$' . number_format($totalDiscount, 2), 0, 1, 'R'); } if ($refundsPaidTotal > 0) { $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Refunds Paid:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); $pdf->Cell(30, 7, '$' . number_format($refundsPaidTotal, 2), 0, 1, 'R'); } $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Balance Due:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); $pdf->Cell(30, 7, '$' . number_format($balance, 2), 0, 1, 'R'); return $pdf->Output('S'); } private function renderErrorPdf(string $message): string { $pdf = new \FPDF(); $pdf->AddPage(); $pdf->SetFont('Arial', 'B', 16); $pdf->Cell(0, 10, 'Error', 0, 1, 'C'); $pdf->SetFont('Arial', '', 12); $pdf->Ln(10); $pdf->MultiCell(0, 10, $message); return $pdf->Output('S'); } }