330 lines
13 KiB
PHP
330 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Fees;
|
|
|
|
use App\Models\ClassSection;
|
|
use App\Models\Configuration;
|
|
use App\Models\DiscountUsage;
|
|
use App\Models\Invoice;
|
|
use App\Models\Payment;
|
|
use App\Models\Student;
|
|
use App\Models\StudentClass;
|
|
use App\Services\Invoices\InvoiceGradeService;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class FeeRefundDetailService
|
|
{
|
|
public function calculateTuitionSubtotalFromStudents(
|
|
array $registeredKids,
|
|
array $withdrawnKids,
|
|
string $schoolYear,
|
|
?string $refundDeadlineOverride = null
|
|
): array {
|
|
$refundDeadlineRaw = $refundDeadlineOverride ?? (string) (Configuration::getConfig('refund_deadline') ?? '');
|
|
|
|
$deadline = null;
|
|
$tz = null;
|
|
try {
|
|
$tzName = (string) (config('School')->attendance['timezone'] ?? (function_exists('user_timezone') ? user_timezone() : 'UTC'));
|
|
$tz = new \DateTimeZone($tzName);
|
|
$deadline = new \DateTimeImmutable($refundDeadlineRaw, $tz);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Refund deadline missing/invalid for tuition calc; withdrawn students will be billed.', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
$tuitionStudents = $registeredKids;
|
|
foreach ($withdrawnKids as $kid) {
|
|
$rawDate = $kid['withdrawal_date'] ?? null;
|
|
if (empty($rawDate) || ! $deadline) {
|
|
Log::warning('Missing withdrawal date or deadline; billing withdrawn student.', [
|
|
'student_id' => $kid['student_id'] ?? null,
|
|
]);
|
|
$tuitionStudents[] = $kid;
|
|
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$withdrawDate = new \DateTimeImmutable((string) $rawDate, $tz ?? new \DateTimeZone('UTC'));
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Invalid withdrawal date; billing withdrawn student.', [
|
|
'student_id' => $kid['student_id'] ?? null,
|
|
]);
|
|
$tuitionStudents[] = $kid;
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($withdrawDate->format('Y-m-d') > $deadline->format('Y-m-d')) {
|
|
$tuitionStudents[] = $kid;
|
|
}
|
|
}
|
|
|
|
$tuitionStudents = array_values(array_filter($tuitionStudents, function ($student) use ($schoolYear) {
|
|
$sid = (int) ($student['student_id'] ?? 0);
|
|
|
|
return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear);
|
|
}));
|
|
|
|
foreach ($tuitionStudents as &$student) {
|
|
$name = null;
|
|
if (! empty($student['class_section_id'])) {
|
|
$name = ClassSection::getClassSectionNameBySectionId($student['class_section_id']);
|
|
}
|
|
$student['grade_name'] = is_string($name) ? strtoupper(trim($name)) : 'N/A';
|
|
}
|
|
unset($student);
|
|
|
|
$gradeFee = (int) (Configuration::getConfig('grade_fee') ?? 9);
|
|
$firstStudentFee = (float) (Configuration::getConfig('first_student_fee') ?? 350);
|
|
$secondStudentFee = (float) (Configuration::getConfig('second_student_fee') ?? 200);
|
|
$youthFee = (float) (Configuration::getConfig('youth_fee') ?? 180);
|
|
|
|
$regularCount = 0;
|
|
$youthCount = 0;
|
|
$grades = new InvoiceGradeService($gradeFee);
|
|
foreach ($tuitionStudents as $student) {
|
|
$level = (int) ($grades->getGradeLevel($student['grade_name'] ?? '')['level'] ?? 999);
|
|
if ($level > $gradeFee) {
|
|
$youthCount++;
|
|
} else {
|
|
$regularCount++;
|
|
}
|
|
}
|
|
|
|
$tuitionSubtotal = 0.0;
|
|
$tuitionSubtotal += $youthCount * $youthFee;
|
|
if ($regularCount >= 2) {
|
|
$tuitionSubtotal += $firstStudentFee + ($regularCount - 1) * $secondStudentFee;
|
|
} elseif ($regularCount === 1) {
|
|
$tuitionSubtotal += $firstStudentFee;
|
|
}
|
|
|
|
return [
|
|
'tuition_subtotal' => round($tuitionSubtotal, 2),
|
|
'tuition_students' => $tuitionStudents,
|
|
'regular_count' => $regularCount,
|
|
'youth_count' => $youthCount,
|
|
];
|
|
}
|
|
|
|
public function calculateRefundDetails(array $students, int $parentId): array
|
|
{
|
|
$schoolYear = (string) (Configuration::getConfig('school_year') ?? '');
|
|
if ($schoolYear === '') {
|
|
return ['ok' => false, 'error' => 'Missing school_year configuration.'];
|
|
}
|
|
|
|
$refundDeadlineRaw = (string) (Configuration::getConfig('refund_deadline') ?? '');
|
|
if ($refundDeadlineRaw === '') {
|
|
return ['ok' => false, 'error' => 'Refund deadline is not configured.'];
|
|
}
|
|
|
|
try {
|
|
$refundDeadline = (new \DateTimeImmutable($refundDeadlineRaw))->format('Y-m-d');
|
|
} catch (\Throwable $e) {
|
|
return ['ok' => false, 'error' => 'Refund deadline is invalid.'];
|
|
}
|
|
|
|
$bookPriceRaw = Configuration::getConfig('book_price');
|
|
if ($bookPriceRaw === null || $bookPriceRaw === '') {
|
|
return ['ok' => false, 'error' => 'Book price is not configured.'];
|
|
}
|
|
if (! is_numeric($bookPriceRaw)) {
|
|
return ['ok' => false, 'error' => 'Book price must be numeric.'];
|
|
}
|
|
$bookPrice = round((float) $bookPriceRaw, 2);
|
|
if ($bookPrice < 0) {
|
|
return ['ok' => false, 'error' => 'Book price must be zero or greater.'];
|
|
}
|
|
|
|
$withdrawDates = [];
|
|
$registered = [];
|
|
$withdrawnEligible = [];
|
|
$withdrawnLate = [];
|
|
$hasWithdrawn = false;
|
|
foreach ($students as $student) {
|
|
$status = (string) ($student['enrollment_status'] ?? '');
|
|
if (in_array($status, ['enrolled', 'payment pending'], true)) {
|
|
$registered[] = $student;
|
|
|
|
continue;
|
|
}
|
|
if (! in_array($status, ['withdrawn', 'refund pending', 'withdraw under review'], true)) {
|
|
continue;
|
|
}
|
|
$hasWithdrawn = true;
|
|
$raw = $student['withdrawal_date'] ?? null;
|
|
if (empty($raw)) {
|
|
return ['ok' => false, 'error' => 'Missing withdrawal date for a withdrawn student.'];
|
|
}
|
|
try {
|
|
$withdrawDate = (new \DateTimeImmutable((string) $raw))->format('Y-m-d');
|
|
$withdrawDates[] = $withdrawDate;
|
|
if (strtotime($withdrawDate) <= strtotime($refundDeadline)) {
|
|
$withdrawnEligible[] = $student;
|
|
} else {
|
|
$withdrawnLate[] = $student;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
return ['ok' => false, 'error' => 'Invalid withdrawal date for a withdrawn student.'];
|
|
}
|
|
}
|
|
|
|
if (! $hasWithdrawn) {
|
|
return ['ok' => false, 'error' => 'No withdrawn students found for refund evaluation.'];
|
|
}
|
|
|
|
$withdrawDateUsed = max($withdrawDates);
|
|
if (count(array_unique($withdrawDates)) > 1) {
|
|
Log::warning('Multiple withdrawal dates detected for refund calc; using latest date.', [
|
|
'withdraw_date' => $withdrawDateUsed,
|
|
'parent_id' => $parentId,
|
|
]);
|
|
}
|
|
|
|
$totalPaid = Payment::getTotalPaidByParentId($parentId, $schoolYear);
|
|
if ($totalPaid < 0) {
|
|
return ['ok' => false, 'error' => 'Paid amount is invalid.'];
|
|
}
|
|
$totalPaid = round((float) $totalPaid, 2);
|
|
|
|
$invoiceTotal = 0.0;
|
|
$totalDiscount = 0.0;
|
|
$fullDiscount = false;
|
|
$invoice = Invoice::query()
|
|
->where('parent_id', $parentId)
|
|
->where('school_year', $schoolYear)
|
|
->orderByDesc('created_at')
|
|
->first();
|
|
if ($invoice) {
|
|
$invoiceTotal = (float) ($invoice->total_amount ?? 0.0);
|
|
$totalDiscount = DiscountUsage::getTotalDiscountByParentIdAndSchoolYear($parentId, $schoolYear);
|
|
if ($invoiceTotal > 0 && $totalDiscount >= $invoiceTotal - 0.00001) {
|
|
$fullDiscount = true;
|
|
}
|
|
}
|
|
|
|
$eligible = ! empty($withdrawnEligible);
|
|
|
|
$refundAmount = 0.0;
|
|
$debitAmount = 0.0;
|
|
$refundableTuition = 0.0;
|
|
$remainingCount = 0;
|
|
if ($eligible) {
|
|
$totalStudentCount = (int) Student::query()
|
|
->where('parent_id', $parentId)
|
|
->distinct()
|
|
->count('id');
|
|
|
|
$firstStudentFee = (float) (Configuration::getConfig('first_student_fee') ?? 350);
|
|
$secondStudentFee = (float) (Configuration::getConfig('second_student_fee') ?? 200);
|
|
|
|
$withdrawnCount = count($withdrawnEligible);
|
|
$remainingCount = max(0, $totalStudentCount - $withdrawnCount);
|
|
if ($totalStudentCount >= 2 && $remainingCount >= 1) {
|
|
$refundableTuition = round($secondStudentFee * $withdrawnCount, 2);
|
|
} elseif ($totalStudentCount >= 2 && $remainingCount === 0) {
|
|
$refundableTuition = round($firstStudentFee + max(0, $withdrawnCount - 1) * $secondStudentFee, 2);
|
|
} else {
|
|
$refundableTuition = round($firstStudentFee * $withdrawnCount, 2);
|
|
}
|
|
|
|
$refundableCash = min($totalPaid, $refundableTuition);
|
|
$raw = round($refundableCash - $bookPrice, 2);
|
|
if ($raw > 0 && ! $fullDiscount) {
|
|
$refundAmount = $raw;
|
|
} elseif ($raw < 0 && ! $fullDiscount) {
|
|
$debitAmount = abs($raw);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'ok' => true,
|
|
'eligible' => $eligible,
|
|
'withdraw_date' => $withdrawDateUsed,
|
|
'refund_deadline' => $refundDeadline,
|
|
'paid_amount' => $totalPaid,
|
|
'book_price' => $bookPrice,
|
|
'refund_amount' => $refundAmount,
|
|
'debit_amount' => $debitAmount,
|
|
'refundable_tuition' => round($refundableTuition, 2),
|
|
'withdrawn_eligible_count' => count($withdrawnEligible),
|
|
'total_student_count' => (int) ($totalStudentCount ?? 0),
|
|
'remaining_count' => $remainingCount,
|
|
'invoice_total' => round($invoiceTotal, 2),
|
|
'discount_total' => round($totalDiscount, 2),
|
|
'full_discount' => $fullDiscount,
|
|
];
|
|
}
|
|
|
|
public function calculateRefund(array $students, int $parentId): float
|
|
{
|
|
$result = $this->calculateRefundDetails($students, $parentId);
|
|
|
|
return (float) ($result['refund_amount'] ?? 0.0);
|
|
}
|
|
|
|
public function getGradeLevelInfo($grade, ?int $gradeFee = null): array
|
|
{
|
|
$resolvedGradeFee = $gradeFee ?? (int) (Configuration::getConfig('grade_fee') ?? 9);
|
|
$grades = new InvoiceGradeService($resolvedGradeFee);
|
|
|
|
return $grades->getGradeLevel($grade);
|
|
}
|
|
|
|
public function compareGrades($gradeA, $gradeB, ?int $gradeFee = null): int
|
|
{
|
|
$resolvedGradeFee = $gradeFee ?? (int) (Configuration::getConfig('grade_fee') ?? 9);
|
|
$grades = new InvoiceGradeService($resolvedGradeFee);
|
|
|
|
return $grades->compareGrades((string) $gradeA, (string) $gradeB);
|
|
}
|
|
|
|
public function calculateTotalTuitionFee(
|
|
array $students,
|
|
?int $gradeFee = null,
|
|
?float $firstStudentFee = null,
|
|
?float $secondStudentFee = null,
|
|
?float $youthFee = null
|
|
): float {
|
|
$resolvedGradeFee = $gradeFee ?? (int) (Configuration::getConfig('grade_fee') ?? 9);
|
|
$resolvedFirstFee = $firstStudentFee ?? (float) (Configuration::getConfig('first_student_fee') ?? 350);
|
|
$resolvedSecondFee = $secondStudentFee ?? (float) (Configuration::getConfig('second_student_fee') ?? 200);
|
|
$resolvedYouthFee = $youthFee ?? (float) (Configuration::getConfig('youth_fee') ?? 180);
|
|
|
|
foreach ($students as &$student) {
|
|
$gradeName = ClassSection::getClassSectionNameBySectionId($student['class_section_id'] ?? null);
|
|
$student['grade'] = strtoupper(trim((string) $gradeName));
|
|
}
|
|
unset($student);
|
|
|
|
$regularCount = 0;
|
|
$youthCount = 0;
|
|
$grades = new InvoiceGradeService($resolvedGradeFee);
|
|
|
|
foreach ($students as $student) {
|
|
$level = (int) ($grades->getGradeLevel($student['grade'] ?? '')['level'] ?? 999);
|
|
if ($level > $resolvedGradeFee) {
|
|
$youthCount++;
|
|
} else {
|
|
$regularCount++;
|
|
}
|
|
}
|
|
|
|
$total = 0.0;
|
|
$total += $youthCount * $resolvedYouthFee;
|
|
|
|
if ($regularCount >= 2) {
|
|
$total += $resolvedFirstFee;
|
|
$total += ($regularCount - 1) * $resolvedSecondFee;
|
|
} elseif ($regularCount === 1) {
|
|
$total += $resolvedFirstFee;
|
|
}
|
|
|
|
return $total;
|
|
}
|
|
}
|