Files
alrahma_sunday_school_api/app/Services/Fees/FeeRefundDetailService.php
T
2026-06-09 01:25:14 -04:00

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;
}
}