update api and add more features
This commit is contained in:
@@ -22,82 +22,198 @@ class FeeRefundCalculatorService
|
||||
|
||||
$totalPaid = Payment::getTotalPaidByParentId($parentId, $schoolYear);
|
||||
if ($totalPaid <= 0) {
|
||||
return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0);
|
||||
Log::info('Refund skipped: parent has no payments', [
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0, []);
|
||||
}
|
||||
|
||||
$registered = [];
|
||||
$withdrawn = [];
|
||||
|
||||
foreach ($students as $student) {
|
||||
$status = strtolower((string) ($student['enrollment_status'] ?? ''));
|
||||
$admission = strtolower((string) ($student['admission_status'] ?? ''));
|
||||
|
||||
if (in_array($status, ['withdrawn', 'refund pending', 'withdraw under review'], true)) {
|
||||
$withdrawn[] = $student;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($status, ['enrolled', 'payment pending'], true) && $admission === 'accepted') {
|
||||
$registered[] = $student;
|
||||
}
|
||||
}
|
||||
[$registered, $withdrawn] = $this->partitionStudents($students);
|
||||
|
||||
if (empty($withdrawn)) {
|
||||
return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0);
|
||||
Log::info('Refund skipped: no withdrawn students found', [
|
||||
'parent_id' => $parentId,
|
||||
'registered_count' => count($registered),
|
||||
]);
|
||||
return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0, []);
|
||||
}
|
||||
|
||||
$allStudents = array_merge($registered, $withdrawn);
|
||||
$allStudents = $this->studentFees->assignFees($allStudents);
|
||||
|
||||
$feeByStudentId = [];
|
||||
foreach ($allStudents as $student) {
|
||||
$key = $student['student_id'] ?? null;
|
||||
if ($key === null) {
|
||||
continue;
|
||||
}
|
||||
$feeByStudentId[(string) $key] = (float) ($student['tuition_fee'] ?? 0);
|
||||
// Per the school billing plan (section 8), each student must carry the
|
||||
// tuition fee that was assigned to them during fee calculation BEFORE
|
||||
// the refund loop runs. We mark withdrawn vs registered before merging
|
||||
// so the fee tier (first/second regular) is computed against the full
|
||||
// family and we can still iterate only the withdrawn list afterwards.
|
||||
foreach ($registered as &$student) {
|
||||
$student['_is_withdrawn'] = false;
|
||||
}
|
||||
unset($student);
|
||||
foreach ($withdrawn as &$student) {
|
||||
$student['_is_withdrawn'] = true;
|
||||
}
|
||||
unset($student);
|
||||
|
||||
$allStudents = $this->studentFees->assignFees(array_merge($registered, $withdrawn));
|
||||
|
||||
$withdrawnWithFees = array_values(array_filter(
|
||||
$allStudents,
|
||||
fn (array $student): bool => !empty($student['_is_withdrawn'])
|
||||
));
|
||||
|
||||
$refundAmount = 0.0;
|
||||
$withdrawCount = 0;
|
||||
$studentBreakdown = [];
|
||||
|
||||
foreach ($withdrawn as $student) {
|
||||
foreach ($withdrawnWithFees as $student) {
|
||||
$studentId = $student['student_id'] ?? null;
|
||||
$studentFee = (float) ($student['tuition_fee'] ?? 0);
|
||||
$withdrawalDate = $student['withdrawal_date'] ?? null;
|
||||
|
||||
$entry = [
|
||||
'student_id' => $studentId,
|
||||
'grade' => $student['grade'] ?? null,
|
||||
'grade_level' => $student['grade_level'] ?? null,
|
||||
'fee_category' => $student['fee_category'] ?? null,
|
||||
'tuition_fee' => round($studentFee, 2),
|
||||
'withdrawal_date' => $withdrawalDate,
|
||||
'weeks_remaining' => 0,
|
||||
'refund_amount' => 0.0,
|
||||
'eligible' => false,
|
||||
'reason' => null,
|
||||
];
|
||||
|
||||
if (!$withdrawalDate) {
|
||||
Log::warning('Missing withdraw date for student', ['student_id' => $student['student_id'] ?? null]);
|
||||
$entry['reason'] = 'missing_withdrawal_date';
|
||||
Log::warning('Refund skipped for student: missing withdrawal date', [
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
]);
|
||||
$studentBreakdown[] = $entry;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$refundDeadline || strtotime($withdrawalDate) > strtotime($refundDeadline)) {
|
||||
$entry['reason'] = 'after_refund_deadline';
|
||||
$studentBreakdown[] = $entry;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$schoolEndDate) {
|
||||
$entry['reason'] = 'missing_school_end_date';
|
||||
Log::warning('Refund skipped for student: missing school end date', [
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
]);
|
||||
$studentBreakdown[] = $entry;
|
||||
continue;
|
||||
}
|
||||
|
||||
$withdrawDateObj = new \DateTime(date('Y-m-d', strtotime($withdrawalDate)));
|
||||
$schoolEndDateObj = new \DateTime($schoolEndDate);
|
||||
$daysRemaining = $withdrawDateObj->diff($schoolEndDateObj)->days;
|
||||
$weeksRemaining = min($weeksOfStudy, max(0, (int) ceil($daysRemaining / 7)));
|
||||
|
||||
$studentId = (string) ($student['student_id'] ?? '');
|
||||
$studentFee = $feeByStudentId[$studentId] ?? 0.0;
|
||||
|
||||
if ($studentFee <= 0 || $weeksOfStudy <= 0) {
|
||||
$entry['reason'] = 'zero_fee_or_weeks';
|
||||
Log::warning('Refund skipped for student: zero fee or weeks_of_study', [
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'student_fee' => $studentFee,
|
||||
'weeks_of_study' => $weeksOfStudy,
|
||||
]);
|
||||
$studentBreakdown[] = $entry;
|
||||
continue;
|
||||
}
|
||||
|
||||
$weeksRemaining = $this->weeksRemaining($withdrawalDate, $schoolEndDate, $weeksOfStudy);
|
||||
|
||||
// Plan section 7: Refund Amount = Student Tuition Fee / Weeks of Study * Weeks Remaining
|
||||
$proportionalRefund = ($studentFee / $weeksOfStudy) * $weeksRemaining;
|
||||
$refundAmount += $proportionalRefund;
|
||||
$withdrawCount++;
|
||||
$proportionalRefund = round(max(0.0, $proportionalRefund), 2);
|
||||
|
||||
$entry['weeks_remaining'] = $weeksRemaining;
|
||||
$entry['refund_amount'] = $proportionalRefund;
|
||||
$entry['eligible'] = $proportionalRefund > 0;
|
||||
if ($entry['eligible']) {
|
||||
$withdrawCount++;
|
||||
$refundAmount += $proportionalRefund;
|
||||
} else {
|
||||
$entry['reason'] = 'no_weeks_remaining';
|
||||
}
|
||||
|
||||
$studentBreakdown[] = $entry;
|
||||
}
|
||||
|
||||
$uncappedRefund = $refundAmount;
|
||||
if ($refundAmount > $totalPaid) {
|
||||
Log::info('Refund capped at total paid', [
|
||||
'parent_id' => $parentId,
|
||||
'uncapped' => round($uncappedRefund, 2),
|
||||
'capped' => round($totalPaid, 2),
|
||||
]);
|
||||
$refundAmount = $totalPaid;
|
||||
|
||||
// Scale per-student refunds proportionally so the breakdown
|
||||
// adds up to the capped family total.
|
||||
if ($uncappedRefund > 0) {
|
||||
$ratio = $totalPaid / $uncappedRefund;
|
||||
foreach ($studentBreakdown as &$row) {
|
||||
if (!empty($row['eligible'])) {
|
||||
$row['refund_amount'] = round($row['refund_amount'] * $ratio, 2);
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->resultPayload($refundAmount, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, $withdrawCount);
|
||||
Log::info('Refund calculation complete', [
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => $schoolYear,
|
||||
'withdrawn_total' => count($withdrawnWithFees),
|
||||
'withdrawn_eligible' => $withdrawCount,
|
||||
'refund_amount' => round($refundAmount, 2),
|
||||
'total_paid' => round($totalPaid, 2),
|
||||
]);
|
||||
|
||||
return $this->resultPayload(
|
||||
$refundAmount,
|
||||
$totalPaid,
|
||||
$refundDeadline,
|
||||
$schoolEndDate,
|
||||
$weeksOfStudy,
|
||||
$withdrawCount,
|
||||
$studentBreakdown
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>}
|
||||
*/
|
||||
private function partitionStudents(array $students): array
|
||||
{
|
||||
$registered = [];
|
||||
$withdrawn = [];
|
||||
|
||||
foreach ($students as $student) {
|
||||
if ($this->studentFees->isWithdrawn($student)) {
|
||||
$withdrawn[] = $student;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->studentFees->isBillable($student)) {
|
||||
$registered[] = $student;
|
||||
}
|
||||
}
|
||||
|
||||
return [$registered, $withdrawn];
|
||||
}
|
||||
|
||||
private function weeksRemaining(string $withdrawalDate, string $schoolEndDate, float $weeksOfStudy): int
|
||||
{
|
||||
$withdrawDateObj = new \DateTime(date('Y-m-d', strtotime($withdrawalDate)));
|
||||
$schoolEndDateObj = new \DateTime($schoolEndDate);
|
||||
|
||||
if ($withdrawDateObj > $schoolEndDateObj) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$daysRemaining = $withdrawDateObj->diff($schoolEndDateObj)->days;
|
||||
return (int) min($weeksOfStudy, max(0, (int) ceil($daysRemaining / 7)));
|
||||
}
|
||||
|
||||
private function resultPayload(
|
||||
@@ -106,7 +222,8 @@ class FeeRefundCalculatorService
|
||||
?string $refundDeadline,
|
||||
?string $schoolEndDate,
|
||||
float $weeksOfStudy,
|
||||
int $withdrawCount
|
||||
int $withdrawCount,
|
||||
array $students
|
||||
): array {
|
||||
return [
|
||||
'refund_amount' => round($refund, 2),
|
||||
@@ -115,6 +232,7 @@ class FeeRefundCalculatorService
|
||||
'school_end_date' => $schoolEndDate,
|
||||
'weeks_of_study' => $weeksOfStudy,
|
||||
'withdrawn_students' => $withdrawCount,
|
||||
'students' => $students,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user