fix financial issues

This commit is contained in:
root
2026-06-01 02:08:27 -04:00
parent 090cb88573
commit 6444b61416
56 changed files with 4196 additions and 1824 deletions
@@ -0,0 +1,99 @@
<?php
namespace Tests\App\Libraries;
use App\Libraries\FinancialAttachmentService;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\Test\CIUnitTestCase;
class FinancialAttachmentServiceTest extends CIUnitTestCase
{
private FinancialAttachmentService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = new FinancialAttachmentService();
}
public function testSaveUploadedFileReturnsNullForInvalidInput(): void
{
$this->assertNull($this->service->saveUploadedFile(null, 'payments'));
$file = $this->createMock(UploadedFile::class);
$file->method('isValid')->willReturn(false);
$this->assertNull($this->service->saveUploadedFile($file, 'payments'));
}
public function testSaveUploadedFileRejectsUnsupportedMimeType(): void
{
$file = $this->createMock(UploadedFile::class);
$file->method('isValid')->willReturn(true);
$file->method('hasMoved')->willReturn(false);
$file->method('getSize')->willReturn(1000);
$file->method('getMimeType')->willReturn('text/plain');
$file->method('getClientExtension')->willReturn('txt');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Unsupported file type.');
$this->service->saveUploadedFile($file, 'payments');
}
public function testSaveUploadedFileRejectsOversizedFiles(): void
{
$file = $this->createMock(UploadedFile::class);
$file->method('isValid')->willReturn(true);
$file->method('hasMoved')->willReturn(false);
$file->method('getSize')->willReturn(5242881);
$file->method('getMimeType')->willReturn('application/pdf');
$file->method('getClientExtension')->willReturn('pdf');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('File too large.');
$this->service->saveUploadedFile($file, 'payments');
}
public function testSaveUploadedFileMovesValidFilesAndReturnsRandomName(): void
{
$file = $this->createMock(UploadedFile::class);
$file->method('isValid')->willReturn(true);
$file->method('hasMoved')->willReturn(false);
$file->method('getSize')->willReturn(1024);
$file->method('getMimeType')->willReturn('application/pdf');
$file->method('getClientExtension')->willReturn('pdf');
$file->method('getRandomName')->willReturn('proof.pdf');
$file->expects($this->once())
->method('move')
->with($this->stringContains('writable/uploads/payments'), 'proof.pdf');
$this->assertSame('proof.pdf', $this->service->saveUploadedFile($file, 'payments'));
}
public function testResolvePathUsesBasenameAndReturnsNullForMissingFiles(): void
{
$dir = $this->service->ensureSubdir('receipts');
$path = $dir . DIRECTORY_SEPARATOR . 'receipt.pdf';
file_put_contents($path, 'pdf');
$this->assertSame($path, $this->service->resolvePath('receipts', '../receipt.pdf'));
$this->assertNull($this->service->resolvePath('receipts', 'missing.pdf'));
}
public function testDetectMimeReturnsKnownMimeForExistingFile(): void
{
$dir = $this->service->ensureSubdir('mime-tests');
$path = $dir . DIRECTORY_SEPARATOR . 'image.png';
file_put_contents(
$path,
base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aap8AAAAASUVORK5CYII=')
);
$mime = $this->service->detectMime($path);
$this->assertStringContainsString('image/', $mime);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Tests\App\Libraries;
use App\Libraries\Tuition\NewTuitionCalculatorService;
use CodeIgniter\Test\CIUnitTestCase;
class NewTuitionCalculatorServiceTest extends CIUnitTestCase
{
public function testFamilyDiscountsApplyByStudentPosition(): void
{
$service = new NewTuitionCalculatorService();
$result = $service->calculateFamilyTuition([
['student_id' => 4, 'student_name' => 'Fourth', 'grade_level' => '4'],
['student_id' => 1, 'student_name' => 'First', 'grade_level' => '1'],
['student_id' => 3, 'student_name' => 'Third', 'grade_level' => '3'],
['student_id' => 2, 'student_name' => 'Second', 'grade_level' => '2'],
], [
'grade_fee' => 9,
'new_tuition_full_amount' => '350.00',
'new_tuition_second_student_discount' => '50.00',
'new_tuition_third_student_discount' => '50.00',
'new_tuition_fourth_plus_discount' => '100.00',
]);
$this->assertSame('1200.00', $result['total']);
$this->assertSame('350.00', $result['details'][0]['amount']);
$this->assertSame('300.00', $result['details'][1]['amount']);
$this->assertSame('300.00', $result['details'][2]['amount']);
$this->assertSame('250.00', $result['details'][3]['amount']);
}
public function testDiscountCannotDriveAmountBelowZero(): void
{
$service = new NewTuitionCalculatorService();
$result = $service->calculateFamilyTuition([
['student_id' => 1, 'student_name' => 'One', 'grade_level' => '1'],
['student_id' => 2, 'student_name' => 'Two', 'grade_level' => '2'],
['student_id' => 3, 'student_name' => 'Three', 'grade_level' => '3'],
['student_id' => 4, 'student_name' => 'Four', 'grade_level' => '4'],
['student_id' => 5, 'student_name' => 'Five', 'grade_level' => '5'],
], [
'grade_fee' => 9,
'new_tuition_full_amount' => '75.00',
'new_tuition_second_student_discount' => '50.00',
'new_tuition_third_student_discount' => '50.00',
'new_tuition_fourth_plus_discount' => '100.00',
]);
$this->assertSame('125.00', $result['total']);
$this->assertSame('0.00', $result['details'][3]['amount']);
$this->assertSame('0.00', $result['details'][4]['amount']);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Tests\App\Libraries;
use App\Libraries\Tuition\OldTuitionCalculatorService;
use CodeIgniter\Test\CIUnitTestCase;
class OldTuitionCalculatorServiceTest extends CIUnitTestCase
{
public function testRegularAndYouthStudentsUseLegacyRules(): void
{
$service = new OldTuitionCalculatorService();
$result = $service->calculateFamilyTuition([
['student_id' => 3, 'student_name' => 'Older Youth', 'grade_level' => 'Youth 1'],
['student_id' => 1, 'student_name' => 'First Regular', 'grade_level' => '3'],
['student_id' => 2, 'student_name' => 'Second Regular', 'grade_level' => '5'],
], [
'grade_fee' => 9,
'first_student_fee' => '350.00',
'second_student_fee' => '200.00',
'youth_fee' => '180.00',
]);
$this->assertSame('730.00', $result['total']);
$this->assertSame('old_first_student_fee', $result['details'][0]['rule']);
$this->assertSame('old_second_student_fee', $result['details'][1]['rule']);
$this->assertSame('old_youth_fee', $result['details'][2]['rule']);
}
}
@@ -0,0 +1,82 @@
<?php
namespace Tests\App\Libraries;
use App\Libraries\Tuition\TuitionForecastService;
use CodeIgniter\Test\CIUnitTestCase;
class TuitionForecastServiceTest extends CIUnitTestCase
{
public function testForecastSubtractsAlreadyCollectedAndKeepsEventOnlyStudentsNonBillable(): void
{
$service = new class () extends TuitionForecastService {
public function __construct()
{
parent::__construct();
}
protected function getDefaultSchoolYear(): string
{
return '2025-2026';
}
protected function getDefaultSemester(): string
{
return '';
}
protected function loadFamilies(string $schoolYear, string $semester): array
{
return [
['parent_id' => 10, 'parent_name' => 'Yusuf, Layla'],
];
}
protected function loadFamilyStudents(int $parentId, string $schoolYear, string $semester, array $options): array
{
return [
'students' => [
['student_id' => 1, 'student_name' => 'Mariam', 'grade_level' => '1'],
['student_id' => 2, 'student_name' => 'Yahya', 'grade_level' => '2'],
],
'all_students' => [
['student_id' => 1, 'student_name' => 'Mariam', 'grade_level' => '1', 'billable' => true, 'excluded_reason' => null],
['student_id' => 2, 'student_name' => 'Yahya', 'grade_level' => '2', 'billable' => true, 'excluded_reason' => null],
['student_id' => 3, 'student_name' => 'Event Only', 'grade_level' => '3', 'billable' => false, 'excluded_reason' => 'event_only'],
],
'warnings' => ['1 event-only student(s) excluded from tuition.'],
];
}
protected function getTuitionConfig(): array
{
return [
'grade_fee' => 9,
'first_student_fee' => '350.00',
'second_student_fee' => '200.00',
'youth_fee' => '180.00',
'new_tuition_full_amount' => $this->unitPriceOverride ?? '350.00',
'new_tuition_second_student_discount' => '50.00',
'new_tuition_third_student_discount' => '50.00',
'new_tuition_fourth_plus_discount' => '100.00',
];
}
};
$result = $service->calculate('2025-2026', 'Fall', 'compare', [
'include_payment_pending' => true,
'include_withdrawn_mode' => 'refund_deadline',
'unit_price' => '400.00',
]);
$this->assertSame(1, $result['summary']['family_count']);
$this->assertSame(3, $result['summary']['student_count']);
$this->assertSame(2, $result['summary']['billable_student_count']);
$this->assertSame('550.00', $result['summary']['old_projected_tuition']);
$this->assertSame('750.00', $result['summary']['new_projected_tuition']);
$this->assertSame('550.00', $result['summary']['old_projected_income']);
$this->assertSame('750.00', $result['summary']['new_projected_income']);
$this->assertSame('400.00', $result['summary']['unit_price']);
$this->assertSame('event_only', $result['families'][0]['student_details'][2]['excluded_reason']);
}
}