add controllers, servoices
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Invoices;
|
||||
|
||||
use App\Models\Configuration;
|
||||
|
||||
class InvoiceConfigService
|
||||
{
|
||||
public function getSchoolYear(): string
|
||||
{
|
||||
return (string) (Configuration::getConfig('school_year') ?? date('Y'));
|
||||
}
|
||||
|
||||
public function getSemester(): string
|
||||
{
|
||||
return (string) (Configuration::getConfig('semester') ?? 'Fall');
|
||||
}
|
||||
|
||||
public function getDueDate(): ?string
|
||||
{
|
||||
$value = Configuration::getConfig('due_date');
|
||||
return $value ? (string) $value : null;
|
||||
}
|
||||
|
||||
public function getGradeFee(): int
|
||||
{
|
||||
return (int) (Configuration::getConfig('grade_fee') ?? 9);
|
||||
}
|
||||
|
||||
public function getFirstStudentFee(): float
|
||||
{
|
||||
return (float) (Configuration::getConfig('first_student_fee') ?? 350);
|
||||
}
|
||||
|
||||
public function getSecondStudentFee(): float
|
||||
{
|
||||
return (float) (Configuration::getConfig('second_student_fee') ?? 200);
|
||||
}
|
||||
|
||||
public function getYouthFee(): float
|
||||
{
|
||||
return (float) (Configuration::getConfig('youth_fee') ?? 180);
|
||||
}
|
||||
|
||||
public function getRefundDeadline(): ?string
|
||||
{
|
||||
$value = Configuration::getConfig('refund_deadline');
|
||||
if (!$value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return date('Y-m-d', strtotime((string) $value));
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getTimezone(): string
|
||||
{
|
||||
return (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Invoices;
|
||||
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\EventCharges;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Refund;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\User;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InvoiceGenerationService
|
||||
{
|
||||
public function __construct(
|
||||
private InvoiceConfigService $config,
|
||||
private InvoiceTuitionService $tuitionService
|
||||
) {
|
||||
}
|
||||
|
||||
public function generateInvoice(int $parentId, ?string $schoolYear = null): array
|
||||
{
|
||||
$schoolYear = $schoolYear ?: $this->config->getSchoolYear();
|
||||
$semester = $this->config->getSemester();
|
||||
|
||||
$enrollments = Enrollment::query()
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
|
||||
if (empty($enrollments)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'No enrollment records found.',
|
||||
];
|
||||
}
|
||||
|
||||
$registeredKids = [];
|
||||
$withdrawnKids = [];
|
||||
|
||||
foreach ($enrollments as $enrollment) {
|
||||
$student = [
|
||||
'student_id' => $enrollment['student_id'],
|
||||
'parent_id' => $enrollment['parent_id'],
|
||||
'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'],
|
||||
];
|
||||
|
||||
if (in_array($enrollment['enrollment_status'], ['enrolled', 'payment pending'], true)) {
|
||||
$registeredKids[] = $student;
|
||||
} elseif (in_array($enrollment['enrollment_status'], ['withdrawn', 'refund pending', 'withdraw under review'], true)) {
|
||||
$withdrawnKids[] = $student;
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
}));
|
||||
|
||||
$tuitionFee = $this->tuitionService->calculateTuitionFee(
|
||||
$registeredKids,
|
||||
$withdrawnKids,
|
||||
$this->config->getRefundDeadline()
|
||||
);
|
||||
|
||||
$eventsList = EventCharges::getChargesWithEventInfo($parentId, $schoolYear);
|
||||
$eventChargeTotal = array_sum(array_column($eventsList, 'charged'));
|
||||
|
||||
$this->recalculateAndUpdateDiscount($parentId, $schoolYear, $tuitionFee, $enrollments);
|
||||
|
||||
$refundPaid = (new Refund())->getTotalApprovedRefundByParentIdAndSchoolYear($parentId, $schoolYear);
|
||||
$totalPaid = Payment::getTotalPaidByParentId($parentId, $schoolYear);
|
||||
|
||||
$totalAmount = max(0.0, (float) $tuitionFee) + (float) $eventChargeTotal;
|
||||
|
||||
$existingInvoices = Invoice::getInvoicesByParentId($parentId, $schoolYear);
|
||||
$updated = false;
|
||||
$updatedIds = [];
|
||||
$insertId = null;
|
||||
|
||||
if (!empty($existingInvoices)) {
|
||||
foreach ($existingInvoices as $invoice) {
|
||||
if (!isset($invoice['id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extrasSum = $this->sumInvoiceAdditionalCharges((int) $invoice['id'], $schoolYear);
|
||||
$newTotal = round($totalAmount + $extrasSum, 2);
|
||||
|
||||
$invDiscount = $this->sumInvoiceDiscounts((int) $invoice['id']);
|
||||
$invRefunds = $this->sumInvoiceRefunds((int) $invoice['id'], $schoolYear);
|
||||
$paidOnInv = (float) ($invoice['paid_amount'] ?? 0.0);
|
||||
|
||||
$newBalance = $newTotal - $invDiscount - $invRefunds - $paidOnInv;
|
||||
$newStatus = $newBalance <= 0.00001
|
||||
? 'Paid'
|
||||
: ($paidOnInv > 0 ? 'Partially Paid' : 'Unpaid');
|
||||
|
||||
Invoice::query()
|
||||
->whereKey($invoice['id'])
|
||||
->update([
|
||||
'total_amount' => $newTotal,
|
||||
'paid_amount' => $paidOnInv,
|
||||
'balance' => $newBalance,
|
||||
'status' => $newStatus,
|
||||
'updated_at' => utc_now(),
|
||||
]);
|
||||
|
||||
$updatedIds[] = (int) $invoice['id'];
|
||||
$updated = true;
|
||||
}
|
||||
} else {
|
||||
$schoolId = User::getSchoolIdByUserId($parentId);
|
||||
$invoiceNumber = $schoolId
|
||||
? ('INV-' . $schoolId . '-' . uniqid())
|
||||
: uniqid('INV-');
|
||||
|
||||
$issueUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
$dueUtc = null;
|
||||
$dueDate = $this->config->getDueDate();
|
||||
if (!empty($dueDate)) {
|
||||
$dueLocal = new DateTime($dueDate . ' 19:59:59', new DateTimeZone($this->config->getTimezone()));
|
||||
$dueLocal->setTimezone(new DateTimeZone('UTC'));
|
||||
$dueUtc = $dueLocal->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$insertId = Invoice::query()->insertGetId([
|
||||
'parent_id' => $parentId,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'total_amount' => $totalAmount,
|
||||
'paid_amount' => 0,
|
||||
'balance' => $totalAmount,
|
||||
'status' => 'Unpaid',
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'issue_date' => $issueUtc,
|
||||
'due_date' => $dueUtc,
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'updated' => $updated,
|
||||
'updated_ids' => $updatedIds,
|
||||
'insert_id' => $insertId ? (int) $insertId : null,
|
||||
'total_paid' => $totalPaid,
|
||||
'refund_paid' => $refundPaid,
|
||||
];
|
||||
}
|
||||
|
||||
private function recalculateAndUpdateDiscount(int $parentId, string $schoolYear, float $tuitionFee, array $enrollments): void
|
||||
{
|
||||
$eventChargeTotal = 0.0;
|
||||
try {
|
||||
$eventsList = EventCharges::getChargesWithEventInfo($parentId, $schoolYear);
|
||||
$eventChargeTotal = array_sum(array_column($eventsList, 'charged'));
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
$invoices = Invoice::getInvoicesByParentId($parentId, $schoolYear);
|
||||
if (empty($invoices)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
if (!isset($invoice['id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$invoiceId = (int) $invoice['id'];
|
||||
$discountUsage = DB::table('discount_usages as du')
|
||||
->select('du.id', 'dv.id as voucher_id', 'dv.discount_type', 'dv.discount_value')
|
||||
->join('discount_vouchers as dv', 'du.voucher_id', '=', 'dv.id')
|
||||
->join('invoices as i', 'du.invoice_id', '=', 'i.id')
|
||||
->where('du.invoice_id', $invoiceId)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if (!$discountUsage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extrasSum = $this->sumInvoiceAdditionalCharges($invoiceId, $schoolYear);
|
||||
$baseTotal = round($tuitionFee + $eventChargeTotal + $extrasSum, 2);
|
||||
|
||||
if (($discountUsage->discount_type ?? '') === 'percent') {
|
||||
$discountAmount = round(($baseTotal * (float) $discountUsage->discount_value) / 100, 2);
|
||||
} else {
|
||||
$discountAmount = min((float) $discountUsage->discount_value, $baseTotal);
|
||||
}
|
||||
|
||||
DB::table('discount_usages')
|
||||
->where('id', $discountUsage->id)
|
||||
->update([
|
||||
'discount_amount' => $discountAmount,
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function sumInvoiceAdditionalCharges(int $invoiceId, string $schoolYear): float
|
||||
{
|
||||
$extrasSum = 0.0;
|
||||
$rows = DB::table('additional_charges')
|
||||
->select('charge_type', 'amount')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('status', 'applied')
|
||||
->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$amount = (float) ($row->amount ?? 0);
|
||||
$type = strtolower((string) ($row->charge_type ?? 'add'));
|
||||
if ($type === 'deduct') {
|
||||
$amount = -abs($amount);
|
||||
} else {
|
||||
$amount = abs($amount);
|
||||
}
|
||||
$extrasSum += $amount;
|
||||
}
|
||||
|
||||
return $extrasSum;
|
||||
}
|
||||
|
||||
private function sumInvoiceDiscounts(int $invoiceId): float
|
||||
{
|
||||
$row = DB::table('discount_usages')
|
||||
->selectRaw('COALESCE(SUM(discount_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->first();
|
||||
|
||||
return (float) ($row->tot ?? 0.0);
|
||||
}
|
||||
|
||||
private function sumInvoiceRefunds(int $invoiceId, string $schoolYear): float
|
||||
{
|
||||
$row = DB::table('refunds')
|
||||
->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->first();
|
||||
|
||||
return (float) ($row->tot ?? 0.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Invoices;
|
||||
|
||||
class InvoiceGradeService
|
||||
{
|
||||
public function __construct(private int $gradeFee)
|
||||
{
|
||||
}
|
||||
|
||||
public function isKindergarten(string $grade): bool
|
||||
{
|
||||
$g = strtolower(trim($grade));
|
||||
return in_array($g, ['KG', 'kg', 'k', 'kindergarten', 'k-g', 'k.g', 'k g'], true);
|
||||
}
|
||||
|
||||
public function compareGrades(string $gradeA, string $gradeB): int
|
||||
{
|
||||
$valA = $this->getGradeLevel($gradeA);
|
||||
$valB = $this->getGradeLevel($gradeB);
|
||||
|
||||
if ($valA['level'] !== $valB['level']) {
|
||||
return $valA['level'] <=> $valB['level'];
|
||||
}
|
||||
|
||||
return strcmp($valA['suffix'] ?? '', $valB['suffix'] ?? '');
|
||||
}
|
||||
|
||||
public function gradeLevelInt(string $grade): int
|
||||
{
|
||||
$gl = $this->getGradeLevel($grade);
|
||||
return (int) ($gl['level'] ?? 0);
|
||||
}
|
||||
|
||||
public function getGradeLevel($grade): array
|
||||
{
|
||||
if (is_numeric($grade)) {
|
||||
$num = (int) $grade;
|
||||
if ($num === 13) {
|
||||
return ['level' => 1, 'suffix' => '', 'classId' => 13];
|
||||
}
|
||||
return ['level' => $num, 'suffix' => '', 'classId' => null];
|
||||
}
|
||||
|
||||
if (!is_string($grade)) {
|
||||
return ['level' => 999, 'suffix' => '', 'classId' => null];
|
||||
}
|
||||
|
||||
$g = strtoupper(trim($grade));
|
||||
$g = preg_replace('/\s+/', ' ', $g);
|
||||
$g = str_replace(['.', '_'], '', $g);
|
||||
$g = str_replace('-', ' ', $g);
|
||||
|
||||
$kg = ['K', 'KG', 'K G', 'KINDER', 'KINDERGARTEN'];
|
||||
if (in_array($g, $kg, true)) {
|
||||
return ['level' => 1, 'suffix' => '', 'classId' => 13];
|
||||
}
|
||||
|
||||
$pk = ['PK', 'P K', 'PREK', 'PRE K', 'PRE-K', 'PRE KINDER', 'PREKINDER'];
|
||||
if (in_array($g, $pk, true)) {
|
||||
return ['level' => -1, 'suffix' => '', 'classId' => null];
|
||||
}
|
||||
|
||||
if (preg_match('/^Y(?:OUTH)?(?:\s*(\d+))?$/', $g, $m)) {
|
||||
$n = isset($m[1]) && $m[1] !== '' ? max(1, (int) $m[1]) : 1;
|
||||
return [
|
||||
'level' => $this->gradeFee + $n,
|
||||
'suffix' => '',
|
||||
'classId' => null,
|
||||
];
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:GR?ADE\s*)?(\d{1,2})\s*([A-Z]*)$/', $g, $m)) {
|
||||
return [
|
||||
'level' => (int) $m[1],
|
||||
'suffix' => $m[2] ?? '',
|
||||
'classId' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return ['level' => 999, 'suffix' => '', 'classId' => null];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Invoices;
|
||||
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InvoiceManagementService
|
||||
{
|
||||
public function __construct(private InvoiceConfigService $config)
|
||||
{
|
||||
}
|
||||
|
||||
public function getManagementData(?string $schoolYear): array
|
||||
{
|
||||
$schoolYear = trim((string) ($schoolYear ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = $this->config->getSchoolYear();
|
||||
}
|
||||
|
||||
$schoolYears = DB::table('invoices')
|
||||
->select('school_year')
|
||||
->distinct()
|
||||
->orderBy('school_year', 'DESC')
|
||||
->pluck('school_year')
|
||||
->map(fn ($val) => (string) $val)
|
||||
->filter(fn ($val) => $val !== '')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (empty($schoolYears)) {
|
||||
$schoolYears = [$schoolYear];
|
||||
}
|
||||
|
||||
$invoiceData = [];
|
||||
$parents = User::getUsersByRoleAndSchoolYear('parent', $schoolYear);
|
||||
|
||||
foreach ($parents as $parent) {
|
||||
$students = Student::query()->where('parent_id', $parent['id'])->get()->map(fn ($r) => (array) $r)->all();
|
||||
|
||||
$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,
|
||||
];
|
||||
|
||||
$invoices = Invoice::getInvoicesByParentId((int) $parent['id'], $schoolYear);
|
||||
foreach ($invoices as $invoice) {
|
||||
if (!$invoice) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parentData['invoice_amount'] = (float) ($invoice['total_amount'] ?? 0);
|
||||
$parentData['last_updated'] = $invoice['updated_at'] ?? null;
|
||||
$parentData['invoice_id'] = $invoice['id'] ?? null;
|
||||
|
||||
if (!empty($invoice['issue_date'])) {
|
||||
$tzName = $this->config->getTimezone();
|
||||
$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'] = !empty($invoice['updated_at'])
|
||||
? date('Y-m-d H:i:s', strtotime($invoice['updated_at']))
|
||||
: null;
|
||||
}
|
||||
|
||||
$refund = DB::table('refunds')
|
||||
->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS refund_paid_amount')
|
||||
->where('parent_id', $parent['id'])
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->first();
|
||||
|
||||
$parentData['refund_amount'] = (float) ($refund->refund_paid_amount ?? 0.0);
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($students as $student) {
|
||||
$grade = 'N/A';
|
||||
$studentClass = DB::table('student_class')
|
||||
->where('student_id', $student['id'])
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if ($studentClass && isset($studentClass->class_section_id)) {
|
||||
$classSection = DB::table('classSection')
|
||||
->where('class_section_id', $studentClass->class_section_id)
|
||||
->first();
|
||||
if ($classSection && isset($classSection->class_section_name)) {
|
||||
$grade = $classSection->class_section_name;
|
||||
}
|
||||
}
|
||||
|
||||
$enrollments = Enrollment::query()
|
||||
->where('student_id', $student['id'])
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
|
||||
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:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($parentData['enrolledKids']) || !empty($parentData['withdrawnKids'])) {
|
||||
$invoiceData[] = $parentData;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'schoolYear' => $schoolYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
'invoices' => $invoiceData,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Invoices;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InvoicePaymentService
|
||||
{
|
||||
public function __construct(private InvoiceConfigService $config)
|
||||
{
|
||||
}
|
||||
|
||||
public function getParentInvoiceSummary(int $parentId, ?string $schoolYear): array
|
||||
{
|
||||
$currentSchoolYear = $this->config->getSchoolYear();
|
||||
|
||||
$schoolYears = DB::table('invoices')
|
||||
->select('school_year')
|
||||
->distinct()
|
||||
->where('parent_id', $parentId)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
|
||||
$selectedYear = $schoolYear ?: null;
|
||||
if (!$selectedYear) {
|
||||
$hasCurrentYear = in_array($currentSchoolYear, array_column($schoolYears, 'school_year'), true);
|
||||
$selectedYear = $hasCurrentYear ? $currentSchoolYear : (!empty($schoolYears) ? $schoolYears[0]['school_year'] : null);
|
||||
}
|
||||
|
||||
$invoices = [];
|
||||
if ($selectedYear) {
|
||||
$invoices = Invoice::getInvoicesByParentId($parentId, $selectedYear);
|
||||
}
|
||||
|
||||
$invoiceIds = array_column($invoices, 'id');
|
||||
$refunds = [];
|
||||
if (!empty($invoiceIds)) {
|
||||
$refundResults = DB::table('refunds')
|
||||
->selectRaw('invoice_id, COALESCE(SUM(refund_paid_amount),0) as refund_paid_amount')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->groupBy('invoice_id')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
|
||||
foreach ($refundResults as $row) {
|
||||
$refunds[$row['invoice_id']] = (float) ($row['refund_paid_amount'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
$lastPayments = [];
|
||||
if (!empty($invoiceIds)) {
|
||||
$paymentResults = Payment::getPaymentsByInvoice($invoiceIds);
|
||||
foreach ($paymentResults as $payment) {
|
||||
$lastPayments[$payment['invoice_id']] = [
|
||||
'last_paid_amount' => (float) ($payment['last_paid_amount'] ?? 0),
|
||||
'last_payment_date' => $payment['last_payment_date'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($invoices as &$invoice) {
|
||||
$invoice['refund_amount'] = $refunds[$invoice['id']] ?? 0.0;
|
||||
$invoice['last_paid_amount'] = $lastPayments[$invoice['id']]['last_paid_amount'] ?? 0.0;
|
||||
$invoice['last_payment_date'] = $lastPayments[$invoice['id']]['last_payment_date'] ?? null;
|
||||
}
|
||||
unset($invoice);
|
||||
|
||||
return [
|
||||
'invoices' => $invoices,
|
||||
'schoolYears' => $schoolYears,
|
||||
'selectedYear' => $selectedYear,
|
||||
'currentSchoolYear' => $currentSchoolYear,
|
||||
'dueDate' => $this->config->getDueDate(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Invoices;
|
||||
|
||||
use App\Models\AdditionalCharge;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\DiscountUsage;
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\EventCharges;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Refund;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class InvoicePdfService
|
||||
{
|
||||
public function __construct(
|
||||
private InvoiceConfigService $config,
|
||||
private InvoiceGradeService $grades
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildPdf(int $invoiceId): string
|
||||
{
|
||||
$data = $this->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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Invoices;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
|
||||
class InvoiceTuitionService
|
||||
{
|
||||
public function __construct(
|
||||
private InvoiceGradeService $grades,
|
||||
private int $gradeFee,
|
||||
private float $firstStudentFee,
|
||||
private float $secondStudentFee,
|
||||
private float $youthFee,
|
||||
private string $timezone
|
||||
) {
|
||||
}
|
||||
|
||||
public function calculateTuitionFee(array $registeredKids, array $withdrawnKids, ?string $refundDeadline): float
|
||||
{
|
||||
$refundOk = $this->isRefundAllowed($refundDeadline);
|
||||
$tuitionStudents = $refundOk ? $registeredKids : array_merge($registeredKids, $withdrawnKids);
|
||||
|
||||
return $this->calculateTotalTuitionFee($tuitionStudents);
|
||||
}
|
||||
|
||||
public function calculateTotalTuitionFee(array $students): float
|
||||
{
|
||||
foreach ($students as &$student) {
|
||||
$gradeName = ClassSection::getClassSectionNameBySectionId($student['class_section_id'] ?? null) ?? '';
|
||||
$student['grade'] = strtoupper(trim($gradeName));
|
||||
}
|
||||
unset($student);
|
||||
|
||||
$regularCount = 0;
|
||||
$youthCount = 0;
|
||||
|
||||
foreach ($students as $student) {
|
||||
$levelInfo = $this->grades->getGradeLevel($student['grade'] ?? '');
|
||||
$level = (int) ($levelInfo['level'] ?? 999);
|
||||
|
||||
if ($level > $this->gradeFee) {
|
||||
$youthCount++;
|
||||
} else {
|
||||
$regularCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$total = 0.0;
|
||||
$total += $youthCount * $this->youthFee;
|
||||
|
||||
if ($regularCount >= 2) {
|
||||
$total += $this->firstStudentFee;
|
||||
$total += ($regularCount - 1) * $this->secondStudentFee;
|
||||
} elseif ($regularCount === 1) {
|
||||
$total += $this->firstStudentFee;
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function isRefundAllowed(?string $refundDeadline): bool
|
||||
{
|
||||
if (!$refundDeadline) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$tz = new \DateTimeZone($this->timezone);
|
||||
$today = new \DateTimeImmutable('today', $tz);
|
||||
$deadline = new \DateTimeImmutable($refundDeadline, $tz);
|
||||
return $today <= $deadline;
|
||||
} catch (\Throwable $e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user