seedFeeConfig(); $this->seedClassSection(101, '3'); $this->seedInvoiceAndPayment(99, 100.00); $service = $this->makeService(); $result = $service->calculateRefund([ [ 'student_id' => 1, 'class_section_id' => 101, 'enrollment_status' => 'withdrawn', 'admission_status' => 'accepted', 'withdrawal_date' => '2025-01-15', ], ], 99); $this->assertSame(100.0, $result['refund_amount']); $this->assertSame(1, $result['withdrawn_students']); $this->assertCount(1, $result['students']); $this->assertTrue($result['students'][0]['eligible']); $this->assertSame(350.0, $result['students'][0]['tuition_fee']); } public function test_returns_zero_when_parent_has_no_payments(): void { $this->seedFeeConfig(); $this->seedClassSection(101, '3'); $service = $this->makeService(); $result = $service->calculateRefund([ [ 'student_id' => 1, 'class_section_id' => 101, 'enrollment_status' => 'withdrawn', 'admission_status' => 'accepted', 'withdrawal_date' => '2025-01-15', ], ], 12345); $this->assertSame(0.0, $result['refund_amount']); $this->assertSame(0, $result['withdrawn_students']); $this->assertSame([], $result['students']); } public function test_returns_zero_when_no_withdrawn_students(): void { $this->seedFeeConfig(); $this->seedClassSection(101, '3'); $this->seedInvoiceAndPayment(50, 500.00); $service = $this->makeService(); $result = $service->calculateRefund([ [ 'student_id' => 1, 'class_section_id' => 101, 'enrollment_status' => 'enrolled', 'admission_status' => 'accepted', ], ], 50); $this->assertSame(0.0, $result['refund_amount']); $this->assertSame(0, $result['withdrawn_students']); } /** * Plan section 8 regression: each withdrawn student must carry the * tuition fee that was assigned during fee calculation, otherwise the * refund loop silently produces $0 for any student whose id does not * round-trip through the lookup map. */ public function test_withdrawn_student_breakdown_carries_assigned_tuition_fee(): void { $this->seedFeeConfig(); $this->seedClassSection(201, '4'); $this->seedClassSection(202, '5'); $this->seedInvoiceAndPayment(77, 1000.00); $service = $this->makeService(); $result = $service->calculateRefund([ [ 'student_id' => 10, 'class_section_id' => 201, 'enrollment_status' => 'enrolled', 'admission_status' => 'accepted', ], [ 'student_id' => 11, 'class_section_id' => 202, 'enrollment_status' => 'withdrawn', 'admission_status' => 'accepted', 'withdrawal_date' => '2025-01-15', ], ], 77); $this->assertCount(1, $result['students']); $withdrawn = $result['students'][0]; $this->assertSame(11, $withdrawn['student_id']); // First regular = $350 (grade 4), additional = $200 (grade 5) -> the // withdrawn 5th-grader is the additional regular student and must // refund using $200, not $0 or a stale lookup value. $this->assertSame(200.0, $withdrawn['tuition_fee']); $this->assertSame('additional_regular', $withdrawn['fee_category']); $this->assertGreaterThan(0, $withdrawn['refund_amount']); $this->assertTrue($withdrawn['eligible']); } public function test_multi_student_family_only_refunds_withdrawn_students(): void { $this->seedFeeConfig(); $this->seedClassSection(301, '2'); $this->seedClassSection(302, '6'); $this->seedClassSection(303, '10'); $this->seedInvoiceAndPayment(88, 1000.00); $service = $this->makeService(); $result = $service->calculateRefund([ [ 'student_id' => 21, 'class_section_id' => 301, 'enrollment_status' => 'enrolled', 'admission_status' => 'accepted', ], [ 'student_id' => 22, 'class_section_id' => 302, 'enrollment_status' => 'withdrawn', 'admission_status' => 'accepted', 'withdrawal_date' => '2025-01-15', ], [ 'student_id' => 23, 'class_section_id' => 303, 'enrollment_status' => 'withdrawn', 'admission_status' => 'accepted', 'withdrawal_date' => '2025-01-15', ], ], 88); $this->assertCount(2, $result['students']); $this->assertSame(2, $result['withdrawn_students']); $byStudent = []; foreach ($result['students'] as $row) { $byStudent[$row['student_id']] = $row; } $this->assertSame('additional_regular', $byStudent[22]['fee_category']); $this->assertSame(200.0, $byStudent[22]['tuition_fee']); $this->assertSame('youth', $byStudent[23]['fee_category']); $this->assertSame(180.0, $byStudent[23]['tuition_fee']); $sumPerStudent = round(array_sum(array_column($result['students'], 'refund_amount')), 2); $this->assertSame($result['refund_amount'], $sumPerStudent); } public function test_withdrawal_after_deadline_is_not_eligible(): void { $this->seedFeeConfig(); $this->seedClassSection(401, '3'); $this->seedInvoiceAndPayment(33, 500.00); $service = $this->makeService(); $result = $service->calculateRefund([ [ 'student_id' => 1, 'class_section_id' => 401, 'enrollment_status' => 'withdrawn', 'admission_status' => 'accepted', 'withdrawal_date' => '2025-03-01', ], ], 33); $this->assertSame(0.0, $result['refund_amount']); $this->assertCount(1, $result['students']); $this->assertFalse($result['students'][0]['eligible']); $this->assertSame('after_refund_deadline', $result['students'][0]['reason']); } public function test_missing_withdrawal_date_is_skipped_with_reason(): void { $this->seedFeeConfig(); $this->seedClassSection(501, '3'); $this->seedInvoiceAndPayment(44, 500.00); $service = $this->makeService(); $result = $service->calculateRefund([ [ 'student_id' => 1, 'class_section_id' => 501, 'enrollment_status' => 'withdrawn', 'admission_status' => 'accepted', ], ], 44); $this->assertSame(0.0, $result['refund_amount']); $this->assertSame('missing_withdrawal_date', $result['students'][0]['reason']); } private function makeService(): FeeRefundCalculatorService { return new FeeRefundCalculatorService( new FeeConfigService, new FeeStudentFeeService(new FeeGradeService, new FeeConfigService) ); } private function seedFeeConfig(): void { DB::table('configuration')->insert([ ['config_key' => 'school_year', 'config_value' => '2025-2026'], ['config_key' => 'refund_deadline', 'config_value' => '2025-02-01'], ['config_key' => 'weeks_study', 'config_value' => '8'], ['config_key' => 'last_school_day', 'config_value' => '2025-06-01'], ['config_key' => 'first_student_fee', 'config_value' => '350'], ['config_key' => 'second_student_fee', 'config_value' => '200'], ['config_key' => 'youth_fee', 'config_value' => '180'], ]); } private function seedClassSection(int $sectionId, string $gradeName): void { DB::table('classSection')->insert([ 'class_id' => $sectionId, 'class_section_id' => $sectionId, 'class_section_name' => $gradeName, 'semester' => 'Fall', 'school_year' => '2025-2026', ]); } private function seedInvoiceAndPayment(int $parentId, float $amount): void { $invoiceId = DB::table('invoices')->insertGetId([ 'parent_id' => $parentId, 'invoice_number' => 'INV-'.$parentId, 'total_amount' => $amount, 'balance' => 0.00, 'paid_amount' => $amount, 'issue_date' => '2025-01-01', 'school_year' => '2025-2026', 'semester' => 'Fall', ]); DB::table('payments')->insert([ 'parent_id' => $parentId, 'invoice_id' => $invoiceId, 'paid_amount' => $amount, 'total_amount' => $amount, 'balance' => 0.00, 'number_of_installments' => 1, 'payment_date' => '2025-01-10', 'school_year' => '2025-2026', 'status' => 'Paid', ]); } }