update api and add more features

This commit is contained in:
root
2026-06-04 02:24:41 -04:00
parent fa6c9519a0
commit 4e33882ac7
131 changed files with 34596 additions and 340 deletions
+157 -11
View File
@@ -3,29 +3,46 @@
namespace App\Services\Fees;
use App\Models\ClassSection;
use Illuminate\Support\Facades\Log;
class FeeStudentFeeService
{
public const CATEGORY_FIRST_REGULAR = 'first_regular';
public const CATEGORY_ADDITIONAL_REGULAR = 'additional_regular';
public const CATEGORY_YOUTH = 'youth';
public const CATEGORY_NOT_BILLABLE = 'not_billable';
private const BILLABLE_ENROLLMENT_STATUSES = ['enrolled', 'payment pending'];
private const WITHDRAWN_ENROLLMENT_STATUSES = ['withdrawn', 'refund pending', 'withdraw under review'];
private const ACCEPTED_ADMISSION_STATUS = 'accepted';
public function __construct(
private FeeGradeService $grades,
private FeeConfigService $config
) {
}
/**
* Normalize grades, sort by grade level, and assign a tuition fee to each
* student so that downstream callers (refund, balance, invoice) can read
* the stored fee directly from the student record.
*
* Each student gets the following keys mutated/added:
* - grade (normalized)
* - grade_level
* - fee_category (first_regular | additional_regular | youth)
* - tuition_fee
*/
public function assignFees(array $students): array
{
$fees = $this->config->getFees();
$firstFee = $fees['first_student_fee'];
$secondFee = $fees['second_student_fee'];
$youthFee = $fees['youth_fee'];
$firstFee = (float) $fees['first_student_fee'];
$secondFee = (float) $fees['second_student_fee'];
$youthFee = (float) $fees['youth_fee'];
foreach ($students as &$student) {
$grade = $student['grade'] ?? null;
if ($grade === null || trim((string) $grade) === '') {
$sectionId = $student['class_section_id'] ?? null;
$grade = $sectionId ? ClassSection::getClassSectionNameBySectionId($sectionId) : null;
}
$student['grade'] = $this->grades->normalizeGrade($grade);
$student['grade'] = $this->resolveGrade($student);
$student['grade_level'] = $this->grades->getGradeLevel($student['grade']);
}
unset($student);
@@ -35,11 +52,18 @@ class FeeStudentFeeService
$regularCount = 0;
foreach ($students as &$student) {
$gradeLevel = $this->grades->getGradeLevel($student['grade'] ?? '');
$gradeLevel = (int) ($student['grade_level'] ?? 999);
if ($gradeLevel > 9) {
$student['fee_category'] = self::CATEGORY_YOUTH;
$student['tuition_fee'] = $youthFee;
} else {
$student['tuition_fee'] = ($regularCount === 0) ? $firstFee : $secondFee;
if ($regularCount === 0) {
$student['fee_category'] = self::CATEGORY_FIRST_REGULAR;
$student['tuition_fee'] = $firstFee;
} else {
$student['fee_category'] = self::CATEGORY_ADDITIONAL_REGULAR;
$student['tuition_fee'] = $secondFee;
}
$regularCount++;
}
}
@@ -59,4 +83,126 @@ class FeeStudentFeeService
return $total;
}
/**
* Return a detailed student-level + family-level tuition breakdown.
*
* Only students that are accepted and either enrolled or payment-pending
* are billed. Withdrawn students appear in the breakdown with a zero
* tuition fee and the not_billable category so callers (refund service,
* invoice generation) can still see them in one consistent payload.
*
* @return array{
* family_total: float,
* billable_count: int,
* non_billable_count: int,
* students: array<int, array<string, mixed>>,
* fees: array<string, float>
* }
*/
public function calculateBreakdown(array $students): array
{
$fees = $this->config->getFees();
$billable = [];
$nonBillable = [];
foreach ($students as $index => $student) {
$student['_original_index'] = $index;
$student['grade'] = $this->resolveGrade($student);
$student['grade_level'] = $this->grades->getGradeLevel($student['grade']);
$student['enrollment_status'] = isset($student['enrollment_status'])
? strtolower((string) $student['enrollment_status'])
: null;
$student['admission_status'] = isset($student['admission_status'])
? strtolower((string) $student['admission_status'])
: null;
if ($this->isBillable($student)) {
$billable[] = $student;
} else {
$nonBillable[] = $student;
}
}
$billable = $this->assignFees($billable);
$nonBillable = array_map(function (array $student): array {
$student['fee_category'] = self::CATEGORY_NOT_BILLABLE;
$student['tuition_fee'] = 0.0;
return $student;
}, $nonBillable);
$combined = array_merge($billable, $nonBillable);
usort($combined, fn ($a, $b) => ($a['_original_index'] ?? 0) <=> ($b['_original_index'] ?? 0));
$familyTotal = 0.0;
$studentRows = [];
foreach ($combined as $student) {
unset($student['_original_index']);
$tuitionFee = (float) ($student['tuition_fee'] ?? 0);
$discount = (float) ($student['discount_amount'] ?? 0);
$finalTuition = max(0.0, $tuitionFee - $discount);
$familyTotal += $finalTuition;
$studentRows[] = [
'student_id' => $student['student_id'] ?? null,
'student_name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
'grade' => $student['grade'] ?? null,
'grade_level' => $student['grade_level'] ?? null,
'enrollment_status' => $student['enrollment_status'] ?? null,
'admission_status' => $student['admission_status'] ?? null,
'fee_category' => $student['fee_category'] ?? null,
'tuition_fee' => round($tuitionFee, 2),
'discount_amount' => round($discount, 2),
'final_tuition_amount' => round($finalTuition, 2),
];
}
Log::info('Tuition breakdown calculated', [
'total_students' => count($studentRows),
'billable' => count($billable),
'non_billable' => count($nonBillable),
'family_total' => round($familyTotal, 2),
]);
return [
'family_total' => round($familyTotal, 2),
'billable_count' => count($billable),
'non_billable_count' => count($nonBillable),
'students' => $studentRows,
'fees' => [
'first_student_fee' => (float) $fees['first_student_fee'],
'second_student_fee' => (float) $fees['second_student_fee'],
'youth_fee' => (float) $fees['youth_fee'],
],
];
}
public function isBillable(array $student): bool
{
$status = strtolower((string) ($student['enrollment_status'] ?? ''));
$admission = strtolower((string) ($student['admission_status'] ?? ''));
return in_array($status, self::BILLABLE_ENROLLMENT_STATUSES, true)
&& $admission === self::ACCEPTED_ADMISSION_STATUS;
}
public function isWithdrawn(array $student): bool
{
$status = strtolower((string) ($student['enrollment_status'] ?? ''));
return in_array($status, self::WITHDRAWN_ENROLLMENT_STATUSES, true);
}
private function resolveGrade(array $student): string
{
$grade = $student['grade'] ?? null;
if ($grade === null || trim((string) $grade) === '') {
$sectionId = $student['class_section_id'] ?? null;
$grade = $sectionId ? ClassSection::getClassSectionNameBySectionId($sectionId) : null;
}
return $this->grades->normalizeGrade($grade);
}
}